diff --git a/src/workspaces/adapters/claude.spec.ts b/src/workspaces/adapters/claude.spec.ts new file mode 100644 index 000000000..f07e432cc --- /dev/null +++ b/src/workspaces/adapters/claude.spec.ts @@ -0,0 +1,41 @@ +import { describe, it, expect } from 'vitest' +import { claudeAdapter } from './claude.js' + +import type { SpawnContext } from '../cli-adapter.js' + +// Every spawn carries this flag so project-scoped `.mcp.json` servers are +// auto-trusted instead of parking at "⏸ Pending approval". See adapter comment. +const SETTINGS = ['--settings', JSON.stringify({ enableAllProjectMcpServers: true })] + +describe('claudeAdapter.composeCommand', () => { + const ctx = (resume: SpawnContext['resume']): SpawnContext => ({ + cwd: '/tmp/ws', + env: {}, + resume, + }) + + it('injects --settings (mcp auto-trust) for fresh (no resume)', () => { + expect(claudeAdapter.composeCommand(['claude'], ctx(undefined))).toEqual(['claude', ...SETTINGS]) + }) + + it('injects --settings then --resume for resume-by-id', () => { + expect(claudeAdapter.composeCommand(['claude'], ctx({ sessionId: 'abc-123' }))).toEqual([ + 'claude', + ...SETTINGS, + '--resume', + 'abc-123', + ]) + }) + + it('emits a parseable settings JSON enabling enableAllProjectMcpServers', () => { + const cmd = claudeAdapter.composeCommand(['claude'], ctx(undefined)) + const i = cmd.indexOf('--settings') + expect(i).toBeGreaterThanOrEqual(0) + expect(JSON.parse(cmd[i + 1]!)).toEqual({ enableAllProjectMcpServers: true }) + }) + + it('throws for resume="last"', () => { + // resume="last" is unsupported; see adapter comment. + expect(() => claudeAdapter.composeCommand(['claude'], ctx('last' as never))).toThrow() + }) +}) diff --git a/src/workspaces/adapters/claude.ts b/src/workspaces/adapters/claude.ts index be96cfe04..1ebcb4528 100644 --- a/src/workspaces/adapters/claude.ts +++ b/src/workspaces/adapters/claude.ts @@ -43,13 +43,30 @@ export const claudeAdapter: CliAdapter = { }, composeCommand(base: readonly string[], ctx: SpawnContext): readonly string[] { - if (ctx.resume === undefined) return base; + // Auto-trust the workspace's project-scoped `.mcp.json` servers. Without + // this, Claude Code parks every project MCP server at "⏸ Pending approval" + // (the trust gate for `.mcp.json` shared via VCS) and the agent never sees + // the OpenAlice tool surface — the symptom users hit as "MCP 啟動唔到". + // + // Injected on the command line (not via a written `.claude/settings.json`) + // to match the launcher's CLI-injection philosophy — same as the codex + // adapter's `-c mcp_servers.openalice.url=...`. Verified empirically that + // neither `--mcp-config`, `--dangerously-skip-permissions`, nor + // `--permission-mode bypassPermissions` clears this gate; only the + // `enableAllProjectMcpServers` setting does. `--settings` accepts an inline + // JSON string (PTY spawn passes argv directly, so no shell-quoting needed). + const cmd = [ + ...base, + '--settings', + JSON.stringify({ enableAllProjectMcpServers: true }), + ]; + if (ctx.resume === undefined) return cmd; if (ctx.resume === 'last') { throw new Error( 'claude adapter: "last" resume not supported — use --resume or undefined (fresh)', ); } - return [...base, '--resume', ctx.resume.sessionId]; + return [...cmd, '--resume', ctx.resume.sessionId]; }, transcriptDir(cwd: string): string {