跳转到内容

MCP 集成

Claude Code 对 Model Context Protocol (MCP) 提供一等支持——这是一个用于将 AI 助手连接到外部 tool 和数据源的开放标准。MCP tool 被视为与 builtin tool 平级,共享相同的分发、permission 和 hook 系统。

graph TD
    A[MCP Configuration] --> B[useManageMCPConnections]
    B --> C{Transport Type}
    C --> D[stdio - local process]
    C --> E[sse - Server-Sent Events]
    C --> F[http - Streamable HTTP]
    C --> G[ws - WebSocket]
    C --> H[sdk - SDK control]
    C --> I[sse-ide / ws-ide - IDE]
    C --> J[claudeai-proxy]

    D --> K[MCP Client]
    E --> K
    F --> K
    G --> K
    H --> K
    I --> K
    J --> K

    K --> L[ListTools]
    L --> M[MCPTool wrappers]
    M --> N[Merged with built-in tools]
    N --> O[ToolUseContext.options.tools]

    K --> P[ListResources]
    P --> Q[ListMcpResources / ReadMcpResource tools]

MCP 服务器在 settings.json 中配置(用户、项目或企业级范围)。配置定义于 src/services/mcp/types.ts

src/services/mcp/types.ts
export const TransportSchema = z.enum([
'stdio', // Local subprocess
'sse', // Server-Sent Events (HTTP/2)
'sse-ide', // IDE-specific SSE
'http', // Streamable HTTP
'ws', // WebSocket
'sdk', // SDK control transport
]);

stdio — 启动本地进程:

const McpStdioServerConfigSchema = z.object({
type: z.literal('stdio').optional(),
command: z.string().min(1),
args: z.array(z.string()).default([]),
env: z.record(z.string(), z.string()).optional(),
});

sse/http — 连接远程服务器:

const McpSSEServerConfigSchema = z.object({
type: z.literal('sse'),
url: z.string(),
headers: z.record(z.string(), z.string()).optional(),
headersHelper: z.string().optional(),
oauth: z.object({
clientId: z.string().optional(),
callbackPort: z.number().optional(),
authServerMetadataUrl: z.string().url().optional(),
}).optional(),
});

每个服务器配置携带一个范围,决定其信任级别:

export type ConfigScope = 'local' | 'user' | 'project' | 'dynamic'
| 'enterprise' | 'claudeai' | 'managed';
export type ScopedMcpServerConfig = McpServerConfig & {
scope: ConfigScope;
pluginSource?: string; // For plugin-provided servers
};
stateDiagram-v2
    [*] --> pending: Config detected
    pending --> connected: Client connects successfully
    pending --> failed: Connection error
    pending --> needs_auth: OAuth required (401)
    pending --> disabled: User disabled
    failed --> pending: Reconnect attempt
    needs_auth --> pending: OAuth flow completed
    connected --> failed: Connection lost
    connected --> pending: Reconnect (tool list changed)
    disabled --> pending: User re-enabled
src/services/mcp/types.ts
export type MCPServerConnection =
| ConnectedMCPServer // type: 'connected'
| FailedMCPServer // type: 'failed'
| NeedsAuthMCPServer // type: 'needs-auth'
| PendingMCPServer // type: 'pending'
| DisabledMCPServer // type: 'disabled'
export type ConnectedMCPServer = {
client: Client; // MCP SDK Client instance
name: string;
type: 'connected';
capabilities: ServerCapabilities;
serverInfo?: { name: string; version: string };
instructions?: string; // Server-provided instructions (capped at 2048 chars)
config: ScopedMcpServerConfig;
cleanup: () => Promise<void>;
};
src/services/mcp/useManageMCPConnections.ts
const MAX_RECONNECT_ATTEMPTS = 5;
const INITIAL_BACKOFF_MS = 1000;
const MAX_BACKOFF_MS = 30000;

断连时:

  1. 将服务器状态设为 pending,并带有 reconnectAttempt 计数器
  2. 等待 min(INITIAL_BACKOFF_MS * 2^attempt, MAX_BACKOFF_MS) 毫秒
  3. 尝试重连
  4. 若失败且 attempt < MAX_RECONNECT_ATTEMPTS,递增并重试
  5. 若所有尝试耗尽,将状态设为 failed

MCP 服务器连接后,其 tool 会被获取并包装:

// src/services/mcp/client.ts (conceptual flow)
async function fetchToolsForClient(
server: ConnectedMCPServer,
): Promise<Tool[]> {
const result: ListToolsResult = await server.client.listTools();
return result.tools.map(mcpTool => {
const normalizedName = buildMcpToolName(server.name, mcpTool.name);
return {
...MCPTool, // Base MCPTool template
name: normalizedName, // e.g., "mcp__github__create_issue"
isMcp: true,
mcpInfo: { serverName: server.name, toolName: mcpTool.name },
inputJSONSchema: mcpTool.inputSchema,
shouldDefer: mcpTool._meta?.['anthropic/deferLoading'] ?? false,
alwaysLoad: mcpTool._meta?.['anthropic/alwaysLoad'] ?? false,
async description() {
return truncate(mcpTool.description, MAX_MCP_DESCRIPTION_LENGTH);
},
async call(args, context, canUseTool, parentMessage, onProgress) {
const result = await server.client.callTool({
name: mcpTool.name,
arguments: args,
}, CallToolResultSchema, {
timeout: getMcpToolTimeoutMs(),
signal: context.abortController.signal,
onprogress: (progress) => {
onProgress?.({
toolUseID: context.toolUseId!,
data: { type: 'mcp_progress', ...progress },
});
},
});
return { data: formatMcpResult(result) };
},
};
});
}

MCP tool 通过命名空间防止冲突:

mcp__{serverName}__{toolName}

示例:名为 github 的 GitHub MCP 服务器中的 create_issue tool 会成为 mcp__github__create_issue

src/services/mcp/normalization.ts 中的规范化逻辑处理:

  • 服务器/tool 名称中的特殊字符
  • 名称长度限制
  • 冲突检测

MCP tool 和 builtin tool 共享同一分发管道:

graph TD
    A[API Response: tool_use block] --> B[findToolByName]
    B --> C{Is MCP tool?}
    C -- yes --> D[MCPTool.call → client.callTool]
    C -- no --> E[BuiltinTool.call → local execution]
    D --> F[Format MCP result]
    E --> G[Format built-in result]
    F --> H[tool_result message]
    G --> H

同一管道适用于:

  • 输入验证:MCP tool 针对 inputJSONSchema 进行验证
  • Permission 检查:相同的 checkPermissions 流程
  • PreToolUse/PostToolUse hook:相同的 hook 系统
  • 结果格式化:相同的 mapToolResultToToolResultBlockParam
  • 并发执行:MCP tool 默认是并发安全的(从 Claude Code 视角来看是只读的)

MCP tool 对 checkPermissions 使用 behavior: 'passthrough',意味着它们始终需要走标准 permission 流程:

src/tools/MCPTool/MCPTool.ts
async checkPermissions(): Promise<PermissionResult> {
return {
behavior: 'passthrough',
message: 'MCPTool requires permission.',
};
}

实际的 permission 决策经过完整链路:hook → 基于规则 → 用户提示。

MCP 服务器可以在 tool 之外暴露资源(文件、数据):

src/services/mcp/types.ts
export type ServerResource = Resource & { server: string };

两个 builtin tool 负责处理资源:

  • ListMcpResourcessrc/tools/ListMcpResourcesTool/):列出所有已连接服务器的可用资源
  • ReadMcpResourcesrc/tools/ReadMcpResourceTool/):通过 URI 读取特定资源

MCP tool 结果可包含多种内容类型。客户端会对其进行处理:

// Content types from MCP
type MCPToolResult = {
content: Array<
| { type: 'text'; text: string }
| { type: 'image'; data: string; mimeType: string }
| { type: 'resource'; resource: { uri: string; text?: string } }
>;
isError?: boolean;
_meta?: Record<string, unknown>;
structuredContent?: Record<string, unknown>;
};

大结果会被截断以防止 context 溢出:

src/utils/mcpValidation.ts
const MAX_MCP_TOOL_RESULT_SIZE = 100_000; // characters
function truncateMcpContentIfNeeded(content: MCPToolResult): MCPToolResult {
if (!mcpContentNeedsTruncation(content)) return content;
// Truncate and add "output truncated" marker
}

MCP session 可能过期(HTTP 404 + JSON-RPC code -32001)。客户端会透明处理此情况:

src/services/mcp/client.ts
export function isMcpSessionExpiredError(error: Error): boolean {
const httpStatus = 'code' in error ? error.code : undefined;
if (httpStatus !== 404) return false;
return (
error.message.includes('"code":-32001') ||
error.message.includes('"code": -32001')
);
}

Session 过期时:

  1. 清除缓存的客户端连接
  2. 通过 ensureConnectedClient 获取新客户端
  3. 重试 tool 调用

已连接的 MCP 服务器可以推送通知,触发 tool/资源刷新:

src/services/mcp/useManageMCPConnections.ts
server.client.setNotificationHandler(ToolListChangedNotificationSchema, () => {
// Re-fetch tools from server
void fetchToolsForClient(server);
});
server.client.setNotificationHandler(ResourceListChangedNotificationSchema, () => {
// Re-fetch resources from server
void fetchResourcesForClient(server);
});
server.client.setNotificationHandler(PromptListChangedNotificationSchema, () => {
// Re-fetch prompts from server
});

这实现了动态 tool 注册——MCP 服务器可以在运行时添加/删除 tool,Claude Code 会自动感知变更。

远程 MCP 服务器(SSE、HTTP)可能需要 OAuth:

src/services/mcp/auth.ts
export class ClaudeAuthProvider {
// Implements the MCP SDK's AuthProvider interface
// Handles OAuth authorization code flow
// Stores tokens in secure storage (macOS Keychain, etc.)
}

认证流程:

  1. 服务器返回 401
  2. 客户端以服务器的 OAuth 配置创建 ClaudeAuthProvider
  3. Provider 发现授权服务器元数据 URL
  4. 打开浏览器供用户授权
  5. 接收带授权码的回调
  6. 用授权码换取 token
  7. 存储 token 并重试连接

认证状态缓存 15 分钟,避免重复认证流程:

const MCP_AUTH_CACHE_TTL_MS = 15 * 60 * 1000;