目录
  1. 1. 一、MCP 是什么
  2. 2. 二、MCP 集成架构
  3. 3. 三、MCP 服务器配置
    1. 3.1. 3.1 配置来源
    2. 3.2. 3.2 配置解析
  4. 4. 四、MCP 客户端生命周期
    1. 4.1. 4.1 连接管理
    2. 4.2. 4.2 MCP 工具转 Claude Code Tool
  5. 5. 五、MCP 资源
  6. 6. 六、频道系统 (Channels)
  7. 7. 七、官方 MCP 注册表
  8. 8. 八、Elicitation 处理
  9. 9. 九、JSON-RPC 2.0 协议帧格式详解
    1. 9.1. 9.1 三种消息类型
    2. 9.2. 9.2 MCP 专有错误码
    3. 9.3. 9.3 协议握手流程
  10. 10. 十、三种传输层完整对比
    1. 10.1. 10.1 传输层架构一览
    2. 10.2. 10.2 stdio 传输
    3. 10.3. 10.3 SSE(Server-Sent Events)传输
    4. 10.4. 10.4 Streamable HTTP 传输(新规范)
    5. 10.5. 10.5 InProcessTransport(同进程传输)
  11. 11. 十一、认证机制深度解析
    1. 11.1. 11.1 认证架构总览
    2. 11.2. 11.2 ClaudeAuthProvider:OAuthClientProvider 实现
    3. 11.3. 11.3 令牌获取与刷新逻辑(tokens() 方法)
    4. 11.4. 11.4 OAuth 流程(oauthPort.ts + auth.ts)
    5. 11.5. 11.5 Step-up 认证(wrapFetchWithStepUpDetection)
    6. 11.6. 11.6 XAA(Cross-App Access)企业无浏览器认证
  12. 12. 十二、SdkControlTransport 机制
    1. 12.1. 12.1 架构背景
    2. 12.2. 12.2 CLI 侧:SdkControlClientTransport
    3. 12.3. 12.3 SDK 侧:SdkControlServerTransport
    4. 12.4. 12.4 使用流程
  13. 13. 十三、错误处理与重连策略
    1. 13.1. 13.1 错误分类与处理矩阵
    2. 13.2. 13.2 错误计数与重连触发
    3. 13.3. 13.3 memoize 缓存与重连机制
    4. 13.4. 13.4 会话过期与重试(HTTP Streamable 传输特有)
    5. 13.5. 13.5 并发批处理(pMap)
  14. 14. 十四、工具名称标准化(normalization.ts)
    1. 14.1. 14.1 核心规则
    2. 14.2. 14.2 工具名称构建(mcpStringUtils.ts)
    3. 14.3. 14.3 skip-prefix 模式(SDK 集成)
  15. 15. 十五、权限模型——MCP 工具进入 CC 权限系统
    1. 15.1. 15.1 权限检查接口实现
    2. 15.2. 15.2 工具注解(Annotations)与权限行为
    3. 15.3. 15.3 IDE 工具白名单
    4. 15.4. 15.4 需要认证时的特殊工具
  16. 16. 十六、channelAllowlist / channelPermissions 完整机制
    1. 16.1. 16.1 频道系统总览
    2. 16.2. 16.2 频道白名单(channelAllowlist.ts)
    3. 16.3. 16.3 频道权限中继(channelPermissions.ts)
    4. 16.4. 16.4 权限中继工作流
    5. 16.5. 16.5 输入预览截断(防止长工具输入泛洪频道)
  17. 17. 十七、MCP 认证缓存策略
    1. 17.1. 17.1 needs-auth 缓存(client.ts)
    2. 17.2. 17.2 工具/资源 LRU 缓存
  18. 18. 涉及源文件
【Claude Code源码剖析】09-MCP 协议集成

⚠️ 学习声明:本文档基于 Claude Code 2.1.88 源码分析整理,仅供个人学习研究使用,不做任何商业用途。

Model Context Protocol (MCP) 让 Claude Code 能连接外部工具服务器,无限扩展能力。


一、MCP 是什么

Model Context Protocol 是 Anthropic 提出的开放协议,定义了 LLM 应用与外部工具/资源服务器之间的标准通信方式。

Claude Code (MCP Client)  ←→  MCP Server (外部工具提供者)
├─ 数据库查询服务器
├─ GitHub API 服务器
├─ Slack 集成服务器
├─ 文件系统服务器
└─ 自定义业务逻辑

二、MCP 集成架构

services/mcp/
├── client.ts — MCP 客户端管理 (连接/断开/获取工具)
├── config.ts — MCP 配置解析 (服务器列表)
├── types.ts — MCP 类型定义
├── auth.ts — MCP 认证
├── claudeai.ts — Claude.ai 平台的 MCP 配置
├── officialRegistry.ts — 官方 MCP 服务器注册表
├── channelAllowlist.ts — 频道白名单
├── channelPermissions.ts — 频道权限
├── elicitationHandler.ts — URL elicitation 处理
├── InProcessTransport.ts — 进程内传输
├── MCPConnectionManager.tsx — 连接管理 UI 组件
├── normalization.ts — 工具名标准化
├── utils.ts — 工具函数
└── xaa.ts / xaaIdpLogin.ts — XAA 身份认证

三、MCP 服务器配置

3.1 配置来源

// ~/.claude/settings.json (全局)
// .claude/settings.json (项目)
{
"mcpServers": {
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {
"GITHUB_TOKEN": "${GITHUB_TOKEN}" // 支持环境变量展开
}
},
"postgres": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-postgres"],
"env": {
"DATABASE_URL": "postgresql://..."
}
}
}
}

3.2 配置解析

// services/mcp/config.ts
export function parseMcpConfig(configPath: string): McpServerConfig[] {
// 1. 读取 JSON 配置
// 2. 展开环境变量 (${VAR_NAME})
// 3. 验证配置格式
// 4. 去重 (claudeai + local 配置合并)
// 5. 按企业策略过滤
}

// 环境变量展开
// services/mcp/envExpansion.ts
function expandEnvVars(value: string): string {
return value.replace(/\$\{(\w+)\}/g, (_, name) => {
return process.env[name] || '';
});
}

四、MCP 客户端生命周期

应用启动


[1] 解析 MCP 配置 → 获取服务器列表


[2] 连接服务器 → spawn 子进程 / WebSocket
│ ├─ stdio 传输 (本地服务器)
│ ├─ SSE 传输 (远程服务器)
│ └─ In-Process 传输 (内嵌服务器)


[3] 获取工具列表 → tools/list


[4] 获取资源列表 → resources/list


[5] 获取命令列表 → prompts/list (可选)


[运行时] 工具调用 → tools/call → 返回结果


[退出] 断开所有连接 → 清理子进程

4.1 连接管理

// services/mcp/client.ts
export async function connectToServer(
config: McpServerConfig,
): Promise<MCPServerConnection> {
// 1. 创建传输层
const transport = config.command
? createStdioTransport(config.command, config.args, config.env)
: createSSETransport(config.url);

// 2. 建立 MCP 连接
const client = new MCPClient(transport);
await client.connect();

// 3. 获取服务器能力
const capabilities = await client.getServerCapabilities();

return {
name: config.name,
client,
capabilities,
transport,
};
}

// 获取所有 MCP 工具和命令
export async function getMcpToolsCommandsAndResources(
clients: MCPServerConnection[],
): Promise<{
tools: Tool[];
commands: Command[];
resources: ServerResource[];
}> {
const allTools: Tool[] = [];
const allCommands: Command[] = [];

for (const client of clients) {
// 获取每个服务器的工具
const serverTools = await fetchToolsForClient(client);
allTools.push(...serverTools);

// 获取每个服务器的 prompt 命令
const serverCommands = await fetchPromptsForClient(client);
allCommands.push(...serverCommands);
}

return { tools: allTools, commands: allCommands, resources: allResources };
}

4.2 MCP 工具转 Claude Code Tool

// MCP 工具被包装成标准的 Claude Code Tool
function mcpToolToClaudeCodeTool(
mcpTool: MCPToolDefinition,
client: MCPServerConnection,
): Tool {
return buildTool({
name: `mcp__${client.name}__${mcpTool.name}`,
description: mcpTool.description,
inputSchema: convertMcpSchemaToZod(mcpTool.inputSchema),

async *call(input, context) {
// 调用 MCP 服务器执行工具
const result = await client.client.callTool(mcpTool.name, input);
yield { type: 'result', content: result.content };
},

isReadOnly: () => false, // MCP 工具默认不可并行(保守策略)
needsPermissions: () => true, // 需要权限检查
});
}

五、MCP 资源

// tools/ListMcpResourcesTool — 列出可用资源
// tools/ReadMcpResourceTool — 读取资源内容

// 资源类型示例:
// - file:///path/to/file (文件)
// - postgres://table/users (数据库表)
// - github://repo/issues (GitHub issues)
// - custom://any/resource (自定义)

六、频道系统 (Channels)

MCP 服务器可以通过 “频道” 机制管理:

// bootstrap/state.ts
type ChannelEntry =
| { kind: 'plugin'; name: string; marketplace: string; dev?: boolean }
| { kind: 'server'; name: string; dev?: boolean };

// 频道白名单
// services/mcp/channelAllowlist.ts
// 只有白名单中的频道可以连接

七、官方 MCP 注册表

// services/mcp/officialRegistry.ts
// Anthropic 维护的官方 MCP 服务器列表
// 用户可以从注册表中一键安装 MCP 服务器

export async function prefetchOfficialMcpUrls(): Promise<void> {
// 预取官方注册表,加速后续安装
}

八、Elicitation 处理

// services/mcp/elicitationHandler.ts
// 当 MCP 工具需要用户额外输入时 (如 OAuth 授权 URL)

type ElicitationRequestEvent = {
serverId: string;
url: string;
message: string;
params: ElicitRequestURLParams;
};

// UI 组件: components/mcp/ElicitationDialog.tsx
// 显示一个对话框让用户完成 OAuth 等流程

九、JSON-RPC 2.0 协议帧格式详解

MCP 完全构建于 JSON-RPC 2.0 协议之上,理解三种消息类型是读懂整个通信链路的基础。

9.1 三种消息类型

// @modelcontextprotocol/sdk/types.js 中导出的核心类型
import type { JSONRPCMessage } from '@modelcontextprotocol/sdk/types.js'

// ① Request — 需要响应的请求(客户端 → 服务端 或 服务端 → 客户端)
// 格式:{ jsonrpc: "2.0", id: number|string, method: string, params?: object }
// 例:tools/list 请求
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list",
"params": {}
}

// ② Response — 对 Request 的回复(id 必须与请求一致)
// 成功响应
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"tools": [
{ "name": "search", "description": "...", "inputSchema": {...} }
]
}
}
// 错误响应
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32001, // JSON-RPC 标准错误码
"message": "Session not found"
}
}

// ③ Notification — 单向通知,无需响应(无 id 字段)
// 例:工具调用进度通知
{
"jsonrpc": "2.0",
"method": "notifications/tools/progress",
"params": {
"progress": 0.5,
"total": 1.0,
"message": "Processing..."
}
}

9.2 MCP 专有错误码

// client.ts: 关键错误码用于区分连接断开 vs 会话过期
// JSON-RPC 标准错误码范围:-32768 ~ -32000

// -32000: 通用服务器错误 / 连接关闭
// SDK 在 transport.close() 后,所有挂起的 callTool() 都以此错误拒绝
// e.message.includes('Connection closed')

// -32001: 会话不存在(MCP Streamable HTTP 规范定义)
// HTTP 服务端在会话 ID 失效时返回 HTTP 404 + 此错误码
export function isMcpSessionExpiredError(error: Error): boolean {
const httpStatus =
'code' in error ? (error as Error & { code?: number }).code : undefined
if (httpStatus !== 404) return false
// SDK 将响应体文本嵌入 error.message
return (
error.message.includes('"code":-32001') ||
error.message.includes('"code": -32001')
)
}

// -32042: URL Elicitation Required(MCP 扩展错误码)
// 工具需要用户打开 URL 才能继续(如 OAuth 授权)
// 错误 data 中包含 elicitations 数组
import { ErrorCode } from '@modelcontextprotocol/sdk/types.js'
if (error instanceof McpError &&
error.code === ErrorCode.UrlElicitationRequired) {
// 进入 URL Elicitation 重试流程
}

9.3 协议握手流程

Claude Code(Client)                    MCP Server
| |
|-- initialize {capabilities} -------->|
| clientInfo: { name, version } |
| capabilities: { roots, elicitation } |
| |
|<-- initialize result -----------------|
| serverInfo: { name, version } |
| capabilities: { tools, resources } |
| instructions: "..." |
| |
|-- initialized (notification) ------->|
| |
|-- tools/list ----------------------->|
|<-- tools/list result ----------------|

CC 在建立连接时声明的能力(client.ts:998):

const client = new Client(
{ name: 'claude-code', title: 'Claude Code', version: MACRO.VERSION },
{
capabilities: {
roots: {}, // 告知服务器支持 roots 机制(工作目录)
elicitation: {}, // 告知服务器支持 elicitation(用户交互请求)
// 注意:空对象声明能力,不填充具体字段以兼容 Java SDK 等严格实现
},
},
)

// CC 作为服务端同时监听 ListRoots 请求(服务器可反向查询根目录)
client.setRequestHandler(ListRootsRequestSchema, async () => ({
roots: [{ uri: `file://${getOriginalCwd()}` }],
}))

十、三种传输层完整对比

10.1 传输层架构一览

Transport 接口(SDK 定义):
start(): Promise<void>
send(message: JSONRPCMessage): Promise<void>
close(): Promise<void>
onmessage?: (message: JSONRPCMessage) => void
onclose?: () => void
onerror?: (error: Error) => void

CC 实现/使用了以下传输类型:

传输类型 使用场景 子进程
stdio StdioClientTransport(SDK) 本地命令行 MCP 服务器
SSE SSEClientTransport(SDK) 远程服务器(旧协议)
HTTP StreamableHTTPClientTransport(SDK) 远程服务器(新规范)
WebSocket WebSocketTransport(CC 实现) IDE 扩展
InProcess InProcessTransport(CC 实现) 同进程 MCP 服务器
SdkControl SdkControlClientTransport(CC 实现) SDK 进程内 MCP 服务器

10.2 stdio 传输

最常见的本地传输方式,CC 作为父进程 spawn 子进程,通过 stdin/stdout 通信:

// client.ts:944
transport = new StdioClientTransport({
command: finalCommand, // 可被 CLAUDE_CODE_SHELL_PREFIX 环境变量覆盖
args: finalArgs,
env: {
...subprocessEnv(), // 继承父进程环境变量
...serverRef.env, // 服务器特定环境变量
} as Record<string, string>,
stderr: 'pipe', // 捕获子进程 stderr,防止污染 UI
})

// stderr 监听(内存上限 64MB)
const stderrHandler = (data: Buffer) => {
if (stderrOutput.length < 64 * 1024 * 1024) {
stderrOutput += data.toString()
}
}
stdioTransport.stderr.on('data', stderrHandler)

// 关闭时优雅退出序列(client.ts:1435):
// SIGINT → 100ms → SIGTERM → 400ms → SIGKILL
process.kill(childPid, 'SIGINT')
// ... 轮询检查进程是否存在 ...
process.kill(childPid, 'SIGTERM')

10.3 SSE(Server-Sent Events)传输

旧版 MCP 规范使用的传输,采用长连接 SSE 流接收消息,POST 请求发送消息:

// client.ts:626
const transportOptions: SSEClientTransportOptions = {
authProvider, // ClaudeAuthProvider(见第十一章)
// POST 请求使用超时包装器(60s 每次请求刷新)
fetch: wrapFetchWithTimeout(
wrapFetchWithStepUpDetection(createFetchWithInit(), authProvider),
),
requestInit: {
headers: { 'User-Agent': getMCPUserAgent(), ...combinedHeaders },
},
}

// 关键:SSE 长连接不使用超时包装器!
// GET 请求(SSE stream)需要永久保持开放
transportOptions.eventSourceInit = {
fetch: async (url, init) => {
const tokens = await authProvider.tokens()
const authHeaders = tokens
? { Authorization: `Bearer ${tokens.access_token}` }
: {}
return fetch(url, {
...init,
headers: { ...authHeaders, ...combinedHeaders, Accept: 'text/event-stream' },
})
},
}

10.4 Streamable HTTP 传输(新规范)

MCP 2025-03-26 规范引入的新传输,单端点,POST 请求既可返回 JSON 也可返回 SSE 流:

// client.ts:471
// MCP Streamable HTTP 规范要求:POST 必须同时接受 JSON 和 SSE
const MCP_STREAMABLE_HTTP_ACCEPT = 'application/json, text/event-stream'

// CC 通过 wrapFetchWithTimeout 确保每次请求获得新的 AbortSignal
// 背景:AbortSignal.timeout() 创建一次后 60s 就失效,导致后续请求立即超时
export function wrapFetchWithTimeout(baseFetch: FetchLike): FetchLike {
return async (url, init) => {
const method = (init?.method ?? 'GET').toUpperCase()
if (method === 'GET') return baseFetch(url, init) // GET(SSE)不加超时

// 确保 Accept 头符合 MCP 规范
const headers = new Headers(init?.headers)
if (!headers.has('accept')) {
headers.set('accept', MCP_STREAMABLE_HTTP_ACCEPT)
}

// 使用 setTimeout 而非 AbortSignal.timeout()
// 原因:AbortSignal.timeout 的内部 timer 在 Bun 中懒惰 GC,
// 每次请求会遗留 ~2.4KB 原生内存长达 60s
const controller = new AbortController()
const timer = setTimeout(c => c.abort(new DOMException(...)), 60000, controller)
timer.unref?.()
// ...
}
}

10.5 InProcessTransport(同进程传输)

用于在同一进程内运行 MCP 服务器(如 Claude-in-Chrome、Computer Use),避免 spawn 325MB 子进程:

// InProcessTransport.ts — 完整实现
class InProcessTransport implements Transport {
private peer: InProcessTransport | undefined
private closed = false

async send(message: JSONRPCMessage): Promise<void> {
if (this.closed) throw new Error('Transport is closed')
// 异步投递避免同步请求/响应循环导致的调用栈溢出
queueMicrotask(() => {
this.peer?.onmessage?.(message)
})
}

async close(): Promise<void> {
if (this.closed) return
this.closed = true
this.onclose?.()
// 关闭时同时关闭对端
if (this.peer && !this.peer.closed) {
this.peer.closed = true
this.peer.onclose?.()
}
}
}

// 工厂函数:创建一对相互连接的传输
export function createLinkedTransportPair(): [Transport, Transport] {
const a = new InProcessTransport()
const b = new InProcessTransport()
a._setPeer(b)
b._setPeer(a)
return [a, b] // [clientTransport, serverTransport]
}

// client.ts 使用示例(Chrome MCP 服务器)
const { createLinkedTransportPair } = await import('./InProcessTransport.js')
const context = createChromeContext(serverRef.env)
inProcessServer = createClaudeForChromeMcpServer(context)
const [clientTransport, serverTransport] = createLinkedTransportPair()
await inProcessServer.connect(serverTransport)
transport = clientTransport // CC MCP Client 连接到 clientTransport

十一、认证机制深度解析

11.1 认证架构总览

CC 的 MCP 认证系统由四个文件协作实现:

auth.ts          — ClaudeAuthProvider (OAuthClientProvider 实现)、OAuth 流程编排
xaa.ts — XAA (Cross-App Access) 企业级无浏览器认证
xaaIdpLogin.ts — IdP OIDC 登录(XAA 的第一步)
oauthPort.ts — OAuth 回调端口选择和 redirect_uri 构建

11.2 ClaudeAuthProvider:OAuthClientProvider 实现

ClaudeAuthProvider 是 CC 对 MCP SDK OAuthClientProvider 接口的完整实现,负责令牌的持久化和刷新:

// auth.ts:1376
export class ClaudeAuthProvider implements OAuthClientProvider {
// OAuth 客户端元数据(用于 Dynamic Client Registration)
get clientMetadata(): OAuthClientMetadata {
return {
client_name: `Claude Code (${this.serverName})`,
redirect_uris: [this.redirectUri],
grant_types: ['authorization_code', 'refresh_token'],
response_types: ['code'],
token_endpoint_auth_method: 'none', // 公开客户端,无 client_secret
scope: getScopeFromMetadata(this._metadata),
}
}

// CIMD (SEP-991): 支持 URL 形式的 client_id,避免 Dynamic Client Registration
get clientMetadataUrl(): string | undefined {
return process.env.MCP_OAUTH_CLIENT_METADATA_URL ?? MCP_CLIENT_METADATA_URL
}

// 令牌存储键:基于服务器名 + 配置的 SHA-256 哈希(前16字节)
// 防止同名服务器复用令牌
}

// auth.ts:325
export function getServerKey(
serverName: string,
serverConfig: McpSSEServerConfig | McpHTTPServerConfig,
): string {
const configJson = jsonStringify({
type: serverConfig.type,
url: serverConfig.url,
headers: serverConfig.headers || {},
})
const hash = createHash('sha256').update(configJson).digest('hex').substring(0, 16)
return `${serverName}|${hash}`
}

11.3 令牌获取与刷新逻辑(tokens() 方法)

tokens() 是整个认证体系的核心,被 MCP SDK 在每次请求前调用:

// auth.ts:1540 — tokens() 决策树
async tokens(): Promise<OAuthTokens | undefined> {
// 1. XAA 静默刷新(企业模式):无浏览器,用 id_token 换 access_token
if (isXaaEnabled() && this.serverConfig.oauth?.xaa && !tokenData?.refreshToken &&
(!tokenData?.accessToken || (tokenData.expiresAt - Date.now()) / 1000 <= 300)) {
// 串行化:多个并发请求共享同一个 Promise
if (!this._refreshInProgress) {
this._refreshInProgress = this.xaaRefresh().finally(() => {
this._refreshInProgress = undefined
})
}
const refreshed = await this._refreshInProgress
if (refreshed) return refreshed
}

// 2. 令牌过期检查(300s 预刷新窗口)
const expiresIn = (tokenData.expiresAt - Date.now()) / 1000

// 3. Step-up 检测:403 insufficient_scope 时跳过 refresh_token
// RFC 6749 §6 禁止通过 refresh 提升 scope,所以直接走 PKCE 流程
const needsStepUp = this._pendingStepUpScope !== undefined &&
this._pendingStepUpScope.split(' ').some(s => !currentScopes.includes(s))

// 4. 主动刷新(距过期 < 5 分钟且有 refresh_token)
if (expiresIn <= 300 && tokenData.refreshToken && !needsStepUp) {
if (!this._refreshInProgress) {
this._refreshInProgress = this.refreshAuthorization(tokenData.refreshToken)
.finally(() => { this._refreshInProgress = undefined })
}
const refreshed = await this._refreshInProgress
if (refreshed) return refreshed
}

// 5. 返回当前令牌(step-up 时省略 refresh_token 强制 PKCE 流程)
return {
access_token: tokenData.accessToken,
refresh_token: needsStepUp ? undefined : tokenData.refreshToken,
expires_in: expiresIn,
scope: tokenData.scope,
token_type: 'Bearer',
}
}

11.4 OAuth 流程(oauthPort.ts + auth.ts)

// oauthPort.ts — 端口选择策略
// Windows: 39152-49151(避开系统动态端口 49152+)
// 其他平台: 49152-65535(标准 IANA 动态范围)
export async function findAvailablePort(): Promise<number> {
const configuredPort = getMcpOAuthCallbackPort() // MCP_OAUTH_CALLBACK_PORT 环境变量
if (configuredPort) return configuredPort

const { min, max } = REDIRECT_PORT_RANGE
// 随机选择端口(安全性优于顺序扫描)
for (let attempt = 0; attempt < 100; attempt++) {
const port = min + Math.floor(Math.random() * (max - min + 1))
// 尝试绑定验证端口可用
const testServer = createServer()
// ...
}
}

export function buildRedirectUri(port = 3118): string {
return `http://localhost:${port}/callback` // RFC 8252 §7.3: 回环地址匹配任意端口
}

OAuth 流程全程(performMCPOAuthFlow,auth.ts:847):

1. 选择回调端口(随机 / 配置 / 默认 3118)
2. 创建 ClaudeAuthProvider(handleRedirection=true)
3. 调用 sdkAuth(provider) → 触发 redirectToAuthorization
4. 打开浏览器,用户完成授权
5. 本地 HTTP 服务器(127.0.0.1:port)接收 /callback 回调
6. 验证 OAuth state(CSRF 防护)
7. 调用 sdkAuth(provider, { authorizationCode }) 完成令牌交换
8. 令牌保存到 secureStorage(macOS Keychain / Linux Secret Service)

11.5 Step-up 认证(wrapFetchWithStepUpDetection)

当 MCP 服务器返回 403 insufficient_scope 时触发:

// auth.ts:1354
// 包装 fetch 以检测 403 insufficient_scope
export function wrapFetchWithStepUpDetection(
baseFetch: FetchLike,
provider: ClaudeAuthProvider,
): FetchLike {
return async (url, init) => {
const response = await baseFetch(url, init)
if (response.status === 403) {
const wwwAuth = response.headers.get('WWW-Authenticate')
if (wwwAuth?.includes('insufficient_scope')) {
// 提取所需 scope(RFC 6750 §3,支持引号和无引号格式)
const match = wwwAuth.match(/scope=(?:"([^"]+)"|([^\s,]+))/)
const scope = match?.[1] ?? match?.[2]
if (scope) {
// 设置标志:下次 tokens() 调用时省略 refresh_token
// 强制 SDK 走 PKCE 流程而非刷新
provider.markStepUpPending(scope)
}
}
}
return response
}
}

11.6 XAA(Cross-App Access)企业无浏览器认证

XAA 是面向企业部署的认证方案(SEP-990),整合链路:

xaa.ts 实现的 4 层协议链(Layer-2 操作 + Layer-3 编排):

Layer-2: discoverProtectedResource()
→ RFC 9728: GET /.well-known/oauth-protected-resource
→ 返回 { resource, authorization_servers[] }

Layer-2: discoverAuthorizationServer()
→ RFC 8414: GET /.well-known/oauth-authorization-server
→ 验证 issuer 匹配(防止 mix-up 攻击)
→ 确保 token_endpoint 使用 HTTPS

Layer-2: requestJwtAuthorizationGrant()
→ RFC 8693 Token Exchange: id_token → ID-JAG
→ POST {grant_type: "token-exchange", subject_token: id_token, ...} 到 IdP

Layer-2: exchangeJwtAuthGrant()
→ RFC 7523 JWT Bearer: ID-JAG → access_token
→ POST {grant_type: "jwt-bearer", assertion: id-jag} 到 AS

Layer-3: performCrossAppAccess() — 编排以上四步
// xaa.ts:426
export async function performCrossAppAccess(
serverUrl: string,
config: XaaConfig,
serverName = 'xaa',
): Promise<XaaResult> {
const prm = await discoverProtectedResource(serverUrl, { fetchFn })
// 发现 AS,过滤掉不支持 jwt-bearer 的 AS
let asMeta: AuthorizationServerMetadata | undefined
for (const asUrl of prm.authorization_servers) {
const candidate = await discoverAuthorizationServer(asUrl, { fetchFn })
if (candidate.grant_types_supported &&
!candidate.grant_types_supported.includes(JWT_BEARER_GRANT)) {
continue // 跳过不支持的 AS
}
asMeta = candidate
break
}
// 根据 AS 能力选择认证方法
const authMethod = authMethods?.includes('client_secret_post') &&
!authMethods.includes('client_secret_basic')
? 'client_secret_post' : 'client_secret_basic'

const jag = await requestJwtAuthorizationGrant({...}) // id_token → ID-JAG
const tokens = await exchangeJwtAuthGrant({...}) // ID-JAG → access_token
return { ...tokens, authorizationServerUrl: asMeta.issuer }
}

十二、SdkControlTransport 机制

12.1 架构背景

SDK MCP 服务器运行在 SDK 进程内(而非 CLI 进程),不能通过 stdout/stdin 直接通信。SdkControlTransport 通过控制消息桥接两个进程:

CLI 进程                          SDK 进程
┌─────────────────────────┐ ┌─────────────────────────────────┐
│ MCP Client │ │ SDK MCP Server (in-process) │
│ ↕ (JSON-RPC) │ │ ↕ (JSON-RPC) │
│ SdkControlClientTransport│<--->│ SdkControlServerTransport │
│ ↕ (control messages) │ │ ↕ │
│ StructuredIO │ │ Query (stdin/stdout 控制通道) │
└─────────────────────────┘ └─────────────────────────────────┘
CLI stdout/stdin ←──────────────────────────────────────────

12.2 CLI 侧:SdkControlClientTransport

// SdkControlTransport.ts:60
export class SdkControlClientTransport implements Transport {
constructor(
private serverName: string,
// 回调:发送 MCP 消息并等待响应(通过 StructuredIO)
private sendMcpMessage: (serverName: string, message: JSONRPCMessage)
=> Promise<JSONRPCMessage>,
) {}

async send(message: JSONRPCMessage): Promise<void> {
if (this.isClosed) throw new Error('Transport is closed')

// 将 MCP 消息封装为控制请求,通过 stdout 发送到 SDK 进程
// sendMcpMessage 内部:{ server_name, request_id, mcp_message } → SDK
const response = await this.sendMcpMessage(this.serverName, message)

// SDK 返回响应后,通过 onmessage 传给 MCP Client
if (this.onmessage) {
this.onmessage(response)
}
}
}

12.3 SDK 侧:SdkControlServerTransport

// SdkControlTransport.ts:109
export class SdkControlServerTransport implements Transport {
constructor(
// 回调:将 MCP 响应发回 CLI(通过 Query 解析控制请求)
private sendMcpMessage: (message: JSONRPCMessage) => void,
) {}

async send(message: JSONRPCMessage): Promise<void> {
if (this.isClosed) throw new Error('Transport is closed')
// 直接将响应传回 CLI 侧的控制请求等待者
this.sendMcpMessage(message)
}
}

12.4 使用流程

// client.ts:3262 — 为 SDK MCP 服务器建立连接
export async function setupSdkMcpClients(
sdkMcpConfigs: Record<string, McpSdkServerConfig>,
sendMcpMessage: (serverName: string, message: JSONRPCMessage) => Promise<JSONRPCMessage>,
) {
for (const [name, config] of Object.entries(sdkMcpConfigs)) {
// 每个 SDK MCP 服务器使用独立的 SdkControlClientTransport
const transport = new SdkControlClientTransport(name, sendMcpMessage)

const client = new Client({ name: 'claude-code', ... }, { capabilities: {} })
await client.connect(transport)

// 连接后正常获取工具列表
const capabilities = client.getServerCapabilities()
if (capabilities?.tools) {
const sdkTools = await fetchToolsForClient(connectedClient)
}
}
}

InProcessTransport 的关键区别:

特性 InProcessTransport SdkControlTransport
跨进程 否(同进程) 是(CLI ↔ SDK 两进程)
通信机制 queueMicrotask stdout/stdin 控制消息
使用场景 Chrome MCP、Computer Use SDK 集成 MCP 服务器
消息路由 直接对端引用 通过 server_name 路由

十三、错误处理与重连策略

13.1 错误分类与处理矩阵

// client.ts:1249 — 终端性连接错误判断
const isTerminalConnectionError = (msg: string): boolean => {
return (
msg.includes('ECONNRESET') || // TCP 连接被重置
msg.includes('ETIMEDOUT') || // 连接超时
msg.includes('EPIPE') || // 断管(服务器单方面关闭)
msg.includes('EHOSTUNREACH') || // 主机不可达
msg.includes('ECONNREFUSED') || // 连接被拒绝
msg.includes('Body Timeout Error') ||
msg.includes('terminated') ||
// SSE 重连中间错误(这些可能包裹真实网络错误)
msg.includes('SSE stream disconnected') ||
msg.includes('Failed to reconnect SSE stream')
)
}

13.2 错误计数与重连触发

// client.ts:1227
let consecutiveConnectionErrors = 0
const MAX_ERRORS_BEFORE_RECONNECT = 3
let hasTriggeredClose = false

// 关闭传输并拒绝所有挂起请求(防止工具调用永久挂起)
const closeTransportAndRejectPending = (reason: string) => {
if (hasTriggeredClose) return // 防止重入
hasTriggeredClose = true
logMCPDebug(name, `Closing transport (${reason})`)
// client.close() → transport.close() → transport.onclose →
// SDK._onclose(): 拒绝所有挂起的 request handlers(McpError -32000)
// → 触发 client.onclose → 清除 memoize 缓存 → 下次调用重连
void client.close().catch(...)
}

client.onerror = (error: Error) => {
// 特殊处理:HTTP/HTTP-proxy 会话过期
if (isMcpSessionExpiredError(error)) {
closeTransportAndRejectPending('session expired')
return
}

// SSE/HTTP 最大重连次数耗尽(SDK 内部默认 maxRetries: 2)
if (error.message.includes('Maximum reconnection attempts')) {
closeTransportAndRejectPending('SSE reconnection exhausted')
return
}

// 累计终端错误 >= 3 次触发重连
if (isTerminalConnectionError(error.message)) {
consecutiveConnectionErrors++
if (consecutiveConnectionErrors >= MAX_ERRORS_BEFORE_RECONNECT) {
consecutiveConnectionErrors = 0
closeTransportAndRejectPending('max consecutive terminal errors')
}
} else {
consecutiveConnectionErrors = 0 // 非终端错误重置计数
}
}

13.3 memoize 缓存与重连机制

// client.ts:1374 — onclose 处理器:清除缓存触发重连
client.onclose = () => {
const key = getServerCacheKey(name, serverRef)

// 同时清除工具/资源缓存,防止重连后使用旧连接的缓存
fetchToolsForClient.cache.delete(name)
fetchResourcesForClient.cache.delete(name)
fetchCommandsForClient.cache.delete(name)

// 从 memoize 缓存删除此连接键
// 下次调用 connectToServer(name, config) 时将重新建立连接
connectToServer.cache.delete(key)
}

// client.ts:595 — connectToServer 本身是 memoize 函数
export const connectToServer = memoize(
async (name, serverRef, serverStats) => {
// 实际连接逻辑...
},
getServerCacheKey, // 缓存键:`${name}-${jsonStringify(serverRef)}`
)

13.4 会话过期与重试(HTTP Streamable 传输特有)

// callMCPTool 内层的重试逻辑(client.ts:1860)
const MAX_SESSION_RETRIES = 1
for (let attempt = 0; ; attempt++) {
try {
const connectedClient = await ensureConnectedClient(client)
return await callMCPToolWithUrlElicitationRetry({...})
} catch (error) {
// 会话过期错误(clearServerCache 已在 onerror 中调用)
if (error instanceof McpSessionExpiredError && attempt < MAX_SESSION_RETRIES) {
logMCPDebug(client.name, `Retrying tool '${tool.name}' after session recovery`)
continue // 循环重试,ensureConnectedClient 会获取新连接
}
throw error
}
}

13.5 并发批处理(pMap)

// client.ts:2218 — 替代固定大小批次的 pMap 动态并发
async function processBatched<T>(items, concurrency, processor) {
await pMap(items, processor, { concurrency })
// 优势:某个服务器完成后立即释放槽位
// 旧方案:await batch1 → await batch2,一个慢服务器阻塞整批
}

// 本地 vs 远程服务器使用不同并发限制
await Promise.all([
processBatched(localServers, getMcpServerConnectionBatchSize(), processServer),
// 默认并发:parseInt(MCP_SERVER_CONNECTION_BATCH_SIZE) || 3
processBatched(remoteServers, getRemoteMcpServerConnectionBatchSize(), processServer),
// 默认并发:parseInt(MCP_REMOTE_SERVER_CONNECTION_BATCH_SIZE) || 20
])

十四、工具名称标准化(normalization.ts)

14.1 核心规则

// normalization.ts — 完整实现
const CLAUDEAI_SERVER_PREFIX = 'claude.ai '

/**
* 将服务器/工具名称标准化为 API 要求的格式:^[a-zA-Z0-9_-]{1,64}$
* 所有非法字符(包括点、空格)替换为下划线
*
* 对 claude.ai 服务器额外处理:
* - 折叠连续下划线(防止干扰 __ 分隔符)
* - 去除首尾下划线
*/
export function normalizeNameForMCP(name: string): string {
let normalized = name.replace(/[^a-zA-Z0-9_-]/g, '_')
if (name.startsWith(CLAUDEAI_SERVER_PREFIX)) {
normalized = normalized.replace(/_+/g, '_').replace(/^_|_$/g, '')
}
return normalized
}

14.2 工具名称构建(mcpStringUtils.ts)

MCP 工具的全限定名(Fully Qualified Name)采用三段式结构:

// mcpStringUtils.ts

// 格式:mcp__{normalized_server_name}__{normalized_tool_name}
export function buildMcpToolName(serverName: string, toolName: string): string {
return `mcp__${normalizeNameForMCP(serverName)}__${normalizeNameForMCP(toolName)}`
}

// 例:服务器 "my.server" + 工具 "search results" → "mcp__my_server__search_results"
// 例:服务器 "claude.ai GitHub" + 工具 "list.repos" → "mcp__claude_ai_GitHub__list_repos"
// (claude.ai 前缀触发额外规范化)

// 反解析:从全限定名提取服务器名和工具名
export function mcpInfoFromString(toolString: string): {
serverName: string; toolName: string | undefined
} | null {
const parts = toolString.split('__')
const [mcpPart, serverName, ...toolNameParts] = parts
if (mcpPart !== 'mcp' || !serverName) return null
// 注意:工具名中的 __ 被保留(toolNameParts.join('__'))
const toolName = toolNameParts.length > 0 ? toolNameParts.join('__') : undefined
return { serverName, toolName }
}

// 权限检查时使用全限定名(防止 MCP 工具影响同名内置工具的 deny 规则)
export function getToolNameForPermissionCheck(tool): string {
return tool.mcpInfo
? buildMcpToolName(tool.mcpInfo.serverName, tool.mcpInfo.toolName)
: tool.name
}

14.3 skip-prefix 模式(SDK 集成)

// client.ts:1763
// 当 CLAUDE_AGENT_SDK_MCP_NO_PREFIX=1 且服务器类型为 'sdk' 时
// 工具名不加 mcp__ 前缀,允许 MCP 工具覆盖内置工具
const skipPrefix =
client.config.type === 'sdk' &&
isEnvTruthy(process.env.CLAUDE_AGENT_SDK_MCP_NO_PREFIX)

return {
name: skipPrefix ? tool.name : fullyQualifiedName, // 模型调用时使用的名称
mcpInfo: { serverName: client.name, toolName: tool.name }, // 权限检查始终用 mcpInfo
isMcp: true,
}

十五、权限模型——MCP 工具进入 CC 权限系统

15.1 权限检查接口实现

每个 MCP 工具在 fetchToolsForClient 中获得 checkPermissions 实现:

// client.ts:1814 — MCP 工具的权限检查实现
async checkPermissions() {
return {
behavior: 'passthrough' as const, // 默认:询问用户
message: 'MCPTool requires permission.',
suggestions: [
{
type: 'addRules' as const,
rules: [
{
toolName: fullyQualifiedName, // "mcp__server__tool"
ruleContent: undefined,
},
],
behavior: 'allow' as const,
destination: 'localSettings' as const, // 保存到项目级 settings
},
],
}
}

15.2 工具注解(Annotations)与权限行为

MCP 工具通过 annotations 字段声明安全属性,CC 据此调整权限逻辑:

// client.ts:1795
isConcurrencySafe() {
return tool.annotations?.readOnlyHint ?? false // 只读工具可并发
},
isReadOnly() {
return tool.annotations?.readOnlyHint ?? false // 只读工具权限提示更宽松
},
isDestructive() {
return tool.annotations?.destructiveHint ?? false // 破坏性工具需特别警告
},
isOpenWorld() {
return tool.annotations?.openWorldHint ?? false // 影响外部世界(如发邮件)
},

15.3 IDE 工具白名单

// client.ts:568
// IDE MCP 服务器只允许特定工具进入权限系统
const ALLOWED_IDE_TOOLS = ['mcp__ide__executeCode', 'mcp__ide__getDiagnostics']
function isIncludedMcpTool(tool: Tool): boolean {
return (
!tool.name.startsWith('mcp__ide__') || ALLOWED_IDE_TOOLS.includes(tool.name)
)
}
// 其他 IDE 工具(如 mcp__ide__getFile)被过滤掉,不注册到权限系统

15.4 需要认证时的特殊工具

当 MCP 服务器需要 OAuth 认证时,CC 注入 McpAuthTool 代替正常工具列表:

// client.ts:2316
if (client.type === 'needs-auth') {
onConnectionAttempt({
client,
tools: [createMcpAuthTool(name, config)], // 引导用户完成 OAuth
commands: [],
})
return
}

十六、channelAllowlist / channelPermissions 完整机制

16.1 频道系统总览

Channel(频道)是 CC 对接第三方通信平台(Telegram、iMessage、Discord 等)的机制。频道本质上是特殊的 MCP 服务器,通过 --channels 命令行参数启用:

CC 启动参数:--channels plugin:telegram@marketplace
└── 频道插件标识符(plugin:name@marketplace)

16.2 频道白名单(channelAllowlist.ts)

// channelAllowlist.ts — 基于 GrowthBook 的动态白名单

// 白名单数据从 GrowthBook 特性旗帜 'tengu_harbor_ledger' 加载
// 无需发版即可更新白名单
export function getChannelAllowlist(): ChannelAllowlistEntry[] {
const raw = getFeatureValue_CACHED_MAY_BE_STALE<unknown>('tengu_harbor_ledger', [])
return ChannelAllowlistSchema().safeParse(raw).data ?? []
}

// 频道总开关(GrowthBook 'tengu_harbor',默认 false)
export function isChannelsEnabled(): boolean {
return getFeatureValue_CACHED_MAY_BE_STALE('tengu_harbor', false)
}

// 检查插件源是否在白名单中
export function isChannelAllowlisted(pluginSource: string | undefined): boolean {
if (!pluginSource) return false
const { name, marketplace } = parsePluginIdentifier(pluginSource)
if (!marketplace) return false // 内置/内联插件永远不在白名单
return getChannelAllowlist().some(
e => e.plugin === name && e.marketplace === marketplace,
)
}

// 白名单粒度:插件级(不是服务器级)
// 理由:如果插件被攻破,其下所有服务器都已被攻破,细粒度无实际意义

16.3 频道权限中继(channelPermissions.ts)

频道的核心能力:用户可以通过 Telegram/Discord 等平台审批 CC 的权限请求:

// channelPermissions.ts

// 功能开关(独立于频道总开关,可独立部署)
export function isChannelPermissionRelayEnabled(): boolean {
return getFeatureValue_CACHED_MAY_BE_STALE('tengu_harbor_permissions', false)
}

// 权限回复格式:/^\s*(y|yes|n|no)\s+([a-km-z]{5})\s*$/i
// 5位短ID(25字母表,去掉'l'避免歧义),例:yes tbxkq
export const PERMISSION_REPLY_RE = /^\s*(y|yes|n|no)\s+([a-km-z]{5})\s*$/i

// 短ID生成:FNV-1a 哈希 → 5位字母(屏蔽词过滤)
export function shortRequestId(toolUseID: string): string {
let candidate = hashToId(toolUseID)
for (let salt = 0; salt < 10; salt++) {
if (!ID_AVOID_SUBSTRINGS.some(bad => candidate.includes(bad))) {
return candidate
}
candidate = hashToId(`${toolUseID}:${salt}`)
}
return candidate
}

// 只有满足以下全部条件的频道才能中继权限:
// 1. type === 'connected'
// 2. 在 --channels 会话白名单中
// 3. 声明 capabilities.experimental['claude/channel']
// 4. 声明 capabilities.experimental['claude/channel/permission']
export function filterPermissionRelayClients<T>(clients, isInAllowlist) {
return clients.filter(
c => c.type === 'connected' &&
isInAllowlist(c.name) &&
c.capabilities?.experimental?.['claude/channel'] !== undefined &&
c.capabilities?.experimental?.['claude/channel/permission'] !== undefined,
)
}

16.4 权限中继工作流

// createChannelPermissionCallbacks() — 工厂函数(每个 session 创建一次)
export function createChannelPermissionCallbacks(): ChannelPermissionCallbacks {
const pending = new Map<string, (response: ChannelPermissionResponse) => void>()

return {
// 注册等待者(权限请求发出时调用)
onResponse(requestId, handler) {
const key = requestId.toLowerCase()
pending.set(key, handler)
return () => { pending.delete(key) } // 返回取消订阅函数
},

// 解析(频道服务器收到用户回复时调用)
resolve(requestId, behavior, fromServer) {
const key = requestId.toLowerCase()
const resolver = pending.get(key)
if (!resolver) return false // 已超时/已解析
pending.delete(key) // 先删后调用(防止重入)
resolver({ behavior, fromServer })
return true
},
}
}

/*
安全模型说明(channelPermissions.ts 注释):
- 批准方是通过频道与 CC 连接的人类用户,不是 Claude 自身
- 信任边界是白名单(tengu_harbor_ledger),而非终端
- 被攻破的频道服务器可以伪造"yes <id>"——已接受的风险
- 但被攻破的频道已经能无限制注入对话轮次,权限自批准只是更快而非更多能力
*/

16.5 输入预览截断(防止长工具输入泛洪频道)

// channelPermissions.ts:160
export function truncateForPreview(input: unknown): string {
try {
const s = jsonStringify(input)
// 200 字符 ≈ 手机屏幕 3 行,让用户看到意图而不被大量文本淹没
return s.length > 200 ? s.slice(0, 200) + '…' : s
} catch {
return '(unserializable)'
}
}

十七、MCP 认证缓存策略

17.1 needs-auth 缓存(client.ts)

// client.ts:257 — 15分钟缓存,避免重复探测失败的认证服务器
const MCP_AUTH_CACHE_TTL_MS = 15 * 60 * 1000

// 缓存文件:~/.claude/mcp-needs-auth-cache.json
// 格式:Record<serverId, { timestamp: number }>

// 写串行化:Promise chain 防止并发 read-modify-write 竞态
let writeChain = Promise.resolve()
function setMcpAuthCacheEntry(serverId: string): void {
writeChain = writeChain.then(async () => {
const cache = await getMcpAuthCache()
cache[serverId] = { timestamp: Date.now() }
await writeFile(cachePath, jsonStringify(cache))
authCachePromise = null // 失效读缓存
})
}

17.2 工具/资源 LRU 缓存

// client.ts:1726
const MCP_FETCH_CACHE_SIZE = 20 // LRU 最大容量

// fetchToolsForClient、fetchResourcesForClient、fetchCommandsForClient
// 都使用 memoizeWithLRU,键为 server name
// 在 client.onclose 时同步失效,确保重连后获取新工具列表

以上章节(九至十七)基于 CC 源码实测,所有代码片段均来自实际实现,无推断内容。核心文件路径:src/services/mcp/client.ts(3349行)、src/services/mcp/auth.ts(2466行)、src/services/mcp/InProcessTransport.tssrc/services/mcp/SdkControlTransport.tssrc/services/mcp/normalization.tssrc/services/mcp/mcpStringUtils.tssrc/services/mcp/channelAllowlist.tssrc/services/mcp/channelPermissions.tssrc/services/mcp/oauthPort.tssrc/services/mcp/xaa.ts

涉及源文件

  • services/mcp/channelAllowlist.ts
  • services/mcp/client.ts
  • services/mcp/config.ts
  • services/mcp/elicitationHandler.ts
  • services/mcp/envExpansion.ts
  • services/mcp/officialRegistry.ts
  • src/services/mcp/auth.ts
  • src/services/mcp/channelAllowlist.ts
  • src/services/mcp/channelPermissions.ts
  • src/services/mcp/client.ts
  • src/services/mcp/InProcessTransport.ts
  • src/services/mcp/mcpStringUtils.ts
  • src/services/mcp/normalization.ts
  • src/services/mcp/oauthPort.ts
  • src/services/mcp/SdkControlTransport.ts
  • src/services/mcp/xaa.ts
打赏
  • 微信
  • 支付宝

评论