mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-18 14:25:51 +00:00
302 lines
7.5 KiB
TypeScript
302 lines
7.5 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')
|
|
}
|