跳转到内容

MCP Integration

此内容尚不支持你的语言。

Claude Code has first-class support for the Model Context Protocol (MCP) — an open standard for connecting AI assistants to external tools and data sources. MCP tools are treated as peers to built-in tools, sharing the same dispatch, permission, and hook systems.

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 servers are configured in settings.json at user, project, or enterprise scope. The configuration is defined in 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 — Launch a local process:

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 — Connect to a remote server:

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(),
});

Each server config carries a scope that determines its trust level:

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;

On disconnection:

  1. Set server state to pending with reconnectAttempt counter
  2. Wait min(INITIAL_BACKOFF_MS * 2^attempt, MAX_BACKOFF_MS) milliseconds
  3. Attempt reconnection
  4. If failed and attempt < MAX_RECONNECT_ATTEMPTS, increment and retry
  5. If all attempts exhausted, set state to failed

When an MCP server connects, its tools are fetched and wrapped:

// 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 tools are namespaced to prevent conflicts:

mcp__{serverName}__{toolName}

Example: A GitHub MCP server named github with a tool create_issue becomes mcp__github__create_issue.

The normalization logic in src/services/mcp/normalization.ts handles:

  • Special characters in server/tool names
  • Name length limits
  • Collision detection

MCP tools and built-in tools share the same dispatch pipeline:

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

The same pipeline applies to:

  • Input validation: MCP tools validate against inputJSONSchema
  • Permission checks: Same checkPermissions flow
  • PreToolUse/PostToolUse hooks: Same hook system
  • Result formatting: Same mapToolResultToToolResultBlockParam
  • Concurrent execution: MCP tools are concurrent-safe by default (read-only from Claude Code’s perspective)

MCP tools use behavior: 'passthrough' for checkPermissions, meaning they always require the standard permission flow:

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

The actual permission decision goes through the full chain: hooks → rule-based → user prompt.

MCP servers can expose resources (files, data) alongside tools:

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

Two built-in tools handle resources:

  • ListMcpResources (src/tools/ListMcpResourcesTool/): Lists available resources from all connected servers
  • ReadMcpResource (src/tools/ReadMcpResourceTool/): Reads a specific resource by URI

MCP tool results can contain various content types. The client processes them:

// 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>;
};

Large results are truncated to prevent context overflow:

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 sessions can expire (HTTP 404 + JSON-RPC code -32001). The client handles this transparently:

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')
);
}

On session expiry:

  1. Clear the cached client connection
  2. Get a fresh client via ensureConnectedClient
  3. Retry the tool call

Connected MCP servers can push notifications that trigger tool/resource refreshes:

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
});

This enables dynamic tool registration — an MCP server can add/remove tools at runtime and Claude Code picks up the changes automatically.

Remote MCP servers (SSE, HTTP) can require 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.)
}

The auth flow:

  1. Server returns 401
  2. Client creates ClaudeAuthProvider with the server’s OAuth config
  3. Provider discovers the authorization server metadata URL
  4. Opens browser for user authorization
  5. Receives callback with authorization code
  6. Exchanges code for tokens
  7. Stores tokens and retries connection

Auth state is cached for 15 minutes to avoid repeated auth flows:

const MCP_AUTH_CACHE_TTL_MS = 15 * 60 * 1000;