From 734d8a01d084615c94c5e378d0dc602ed6de3e03 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Thu, 24 Sep 2026 22:47:07 -0700 Subject: [PATCH] fix(exec): show MCP spawn args in trust prompt --- src/exec/runner.ts | 19 ++++++++---- tests/unit/exec/runner.test.ts | 53 ++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 6 deletions(-) diff --git a/src/exec/runner.ts b/src/exec/runner.ts index 8757ae19c..70570712f 100644 --- a/src/exec/runner.ts +++ b/src/exec/runner.ts @@ -8,6 +8,7 @@ import { type Config } from "../config/index.js"; import { shellTimeoutFromSettings, toolWatchdogFromSettings, + type MCPServerConfig, } from "../config/settings.js"; import { codexProfileFromProviderName, @@ -162,6 +163,17 @@ const logger = getLogger([LOG_NAMESPACE_ROOT, "exec"]); const SELECTED_PROVIDER_FAILURE = "SelectedProviderFailure"; +export function formatExecMcpTrustQuestion(server: MCPServerConfig): string { + return ( + `Trust local MCP server "${server.name}" for this project?` + + (server.command !== undefined + ? `\nCommand: ${server.command}${(server.args ?? []).length > 0 ? ` ${(server.args ?? []).join(" ")}` : ""}` + : server.url !== undefined + ? `\nURL: ${server.url}` + : "") + ); +} + export async function refreshSelectedProviderCredential( refresh: () => Promise, ): Promise { @@ -706,12 +718,7 @@ export async function runExec(config: Config): Promise { requestMcpTrust: async (server) => { if (!interactive) return false; const result = await promptOperator( - `Trust local MCP server "${server.name}" for this project?` + - (server.command !== undefined - ? `\nCommand: ${server.command}` - : server.url !== undefined - ? `\nURL: ${server.url}` - : ""), + formatExecMcpTrustQuestion(server), ["Trust and connect", "Deny"], true, ); diff --git a/tests/unit/exec/runner.test.ts b/tests/unit/exec/runner.test.ts index 7206f4be5..673865419 100644 --- a/tests/unit/exec/runner.test.ts +++ b/tests/unit/exec/runner.test.ts @@ -12,6 +12,7 @@ import { createExecToolCallGate, createExecToolPromoter, execUserFailureMessage, + formatExecMcpTrustQuestion, refreshSelectedProviderCredential, resolveExecDirectorOverlay, runExec, @@ -70,6 +71,58 @@ function bareConfig(task: string): Config { } as unknown as Config; } +describe("exec MCP trust prompt", () => { + test("shows a stdio command with literal space-joined args", () => { + const question = formatExecMcpTrustQuestion({ + name: "filesystem", + command: "npx", + args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp/work"], + }); + + expect(question).toBe( + 'Trust local MCP server "filesystem" for this project?\nCommand: npx -y @modelcontextprotocol/server-filesystem /tmp/work', + ); + }); + + test("shows only the stdio command when args are omitted", () => { + expect(formatExecMcpTrustQuestion({ name: "local", command: "node" })).toBe( + 'Trust local MCP server "local" for this project?\nCommand: node', + ); + }); + + test("shows only the stdio command when args are empty", () => { + expect( + formatExecMcpTrustQuestion({ name: "local", command: "node", args: [] }), + ).toBe('Trust local MCP server "local" for this project?\nCommand: node'); + }); + + test("shows an HTTP server URL", () => { + expect( + formatExecMcpTrustQuestion({ + name: "remote", + type: "http", + url: "https://mcp.example.test/api", + }), + ).toBe( + 'Trust local MCP server "remote" for this project?\nURL: https://mcp.example.test/api', + ); + }); + + test("does not show environment secrets", () => { + const question = formatExecMcpTrustQuestion({ + name: "private", + command: "private-server", + env: { API_TOKEN: "super-secret" }, + }); + + expect(question).toBe( + 'Trust local MCP server "private" for this project?\nCommand: private-server', + ); + expect(question).not.toContain("API_TOKEN"); + expect(question).not.toContain("super-secret"); + }); +}); + describe("formatCaughtError", () => { test("prefers Error.message and stringifies other values", () => { expect(formatCaughtError(new Error("disk full"))).toBe("disk full");