MCP 客户端
昨天(Day 11)看到斜杠命令能挂"内建扩展";今天讲 MCP——怎么接外部工具,并看外部工具怎么被映射成 Day 07 的 CoreTool、和内置工具无缝并列。这是"统一抽象"主线的又一次印证,为 Day 13 Skills 铺路。
MCP 是什么
MCP(Model Context Protocol,模型上下文协议)是一个开放标准,让 AI 应用能"即插即用"地接入外部工具和数据源。你写一个 MCP server(比如封装公司内部 API),任何支持 MCP 的客户端(Claude Code、Cursor…)都能连上、用它的工具——不用改客户端代码。
核心代码:客户端库 packages/mcp-client/、宿主集成 src/services/mcp/。
先厘清:客户端 vs 服务端(易混)
Claude Code 在 MCP 里有两个身份,别搞混:
src/entrypoints/mcp.tsClaude Code 作为 MCP Server(claude mcp serve)——把自己的内置工具暴露给别的 MCP 客户端用。不是本节重点。
packages/mcp-client/ + src/services/mcp/Claude Code 作为 MCP 客户端——连接外部 MCP server、用它们的工具。这才是今天讲的。
支持的传输方式(真实枚举)
"传输(transport)"= Claude Code 和 MCP server 之间怎么通信。真实的类型枚举(src/services/mcp/types.ts:23):
export const TransportSchema = lazySchema(() =>
z.enum(['stdio', 'sse', 'sse-ide', 'http', 'ws', 'sdk', 'claudeai-proxy']),
)
// stdio 型服务器的配置(最常用):
export const McpStdioServerConfigSchema = lazySchema(() =>
z.object({
type: z.literal('stdio').optional(), // 可省略(默认 stdio)
command: z.string().min(1), // 启动命令
args: z.array(z.string()).default([]), // 命令参数
env: z.record(z.string(), z.string()).optional(), // 环境变量
}),
)
建连在 src/services/mcp/client.ts(真实 import):StdioClientTransport / SSEClientTransport / StreamableHTTPClientTransport / WebSocketTransport,不支持的 type 抛 "Unsupported server type"。
npx some-mcp-server)。stdio 传输就是"启动这个程序,往它的标准输入写请求、从标准输出读响应"——像管道。不需要网络端口、不需要认证,最简单。你在 .mcp.json 里写 command + args 的(就是上面那个 schema)就是 stdio 型。JSON-RPC 是 MCP 底层用的消息格式(基于 JSON 的远程调用协议)。z.enum([...]) 就是 zod 声明"这个值只能是这几个字符串之一"。.mcp.json 配置
项目级配置文件是仓库根的 .mcp.json。一个典型配置:
{
"mcpServers": {
"my-db": { // 服务器名
"command": "npx", // stdio: 启动命令(对应 L03 schema)
"args": ["-y", "my-db-mcp-server"],
"env": { "DB_URL": "..." }
},
"internal-api": {
"type": "http", // http 型
"url": "https://mcp.mycompany.com"
}
}
}
读取支持四个作用域——project(.mcp.json)/ user / local / enterprise(企业锁定)。getAllMcpConfigs 汇总所有来源 + 插件提供的 server。
project 的 .mcp.json 进 git、团队共享;user 个人全局;local 本机不进 git;enterprise 公司强制(最高,能锁死"只准用这些")。整个项目的配置哲学统一:分层 + 后覆盖前 + 企业最高。工具发现:外部工具 → CoreTool(真实代码)
连上 server 后怎么知道它有哪些工具?discoverTools(packages/mcp-client/src/discovery.ts:47)。这段真实代码是本页精华——它把每个 MCP 工具映射成 Day 07 的 CoreTool:
const result = await client.request({ method: 'tools/list' }, ...) // ① 问它有啥工具
return result.tools.map((tool): CoreTool => {
const fullyQualifiedName = buildMcpToolName(serverName, tool.name) // ② 加 mcp__ 前缀
return {
name: skipPrefix ? tool.name : fullyQualifiedName,
isMcp: true,
inputJSONSchema: tool.inputSchema, // ③ 直接用它原始 JSON Schema(不 zod 转)
async prompt() { return tool.description ?? '' }, // ④ 给模型看的说明(Day 07 L07)
isReadOnly: () => tool.annotations?.readOnlyHint ?? false, // ⑤ 注解 → 行为标志
isConcurrencySafe: () => tool.annotations?.readOnlyHint ?? false,
isDestructive: () => tool.annotations?.destructiveHint ?? false,
async checkPermissions() { return { behavior: 'passthrough' } }, // ⑥ 权限弃权,交给通用系统
}
})
inputJSONSchema(不像内置工具走 zod 转);④ 用 prompt() 给模型看说明(正是 Day 07 L07 那个反直觉点);⑤ 把 MCP 的 readOnlyHint 注解翻译成 Day 08 的 isReadOnly(决定能否并发);⑥ 权限返回 passthrough(Day 07 L03 的弃权态,交给 Day 09 统一裁决)。query(description="查询订单库",inputSchema 要一个 sql 字段)→ 经 discoverTools → 变成名叫 mcp__my-db__query、isMcp:true、inputJSONSchema 直接用它原始 schema 的一个 CoreTool → 模型看到工具列表里多了这一项,就能像调 Read 一样调它。mcp__server__tool 前缀? 因为不同 MCP server 可能有同名工具(都叫 query)。加上 server 名前缀就不冲突,模型也能看出"这工具来自哪个 server"。而且权限规则能按前缀批量控制——deny: ["mcp__my-db"] 一下禁掉整个 server 的所有工具(Day 07 L06 讲的前缀级 deny)。最关键的一点看懂了没:MCP 工具在发现时就被造成了一个符合 Day 07 CoreTool 协议的对象——所以它天生就能和内置工具一样被对待(L06)。与内置工具无缝并列:两层接口的回报
MCP 工具怎么和内置工具一起给模型?回忆 Day 07 的 assembleToolPool——它就是干这个的:
// 内置工具 + MCP 工具 合并、去重、按名排序(Day 07 讲的缓存稳定)
assembleToolPool(toolPermissionContext, mcpTools)
CoreTool(同一种接口),到了 assembleToolPool 这里,内置工具和 MCP 工具长得一模一样——合并、排序、转成 API tool 定义(Day 07 的 toolToAPISchema)都走同一套代码。模型看到的工具池里 Read(内置)和 mcp__my-db__query(MCP)并排站着,调用方式也一样。这就是 Day 07"两层接口 / 协议层"设计的回报——协议统一,任何来源的工具都能无缝并入。唯一区别:MCP 工具的 schema 用它原始的 inputJSONSchema 而非 zod 转(L05③、Day 07 L07 都讲过)。CoreTool 协议。Day 11:命令/技能/MCP-prompt 都是同一种 Command。Day 12:MCP 工具也被造成 CoreTool。看出规律了吗——Claude Code 的扩展性不是堆 N 套机制,而是把一切归约到少数几个统一协议(Tool / Command),让各种来源都实现它。这是本项目架构最优雅处,Day 13/15 还会印证。连接韧性:外部 server 断了怎么办
外部 MCP server 可能崩、可能断网。packages/mcp-client/src/connection.ts 做了不少韧性处理:
- 连接超时(默认 30s)——连不上不无限等。
- 断线监控 + 重连:连续 3 次终止性错误就触发重连。
- 进程清理升级:关 stdio 子进程时 SIGINT → SIGTERM → SIGKILL 逐级升压,确保进程真被杀掉。
今日小结 + 动手
🧠 今天你应该能回答
- MCP 是什么?(AI 工具的统一接入协议,类比 USB)
- Claude Code 的两个 MCP 身份(客户端/服务端)?
- 传输方式有哪些、为什么 stdio 最常用?
- .mcp.json 四个作用域?
- MCP 工具怎么被映射成 CoreTool(inputJSONSchema/prompt/readOnlyHint/passthrough)?
- 为什么"两层接口"让 MCP 工具能无缝并入?
✋ 动手:对着真实代码读一遍
# 1. 传输枚举 + stdio 配置 schema(L03)
sed -n '23,35p' src/services/mcp/types.ts
# 2. 工具发现映射成 CoreTool(L05,本页精华)
sed -n '47,92p' packages/mcp-client/src/discovery.ts
# 3. 建连的各种 transport(L03)
grep -n "ClientTransport\|Unsupported server type" src/services/mcp/client.ts | head
# 4. 连接韧性(L07)
grep -n "Timeout\|reconnect\|SIGKILL\|MAX_ERRORS" packages/mcp-client/src/connection.ts | head