diff --git a/packages/agent/src/adapters/claude/permissions/permission-handlers.test.ts b/packages/agent/src/adapters/claude/permissions/permission-handlers.test.ts index 15a3ee00b..528a219e1 100644 --- a/packages/agent/src/adapters/claude/permissions/permission-handlers.test.ts +++ b/packages/agent/src/adapters/claude/permissions/permission-handlers.test.ts @@ -104,6 +104,35 @@ describe("canUseTool MCP approval enforcement", () => { expect(context.client.requestPermission).toHaveBeenCalled(); }); + it("tags MCP tools in the default permission flow with claudeCode.toolName so the renderer can show the server name and unwrap exec dispatch args", async () => { + setMcpToolApprovalStates({ mcp__posthog__exec: "approved" }); + + const context = createContext("mcp__posthog__exec", { + toolInput: { command: "call experiment-get-all {}" }, + }); + const result = await canUseTool(context); + + expect(result.behavior).toBe("allow"); + expect(context.client.requestPermission).toHaveBeenCalledWith( + expect.objectContaining({ + toolCall: expect.objectContaining({ + _meta: { claudeCode: { toolName: "mcp__posthog__exec" } }, + }), + }), + ); + }); + + it("does not set claudeCode._meta on non-MCP tools in the default permission flow", async () => { + const context = createContext("WebFetch", { + toolInput: { url: "https://example.com" }, + }); + await canUseTool(context); + + const call = (context.client.requestPermission as ReturnType) + .mock.calls[0]?.[0]; + expect(call?.toolCall?._meta).toBeUndefined(); + }); + it("blocks do_not_use even on read-only MCP tools", async () => { setMcpToolApprovalStates({ mcp__server__readonly_blocked: "do_not_use", diff --git a/packages/agent/src/adapters/claude/permissions/permission-handlers.ts b/packages/agent/src/adapters/claude/permissions/permission-handlers.ts index ec071bd93..485c266c8 100644 --- a/packages/agent/src/adapters/claude/permissions/permission-handlers.ts +++ b/packages/agent/src/adapters/claude/permissions/permission-handlers.ts @@ -364,6 +364,12 @@ async function handleDefaultPermissionFlow( suggestions, ); + // Tag MCP tool calls so the renderer routes them through McpPermission, + // which knows how to show `serverName - toolName (MCP)` plus the unwrapped + // PostHog exec body. Without this, the dialog falls back to DefaultPermission + // and just shows the bare tool name (e.g. "exec") with no context. + const isMcpTool = toolName.startsWith("mcp__"); + const response = await client.requestPermission({ options, sessionId, @@ -374,6 +380,7 @@ async function handleDefaultPermissionFlow( content: toolInfo.content, locations: toolInfo.locations, rawInput: { ...(toolInput as Record), toolName }, + ...(isMcpTool ? { _meta: { claudeCode: { toolName } } } : {}), }, });