From 85b2fbd61031b429a03074ba907fac1284d98da0 Mon Sep 17 00:00:00 2001 From: raystorm Date: Sun, 12 Jul 2026 07:44:59 +0800 Subject: [PATCH] fix: hide claude permission mode from mcp callers --- src/mcp.ts | 11 +++++++---- tests/mcp-tools.test.ts | 38 +++++++++++++++++++++++++++++--------- 2 files changed, 36 insertions(+), 13 deletions(-) diff --git a/src/mcp.ts b/src/mcp.ts index e85e310..d4d853a 100644 --- a/src/mcp.ts +++ b/src/mcp.ts @@ -597,6 +597,11 @@ export function createMcpServer(retinue: RetinueApi = createMcpRetinueFromEnv(), return server; } +function stripCallerPermissionMode(args: T): Omit { + const { permissionMode: _callerControlledPermissionMode, ...safeArgs } = args as T & { permissionMode?: unknown }; + return safeArgs; +} + function registerDiagnosticTools(server: McpServer): void { server.registerTool( "audit_logs", @@ -658,11 +663,10 @@ function registerBackendTools(server: McpServer, retinue: RetinueApi): void { name: z.string().optional(), resume: z.string().optional(), maxTurns: z.number().int().positive().optional(), - permissionMode: z.enum(["default", "acceptEdits", "plan", "auto", "dontAsk"]).optional(), timeoutMs: z.number().int().positive().optional() } }, - async (args) => jsonToolResult(await retinue.run(args)) + async (args) => jsonToolResult(await retinue.run(stripCallerPermissionMode(args))) ); server.registerTool( @@ -710,11 +714,10 @@ function registerBackendTools(server: McpServer, retinue: RetinueApi): void { sessionId: z.string().optional(), name: z.string().optional(), maxTurns: z.number().int().positive().optional(), - permissionMode: z.enum(["default", "acceptEdits", "plan", "auto", "dontAsk"]).optional(), timeoutMs: z.number().int().positive().optional() } }, - async (args) => jsonToolResult(await retinue.continueJob(args)) + async (args) => jsonToolResult(await retinue.continueJob(stripCallerPermissionMode(args))) ); server.registerTool( diff --git a/tests/mcp-tools.test.ts b/tests/mcp-tools.test.ts index 39d0fe3..2ba4c31 100644 --- a/tests/mcp-tools.test.ts +++ b/tests/mcp-tools.test.ts @@ -272,15 +272,32 @@ describe("MCP tools", () => { } }); - it("returns structured MCP errors for unsupported permission modes", async () => { - const connection = await connectMcpClientWithRetinue(new ClaudeRetinue({ stateDir: "unused" })); + it("does not forward caller-supplied Claude permission modes from MCP", async () => { + let runOptions: unknown; + let continueOptions: unknown; + const retinue = { + run: async (options: unknown) => { + runOptions = options; + return { jobId: "job_run", status: "running" }; + }, + continueJob: async (options: unknown) => { + continueOptions = options; + return { jobId: "job_continue", status: "running" }; + } + } as RetinueApi; + const connection = await connectMcpClientWithRetinue(retinue); try { - await expectMcpInvalidParams( - connection.client.callTool({ - name: "claude_run", - arguments: { cwd: ".", prompt: "x", permissionMode: "root" } - }) - ); + await connection.client.callTool({ + name: "claude_run", + arguments: { cwd: ".", prompt: "x", permissionMode: "dontAsk" } + }); + await connection.client.callTool({ + name: "claude_continue", + arguments: { cwd: ".", prompt: "x", permissionMode: "auto" } + }); + + expect(runOptions).toEqual({ cwd: ".", prompt: "x" }); + expect(continueOptions).toEqual({ cwd: ".", prompt: "x" }); } finally { await closeMcpClient(connection); } @@ -292,10 +309,13 @@ describe("MCP tools", () => { const tools = await connection.client.listTools(); assertRequiredFields(tools.tools, "claude_run", ["cwd", "prompt"]); + assertAbsentFields(tools.tools, "claude_run", ["permissionMode"]); assertRequiredFields(tools.tools, "claude_status", ["jobId"]); assertRequiredFields(tools.tools, "claude_wait", ["jobId"]); assertOptionalField(tools.tools, "claude_wait", "timeoutMs"); assertOptionalField(tools.tools, "claude_cleanup", "olderThanMs"); + assertRequiredFields(tools.tools, "claude_continue", ["cwd", "prompt"]); + assertAbsentFields(tools.tools, "claude_continue", ["permissionMode"]); assertRequiredFields(tools.tools, "opencode_run", ["cwd", "prompt"]); assertOptionalField(tools.tools, "opencode_run", "opencodeBaseUrl"); assertOptionalField(tools.tools, "opencode_run", "model"); @@ -2799,7 +2819,7 @@ async function connectMcpClient(daemonUrl: string) { } async function connectMcpClientWithRetinue( - retinue: ClaudeRetinue, + retinue: RetinueApi, options: boolean | CreateMcpServerOptions = true ) { const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();