mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-18 14:25:51 +00:00
Replace the 6-line stub in @ant/claude-for-chrome-mcp with the complete implementation (8 files, 3038 lines) from the reference project. Provides 17 browser tools: navigate, screenshot, click, type, read DOM, execute JS, record GIF, monitor console/network, manage tabs, etc. No feature flag needed. No changes to src/ (already matches official). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
302 lines
7.6 KiB
TypeScript
302 lines
7.6 KiB
TypeScript
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<string, unknown>,
|
|
permissionOverrides?: PermissionOverrides,
|
|
): Promise<CallToolResult> => {
|
|
// 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<string, unknown>,
|
|
permissionOverrides?: PermissionOverrides,
|
|
): Promise<CallToolResult> {
|
|
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<string, unknown>,
|
|
): Promise<CallToolResult> {
|
|
// 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<CallToolResult> {
|
|
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");
|
|
}
|