From 160ac0f4a30b071ee9950b651ca931c4fd3f2b41 Mon Sep 17 00:00:00 2001 From: Matthias 't Jong Date: Fri, 1 May 2026 09:28:45 +0200 Subject: [PATCH] fix: accept string "true" for approved parameter and improve approval instructions Claude Code MCP clients sometimes serialize boolean parameters as strings, causing `approved: "true"` to fail Zod validation. Use z.preprocess to coerce string booleans. Also improve the approval_required response: - Include all original request parameters in the response so the agent can easily re-send the exact same call with approved: true - Make next_action instructions more explicit about re-calling with all the same parameters Co-Authored-By: Claude Opus 4.6 (1M context) --- src/lib/server/mcp/server.ts | 4 ++-- src/lib/server/mcp/tools/api-request.ts | 4 ++-- src/lib/server/mcp/tools/ssh-exec.ts | 4 ++-- tests/integration/mcp.test.ts | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/lib/server/mcp/server.ts b/src/lib/server/mcp/server.ts index 03830fb..27bda16 100644 --- a/src/lib/server/mcp/server.ts +++ b/src/lib/server/mcp/server.ts @@ -36,7 +36,7 @@ export function registerTools(server: McpServer, token: Token) { path: z.string().describe("Path appended to target's baseUrl"), headers: z.record(z.string(), z.string()).optional().describe("Additional request headers"), body: z.union([z.string(), z.record(z.string(), z.unknown())]).optional().describe("Request body"), - approved: z.boolean().optional().describe("Set to true after user approves a guarded request"), + approved: z.preprocess(val => val === "true" || val === true, z.boolean()).optional().describe("Set to true after user approves a guarded request"), }, async (args) => { const result = await apiRequest(token, args); @@ -51,7 +51,7 @@ export function registerTools(server: McpServer, token: Token) { target: z.string().describe("Target slug"), command: z.string().describe("Shell command to execute"), timeout: z.number().optional().describe("Timeout in seconds (default 30, max 60)"), - approved: z.boolean().optional().describe("Set to true after user approves a guarded request"), + approved: z.preprocess(val => val === "true" || val === true, z.boolean()).optional().describe("Set to true after user approves a guarded request"), }, async (args) => { const result = await sshExec(token, args); diff --git a/src/lib/server/mcp/tools/api-request.ts b/src/lib/server/mcp/tools/api-request.ts index 4d2c744..47cfa5f 100644 --- a/src/lib/server/mcp/tools/api-request.ts +++ b/src/lib/server/mcp/tools/api-request.ts @@ -69,9 +69,9 @@ export async function apiRequest(token: Token, args: ApiRequestArgs) { status: "approval_required", reason: guardResult.reason, matched: guardResult.matched, - request: { type: "api", method, path }, + request: { target: targetSlug, method, path, headers, body }, next_action: - "STOP. Do NOT re-send this request yet. Present the reason to the user, wait for their explicit approval, then re-call this tool with approved: true.", + "STOP. Do NOT re-send this request yet. Present the reason to the user and explain why it was flagged. Wait for the user to explicitly approve. Only then re-call this SAME tool with all the SAME parameters (target, method, path, headers, body) AND set approved: true.", }; } } diff --git a/src/lib/server/mcp/tools/ssh-exec.ts b/src/lib/server/mcp/tools/ssh-exec.ts index 36182ed..d2d4070 100644 --- a/src/lib/server/mcp/tools/ssh-exec.ts +++ b/src/lib/server/mcp/tools/ssh-exec.ts @@ -91,9 +91,9 @@ export async function sshExec(token: Token, args: SshExecArgs) { status: "approval_required", reason: guardResult.reason, matched: guardResult.matched, - request: { type: "ssh", command }, + request: { target: targetSlug, command, timeout }, next_action: - "STOP. Do NOT re-send this request yet. You MUST present the blocked command to the user, explain what it does and why it was flagged, then wait for the user to explicitly reply with approval. Only after the user responds confirming approval may you re-call this tool with approved: true. If the user denies, abort. Never auto-approve.", + "STOP. Do NOT re-send this request yet. Present the command to the user, explain what it does and why it was flagged. Wait for the user to explicitly approve. Only then re-call this SAME tool with all the SAME parameters (target, command, timeout) AND set approved: true. If the user denies, abort. Never auto-approve.", }; } } diff --git a/tests/integration/mcp.test.ts b/tests/integration/mcp.test.ts index d98fcfb..a38627b 100644 --- a/tests/integration/mcp.test.ts +++ b/tests/integration/mcp.test.ts @@ -106,7 +106,7 @@ describe("MCP tools", () => { expect(result.status).toBe("approval_required"); expect(result.reason).toContain("rm -r"); expect(result.matched).toBe("rm -r"); - expect(result.request).toEqual({ type: "ssh", command: "rm -rf /tmp/old" }); + expect(result.request).toEqual({ target: "deployserver", command: "rm -rf /tmp/old", timeout: undefined }); expect(result.next_action).toContain("approved: true"); }); });