import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import { SocketConnectionError } from "./mcpSocketClient.js"; import type { ClaudeForChromeContext, PermissionMode, PermissionOverrides, SocketClient, } from "./types.js"; export const handleToolCall = async ( context: ClaudeForChromeContext, socketClient: SocketClient, name: string, args: Record, permissionOverrides?: PermissionOverrides, ): Promise => { // Handle permission mode changes locally (not forwarded to extension) if (name === "set_permission_mode") { return handleSetPermissionMode(socketClient, args); } // Handle switch_browser outside the normal tool call flow (manages its own connection) if (name === "switch_browser") { return handleSwitchBrowser(context, socketClient); } try { const isConnected = await socketClient.ensureConnected(); context.logger.silly( `[${context.serverName}] Server is connected: ${isConnected}. Received tool call: ${name} with args: ${JSON.stringify(args)}.`, ); if (isConnected) { return await handleToolCallConnected( context, socketClient, name, args, permissionOverrides, ); } return handleToolCallDisconnected(context); } catch (error) { context.logger.info(`[${context.serverName}] Error calling tool:`, error); if (error instanceof SocketConnectionError) { return handleToolCallDisconnected(context); } return { content: [ { type: "text", text: `Error calling tool, please try again. : ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } }; async function handleToolCallConnected( context: ClaudeForChromeContext, socketClient: SocketClient, name: string, args: Record, permissionOverrides?: PermissionOverrides, ): Promise { const response = await socketClient.callTool(name, args, permissionOverrides); context.logger.silly( `[${context.serverName}] Received result from socket bridge: ${JSON.stringify(response)}`, ); if (response === null || response === undefined) { return { content: [{ type: "text", text: "Tool execution completed" }], }; } // Response will have either result or error field const { result, error } = response as { result?: { content: unknown[] | string }; error?: { content: unknown[] | string }; }; // Determine which field has the content and whether it's an error const contentData = error || result; const isError = !!error; if (!contentData) { return { content: [{ type: "text", text: "Tool execution completed" }], }; } if (isError && isAuthenticationError(contentData.content)) { context.onAuthenticationError(); } const { content } = contentData; if (content && Array.isArray(content)) { if (isError) { return { content: content.map((item: unknown) => { if (typeof item === "object" && item !== null && "type" in item) { return item; } return { type: "text", text: String(item) }; }), isError: true, } as CallToolResult; } const convertedContent = content.map((item: unknown) => { if ( typeof item === "object" && item !== null && "type" in item && "source" in item ) { const typedItem = item; if ( typedItem.type === "image" && typeof typedItem.source === "object" && typedItem.source !== null && "data" in typedItem.source ) { return { type: "image", data: typedItem.source.data, mimeType: "media_type" in typedItem.source ? typedItem.source.media_type || "image/png" : "image/png", }; } } if (typeof item === "object" && item !== null && "type" in item) { return item; } return { type: "text", text: String(item) }; }); return { content: convertedContent, isError, } as CallToolResult; } // Handle string content if (typeof content === "string") { return { content: [{ type: "text", text: content }], isError, } as CallToolResult; } // Fallback for unexpected result format context.logger.warn( `[${context.serverName}] Unexpected result format from socket bridge`, response, ); return { content: [{ type: "text", text: JSON.stringify(response) }], isError, }; } function handleToolCallDisconnected( context: ClaudeForChromeContext, ): CallToolResult { const text = context.onToolCallDisconnected(); return { content: [{ type: "text", text }], }; } /** * Handle set_permission_mode tool call locally. * This is security-sensitive as it controls whether permission prompts are shown. */ async function handleSetPermissionMode( socketClient: SocketClient, args: Record, ): Promise { // Validate permission mode at runtime const validModes = [ "ask", "skip_all_permission_checks", "follow_a_plan", ] as const; const mode = args.mode as string | undefined; const permissionMode: PermissionMode = mode && validModes.includes(mode as PermissionMode) ? (mode as PermissionMode) : "ask"; if (socketClient.setPermissionMode) { await socketClient.setPermissionMode( permissionMode, args.allowed_domains as string[] | undefined, ); } return { content: [ { type: "text", text: `Permission mode set to: ${permissionMode}` }, ], }; } /** * Handle switch_browser tool call. Broadcasts a pairing request and blocks * until a browser responds or timeout. */ async function handleSwitchBrowser( context: ClaudeForChromeContext, socketClient: SocketClient, ): Promise { if (!context.bridgeConfig) { return { content: [ { type: "text", text: "Browser switching is only available with bridge connections.", }, ], isError: true, }; } const isConnected = await socketClient.ensureConnected(); if (!isConnected) { return handleToolCallDisconnected(context); } const result = (await socketClient.switchBrowser?.()) ?? null; if (result === "no_other_browsers") { return { content: [ { type: "text", text: "No other browsers available to switch to. Open Chrome with the Claude extension in another browser to switch.", }, ], isError: true, }; } if (result) { return { content: [ { type: "text", text: `Connected to browser "${result.name}".` }, ], }; } return { content: [ { type: "text", text: "No browser responded within the timeout. Make sure Chrome is open with the Claude extension installed, then try again.", }, ], isError: true, }; } /** * Check if the error content indicates an authentication issue */ function isAuthenticationError(content: unknown[] | string): boolean { const errorText = Array.isArray(content) ? content .map((item) => { if (typeof item === "string") return item; if ( typeof item === "object" && item !== null && "text" in item && typeof item.text === "string" ) { return item.text; } return ""; }) .join(" ") : String(content); return errorText.toLowerCase().includes("re-authenticated"); }