Skip to content
Draft
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
11 changes: 7 additions & 4 deletions src/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,11 @@ export function createMcpServer(retinue: RetinueApi = createMcpRetinueFromEnv(),
return server;
}

function stripCallerPermissionMode<T extends object>(args: T): Omit<T, "permissionMode"> {
const { permissionMode: _callerControlledPermissionMode, ...safeArgs } = args as T & { permissionMode?: unknown };
return safeArgs;
}

function registerDiagnosticTools(server: McpServer): void {
server.registerTool(
"audit_logs",
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down
38 changes: 29 additions & 9 deletions tests/mcp-tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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");
Expand Down Expand Up @@ -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();
Expand Down
Loading