Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions src/exec/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { type Config } from "../config/index.js";
import {
shellTimeoutFromSettings,
toolWatchdogFromSettings,
type MCPServerConfig,
} from "../config/settings.js";
import {
codexProfileFromProviderName,
Expand Down Expand Up @@ -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<T>(
refresh: () => Promise<T>,
): Promise<T> {
Expand Down Expand Up @@ -706,12 +718,7 @@ export async function runExec(config: Config): Promise<ExecResult> {
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,
);
Expand Down
53 changes: 53 additions & 0 deletions tests/unit/exec/runner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
createExecToolCallGate,
createExecToolPromoter,
execUserFailureMessage,
formatExecMcpTrustQuestion,
refreshSelectedProviderCredential,
resolveExecDirectorOverlay,
runExec,
Expand Down Expand Up @@ -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");
Expand Down
Loading