Files
claude-code/packages/mcp-client/src/execution.ts
claude-code-best 2fb1c9dcd8 feat: 工具层及 mcp 大重构 (#252)
* feat: 第一版大重构

* fix: 修复类型问题

* chore: 更新版本到 1.3.2

* Add brave as alternative WebSearchTool

* fix: 修正顺序

* fix: 修复对穷鬼模式的 auto dream 和 session memory 越过

* feat: 穷鬼模式去除 session-summary

* feat: 创建 builtin-tools 包,搬运所有工具实现

将 src/tools/ 下的全部 60 个工具目录迁移至 packages/builtin-tools/src/tools/,
内部导入路径已更新为 src/ alias 模式。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: 更新 src/ 中所有工具引用至 builtin-tools 包,删除 src/tools/

- src/tools.ts 及 178 个 src/ 文件的 import 路径从 ./tools/ 改为 builtin-tools/tools/
- 删除 src/tools/ 整个目录(已迁移至 packages/builtin-tools/)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: 添加 builtin-tools 路径别名至 tsconfig,更新 bun.lock

- tsconfig.json 新增 builtin-tools/* 和 builtin-tools 路径映射
- 新增 packages/builtin-tools/src 至 include

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: 为 builtin-tools、mcp-client、agent-tools 添加 @claude-code-best 作用域前缀

所有包名及 import 路径统一添加 @claude-code-best/ 前缀:
- builtin-tools → @claude-code-best/builtin-tools
- mcp-client → @claude-code-best/mcp-client
- agent-tools → @claude-code-best/agent-tools

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: 修复 node 环境没有 bun 的问题

---------

Co-authored-by: Eric-Guo <eric.guocz@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 09:52:05 +08:00

183 lines
5.1 KiB
TypeScript

// MCP tool execution — call tools on connected MCP servers
// Extracted from src/services/mcp/client.ts (callMCPTool)
import {
CallToolResultSchema,
} from '@modelcontextprotocol/sdk/types.js'
import type { ConnectedMCPServer } from './types.js'
import type { McpClientDependencies } from './interfaces.js'
import {
McpToolCallError,
McpAuthError,
} from './errors.js'
// ============================================================================
// Constants
// ============================================================================
/** Default timeout for MCP tool calls (~27.8 hours — effectively infinite) */
const DEFAULT_MCP_TOOL_TIMEOUT_MS = 100_000_000
// ============================================================================
// Tool execution
// ============================================================================
export interface CallToolOptions {
/** The connected MCP server to call */
client: ConnectedMCPServer
/** Tool name (as registered on the server, not the fully qualified name) */
tool: string
/** Tool arguments */
args: Record<string, unknown>
/** Optional metadata to send with the call */
meta?: Record<string, unknown>
/** Abort signal for cancellation */
signal: AbortSignal
/** Progress callback */
onProgress?: (data: { progress?: number; total?: number; message?: string }) => void
/** Tool call timeout in ms (defaults to ~27.8 hours) */
timeoutMs?: number
}
export interface CallToolResult {
content: unknown
_meta?: Record<string, unknown>
structuredContent?: Record<string, unknown>
isError?: boolean
}
/**
* Call a tool on a connected MCP server with timeout and progress handling.
*
* This is the protocol-level tool execution function. The host is responsible for:
* - Session management (reconnection on expiry)
* - Result transformation (content processing, truncation, persistence)
* - Error wrapping for telemetry
*/
export async function callMcpTool(
options: CallToolOptions,
deps: McpClientDependencies,
): Promise<CallToolResult> {
const { client, tool, args, meta, signal, onProgress, timeoutMs } = options
const { name: serverName, client: mcpClient } = client
const effectiveTimeout = timeoutMs ?? getMcpToolTimeoutMs()
let progressInterval: ReturnType<typeof setInterval> | undefined
try {
deps.logger.debug(`[${serverName}] Calling MCP tool: ${tool}`)
// Progress logging for long-running tools (every 30 seconds)
progressInterval = setInterval(
() => {
deps.logger.debug(`[${serverName}] Tool '${tool}' still running`)
},
30_000,
)
const result = await Promise.race([
mcpClient.callTool(
{
name: tool,
arguments: args,
_meta: meta,
},
CallToolResultSchema,
{
signal,
timeout: effectiveTimeout,
onprogress: onProgress,
},
),
createTimeoutPromise(serverName, tool, effectiveTimeout),
])
// Handle isError in result
if ('isError' in result && result.isError) {
let errorDetails = 'Unknown error'
if (
'content' in result &&
Array.isArray(result.content) &&
result.content.length > 0
) {
const firstContent = result.content[0]
if (
firstContent &&
typeof firstContent === 'object' &&
'text' in firstContent
) {
errorDetails = (firstContent as { text: string }).text
}
}
throw new McpToolCallError(serverName, tool, errorDetails)
}
return {
content: result,
_meta: result._meta as Record<string, unknown> | undefined,
structuredContent: result.structuredContent as
| Record<string, unknown>
| undefined,
}
} catch (e) {
if (progressInterval !== undefined) {
clearInterval(progressInterval)
}
if (e instanceof Error && e.name !== 'AbortError') {
deps.logger.debug(
`[${serverName}] Tool '${tool}' failed: ${e.message}`,
)
}
// Check for 401 errors
if (e instanceof Error) {
const errorCode = 'code' in e ? (e.code as number | undefined) : undefined
if (errorCode === 401) {
throw new McpAuthError(
serverName,
`MCP server "${serverName}" requires re-authorization (token expired)`,
)
}
}
throw e
} finally {
if (progressInterval !== undefined) {
clearInterval(progressInterval)
}
}
}
// ============================================================================
// Helpers
// ============================================================================
function getMcpToolTimeoutMs(): number {
return (
parseInt(process.env.MCP_TOOL_TIMEOUT || '', 10) ||
DEFAULT_MCP_TOOL_TIMEOUT_MS
)
}
function createTimeoutPromise(
serverName: string,
tool: string,
timeoutMs: number,
): Promise<never> {
return new Promise((_, reject) => {
const timeoutId = setTimeout(
() => {
reject(
new Error(
`MCP server "${serverName}" tool "${tool}" timed out after ${Math.floor(timeoutMs / 1000)}s`,
),
)
},
timeoutMs,
)
timeoutId.unref?.()
})
}