Day 12 / 共 20 天 · 第 3 周 扩展能力 · 加深版

MCP 客户端

昨天(Day 11)看到斜杠命令能挂"内建扩展";今天讲 MCP——怎么接外部工具,并看外部工具怎么被映射成 Day 07 的 CoreTool、和内置工具无缝并列。这是"统一抽象"主线的又一次印证,为 Day 13 Skills 铺路。

📍 你在整门课的位置 · 第 3 周「扩展能力」(本周主线:五花八门的扩展,最后都归约到少数几个统一协议)
D10 上下文管理 D11 Slash 命令 D12 MCP 客户端 D13 Skills 技能 D14 Hooks D15 子代理
L01

MCP 是什么

🤔 痛点:没有统一协议时 你想让 Claude Code 能查你公司的内部数据库、能操作你的 Jira。如果没有统一标准,就得为每个 AI 客户端(Claude Code、Cursor、别的)各写一套对接代码;换个客户端全部推倒重来,工具作者和客户端作者都苦不堪言。
💡 本质:MCP = 给 AI 工具定的"外接扩展坞"标准 MCP 就像笔记本电脑的扩展坞(Type-C 拓展坞):只要设备遵守同一个口的规范,网线、HDMI、U 盘随便插。工具作者写一次 MCP server,所有支持 MCP 的客户端都能即插即用,不用为每家客户端重写。

MCP(Model Context Protocol,模型上下文协议)是一个开放标准,让 AI 应用能"即插即用"地接入外部工具和数据源。你写一个 MCP server(比如封装公司内部 API),任何支持 MCP 的客户端(Claude Code、Cursor…)都能连上、用它的工具——不用改客户端代码。

打个比方 MCP 之于 AI 工具,就像 USB 之于外设。USB 定了统一接口,鼠标键盘 U 盘都能插同一个口。MCP 定了"工具怎么描述、怎么调用、怎么返回"的统一协议,任何 MCP server 提供的工具都能被任何 MCP 客户端使用。这样生态里的工具能互通,不用为每个客户端各写一遍。

核心代码:客户端库 packages/mcp-client/、宿主集成 src/services/mcp/

L02

先厘清:客户端 vs 服务端(易混)

Claude Code 在 MCP 里有两个身份,别搞混:

src/entrypoints/mcp.ts

Claude Code 作为 MCP Serverclaude mcp serve)——把自己的内置工具暴露给别的 MCP 客户端用。不是本节重点。

packages/mcp-client/ + src/services/mcp/

Claude Code 作为 MCP 客户端——连接外部 MCP server、用它们的工具。这才是今天讲的。

为什么一个程序既是客户端又是服务端? 作为客户端:Claude Code 用你配的外部工具(如查数据库)。作为服务端:别的 AI 应用可以把 Claude Code 的能力当工具来用。两个方向独立。今天只关心"Claude Code 怎么用外部工具",即客户端方向。
L03

支持的传输方式(真实枚举)

"传输(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"。

stdio 为什么最常用? 本地 MCP server 就是一个可执行程序(比如 npx some-mcp-server)。stdio 传输就是"启动这个程序,往它的标准输入写请求、从标准输出读响应"——像管道。不需要网络端口、不需要认证,最简单。你在 .mcp.json 里写 command + args 的(就是上面那个 schema)就是 stdio 型。JSON-RPC 是 MCP 底层用的消息格式(基于 JSON 的远程调用协议)。z.enum([...]) 就是 zod 声明"这个值只能是这几个字符串之一"。
L04

.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。

为什么分四个作用域? 跟 Day 09 权限规则、Day 16 CLAUDE.md、Day 17 settings 一样的分层思路:project.mcp.json 进 git、团队共享;user 个人全局;local 本机不进 git;enterprise 公司强制(最高,能锁死"只准用这些")。整个项目的配置哲学统一:分层 + 后覆盖前 + 企业最高。
L05

工具发现:外部工具 → CoreTool(真实代码)

连上 server 后怎么知道它有哪些工具?discoverToolspackages/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' } },  // ⑥ 权限弃权,交给通用系统
  }
})
逐条对照 Day 07/08 学的:③ MCP 工具用它原始的 inputJSONSchema(不像内置工具走 zod 转);④ 用 prompt() 给模型看说明(正是 Day 07 L07 那个反直觉点);⑤ 把 MCP 的 readOnlyHint 注解翻译成 Day 08 的 isReadOnly(决定能否并发);⑥ 权限返回 passthrough(Day 07 L03 的弃权态,交给 Day 09 统一裁决)。
Claude CodeMCP 客户端mcp-client ① tools/list 请求 ② 返回工具清单 外部 MCP servernpx my-db-mcpstdio 管道 ③ discoverTools 把每个工具造成 CoreTool(加 mcp__ 前缀) 统一工具池Read (内置)Edit (内置)mcp__my-db__query → 到了工具池里,MCP 工具和内置工具长得一模一样,模型分不出谁是外来的
图注:连上 server → 问它有哪些工具 → 每个工具映射成 CoreTool 并入统一工具池。
📝 举个例子:一个 MCP 工具落地成什么 外部 server "my-db" 声明工具 query(description="查询订单库",inputSchema 要一个 sql 字段)→ 经 discoverTools → 变成名叫 mcp__my-db__queryisMcp:trueinputJSONSchema 直接用它原始 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)。
L06

与内置工具无缝并列:两层接口的回报

MCP 工具怎么和内置工具一起给模型?回忆 Day 07 的 assembleToolPool——它就是干这个的:

// 内置工具 + MCP 工具 合并、去重、按名排序(Day 07 讲的缓存稳定)
assembleToolPool(toolPermissionContext, mcpTools)
关键:对模型完全透明。 因为 L05 里 MCP 工具已被映射成 Day 07 的 CoreTool(同一种接口),到了 assembleToolPool 这里,内置工具和 MCP 工具长得一模一样——合并、排序、转成 API tool 定义(Day 07 的 toolToAPISchema)都走同一套代码。模型看到的工具池里 Read(内置)和 mcp__my-db__query(MCP)并排站着,调用方式也一样。这就是 Day 07"两层接口 / 协议层"设计的回报——协议统一,任何来源的工具都能无缝并入。唯一区别:MCP 工具的 schema 用它原始的 inputJSONSchema 而非 zod 转(L05③、Day 07 L07 都讲过)。
把 Day 07/11/12 串起来看 Day 07:内置工具实现 CoreTool 协议。Day 11:命令/技能/MCP-prompt 都是同一种 Command。Day 12:MCP 工具也被造成 CoreTool看出规律了吗——Claude Code 的扩展性不是堆 N 套机制,而是把一切归约到少数几个统一协议(Tool / Command),让各种来源都实现它。这是本项目架构最优雅处,Day 13/15 还会印证。
L07

连接韧性:外部 server 断了怎么办

外部 MCP server 可能崩、可能断网。packages/mcp-client/src/connection.ts 做了不少韧性处理:

  • 连接超时(默认 30s)——连不上不无限等。
  • 断线监控 + 重连:连续 3 次终止性错误就触发重连。
  • 进程清理升级:关 stdio 子进程时 SIGINT → SIGTERM → SIGKILL 逐级升压,确保进程真被杀掉。
为什么这么小心? 因为 MCP server 是"外部的、不受控的"——可能是第三方写的、质量参差。一个挂掉的 MCP server 不能拖垮整个 Claude Code。超时、重连、强制清理这套,是"和不可靠外部依赖打交道"的标准韧性套路,跟 gov-agents 教程的"失败闸门/优雅降级"同一种防御思想:一个 MCP server 连不上,只是它的工具用不了,Claude Code 其余照常。
L08

今日小结 + 动手

🧠 今天你应该能回答

  • 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
明天预告 · Day 13:MCP 接的是"工具",Skills 接的是"操作流程 SOP"。Day 13 讲 Skills——SKILL.md 格式、发现加载、怎么被触发,并印证 Day 11 那个"skill 本质是 prompt Command"的洞察。

← Day 11 Slash 命令 Day 13 · Skills 技能系统 →