diff --git a/frontend/src/lib/components/copilot/chat/global/mcpTools.test.ts b/frontend/src/lib/components/copilot/chat/global/mcpTools.test.ts index 9e1b46589c..30a4403746 100644 --- a/frontend/src/lib/components/copilot/chat/global/mcpTools.test.ts +++ b/frontend/src/lib/components/copilot/chat/global/mcpTools.test.ts @@ -127,6 +127,12 @@ describe('read/write split', () => { expect(getTool('call_mcp_write_tool').requiresConfirmation).toBe(true) }) + it('admits search and reads in plan mode, never writes', () => { + expect(getTool('search_mcp_tools').planModeSafe).toBe(true) + expect(getTool('call_mcp_read_tool').planModeSafe).toBe(true) + expect(getTool('call_mcp_write_tool').planModeSafe).toBeFalsy() + }) + // The rejection sends the model to the write tool, which classifies from the // same cached listing: without dropping it, that retry is refused too and the // model has nowhere to go until the entry expires. diff --git a/frontend/src/lib/components/copilot/chat/global/mcpTools.ts b/frontend/src/lib/components/copilot/chat/global/mcpTools.ts index c1db1cac1b..90afa0b04b 100644 --- a/frontend/src/lib/components/copilot/chat/global/mcpTools.ts +++ b/frontend/src/lib/components/copilot/chat/global/mcpTools.ts @@ -345,8 +345,9 @@ const callMcpToolSchema = z.object({ /** * The read and write call tools differ only in which side of the `readOnlyHint` - * split they accept, and that check is what keeps a mutating call behind the - * user's confirmation — building both from one body keeps them from drifting. + * split they accept, and that check is what keeps a mutating call out of plan + * mode and behind the user's confirmation — building both from one body keeps + * them from drifting. */ function createCallTool(servers: McpServer[], mode: 'read' | 'write'): Tool<{}> { const isRead = mode === 'read' @@ -360,7 +361,7 @@ function createCallTool(servers: McpServer[], mode: 'read' | 'write'): Tool<{}> ), showDetails: true, ...(isRead - ? {} + ? { planModeSafe: true } : { requiresConfirmation: true, confirmationMessage: (args: any) => `Call ${args?.tool ?? ''} on ${args?.server ?? ''}` @@ -429,6 +430,7 @@ export function createMcpTools(servers: McpServer[]): Tool<{}>[] { 'search_mcp_tools', 'Search the tools exposed by the MCP servers connected to this workspace (listed in the system prompt). Returns server + tool names to pass to call_mcp_read_tool or call_mcp_write_tool, each with the input schema its arguments must follow.' ), + planModeSafe: true, fn: async ({ args, workspace, toolId, toolCallbacks }) => { const parsed = searchMcpToolsSchema.parse(args) toolCallbacks.setToolStatus(toolId, { content: 'Searching MCP tools...' })