内置工具是起点,不是终点——真正强大的 Agent 知道如何调用自己之外的世界。
14.1 核心问题
前面 13 章构建的 Agent 只有内置工具(bash、read_file、write_file...)。但现实世界的任务往往需要访问内置工具以外的能力:
用户:帮我查一下 GitHub 上 anthropics/claude-code 仓库最近的 issue
Agent:我没有访问 GitHub API 的能力,抱歉。
用户:帮我操作浏览器打开 https://example.com
Agent:我无法控制浏览器。
用户:帮我查询公司内部数据库
Agent:我不知道你们公司数据库的 API。
这是能力封闭问题:Agent 的工具集在编译时就固定了,无法在运行时扩展。
解决方案:MCP(Model Context Protocol)
MCP 把"让 Agent 调用外部能力"标准化为一个协议:
- MCP Server:任何人(包括用户自己)都可以写一个独立进程,通过标准协议暴露工具
- MCP Client:Agent 在启动时连接配置的 MCP Server,把其工具自动加入可用工具列表
- 工具调用:LLM 像调用内置工具一样调用 MCP 工具,完全透明
14.2 原理讲解
14.2.1 MCP 协议概览
MCP(Model Context Protocol)是 Anthropic 定义的开放标准,规定了 Agent 如何发现和调用外部工具:
MCP 协议层次结构:
Claude Agent (MCP Client)
│
│ MCP Protocol (JSON-RPC 2.0)
│ ├── initialize ← 握手,交换能力
│ ├── tools/list ← 发现工具列表
│ ├── tools/call ← 调用工具
│ ├── resources/list ← 发现资源(可选)
│ └── prompts/list ← 发现提示词模板(可选)
│
MCP Server (独立进程或远端服务)
├── github-server ← 暴露 GitHub API 作为工具
├── postgres-server ← 暴露 SQL 查询作为工具
├── browser-server ← 暴露浏览器控制作为工具
└── your-custom-server ← 你自定义的任何工具
MCP 协议传输层(Claude Code 支持 5 种):
| 传输类型 | 适用场景 |
|---|---|
stdio |
本地进程(最常用),通过标准输入输出通信 |
sse |
远端 HTTP 服务,使用 Server-Sent Events |
http |
远端 HTTP 服务,使用 Streamable HTTP(新规范) |
ws |
远端 WebSocket 服务 |
sdk |
同进程内的内嵌 Server(IDE 插件模式) |
14.2.2 配置格式:~/.claude/settings.json
Claude Code 的 MCP Server 配置存放在 ~/.claude/settings.json(用户级)或项目根目录的 .mcp.json(项目级):
// ~/.claude/settings.json
{
"mcpServers": {
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {
"GITHUB_TOKEN": "ghp_xxxxxxxxxxxx"
}
},
"postgres": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-postgres"],
"env": {
"DATABASE_URL": "postgresql://localhost/mydb"
}
}
}
}
对应的类型定义 McpStdioServerConfig:
// src/services/mcp/types.ts
const McpStdioServerConfigSchema = 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(), // 环境变量
})
14.2.3 配置来源的多层优先级
Claude Code 从多个来源加载 MCP 配置,优先级从低到高:
enterprise(企业策略) /etc/claude/.mcp.json
↓ 被覆盖
user(用户全局) ~/.claude/settings.json
↓ 被覆盖
project(项目配置) ./.mcp.json
↓ 被覆盖
dynamic(运行时动态注入) SDK / 插件 程序注入
ConfigScope 枚举(src/services/mcp/types.ts)就是这四个层次加上 claudeai、managed 等特殊来源。相同名称的 Server 被更高优先级的配置覆盖。
14.2.4 连接流程:connectToServer()
src/services/mcp/client.ts 中的 connectToServer() 是连接单个 MCP Server 的主函数,整体流程:
connectToServer(name, config)
│
├─ 1. 根据 config.type 创建传输层
│ ├─ stdio → StdioClientTransport(启动子进程)
│ ├─ sse → SSEClientTransport
│ ├─ http → StreamableHTTPClientTransport
│ └─ ws → WebSocketTransport
│
├─ 2. 创建 MCP Client(@modelcontextprotocol/sdk)
│ └─ name: "claude-code", capabilities: { roots, elicitation }
│
├─ 3. 连接 + 超时竞争(默认 30 秒)
│ └─ Promise.race([client.connect(transport), timeoutPromise])
│
├─ 4. 发现工具列表
│ └─ client.listTools() → tools[]
│
├─ 5. 为每个工具生成 MCPTool 实例(添加 mcp__ 前缀)
│ └─ buildMcpToolName(serverName, toolName)
│ → "mcp__github__list_issues"
│
└─ 6. 返回 MCPServerConnection(含 client + tools[])
注意:connectToServer() 使用 memoize 缓存,相同 server 配置只连接一次。
14.2.5 工具命名空间:mcp__server__tool 格式
为避免不同 MCP Server 的工具名冲突(以及与内置工具冲突),MCP 工具统一加前缀:
// src/services/mcp/mcpStringUtils.ts
function buildMcpToolName(serverName: string, toolName: string): string {
return `mcp__${normalizeNameForMCP(serverName)}__${normalizeNameForMCP(toolName)}`
}
// 示例:
// server="github", tool="create_issue"
// → "mcp__github__create_issue"
// server="my postgres", tool="run query"
// → "mcp__my_postgres__run_query"(normalizeNameForMCP 处理空格/特殊字符)
LLM 在 tool_use 中使用这个完整名称,Claude Code 通过 mcpInfoFromString() 反向解析出 server 和 tool:
mcpInfoFromString("mcp__github__list_issues")
// → { serverName: "github", toolName: "list_issues" }
14.2.6 MCPTool:内置工具的 MCP 版本
src/tools/MCPTool/MCPTool.ts 定义了一个通用的 MCP 工具模板。每个 MCP Server 暴露的每个工具,都会生成一个 MCPTool 的实例(属性被覆盖为真实的名称/描述/Schema):
// 简化的 MCPTool 结构
export const MCPTool = buildTool({
isMcp: true, // 标记为 MCP 工具
name: 'mcp', // ← 实际被覆盖为 "mcp__server__tool"
async description() { return DESCRIPTION }, // ← 被覆盖为 MCP Server 返回的描述
get inputSchema() { return z.object({}).passthrough() }, // ← 被覆盖为 MCP Server 的 Schema
async call() { return { data: '' } }, // ← 被覆盖为真实的 client.callTool()
// ...
})
14.2.7 MCP 工具调用的完整链路
LLM 输出 tool_use:{ name: "mcp__github__list_issues", input: { repo: "..." } }
│
▼
QueryEngine.callTool()
│
├─ 查找工具:tools.find(t => toolMatchesName(t, "mcp__github__list_issues"))
│
├─ MCPTool.checkPermissions() ← 权限检查(第 8 章)
│
└─ MCPTool.call(input)
│
└─ client.callTool({ name: "list_issues", arguments: { repo: "..." } })
│ ← 发送给 MCP Server(调用时去掉 mcp__github__ 前缀)
▼
MCP Server(github-server 进程)
│
▼ 返回 tool_result
Agent 获得结果,追加到消息历史
注意:Claude Code 调用 MCP Server 时,工具名去掉了 mcp__github__ 前缀(Server 只认识自己的工具名 list_issues)。
14.2.8 资源(Resources)与提示词模板(Prompts)
除了工具,MCP 协议还支持另外两种能力:
Resources(资源):MCP Server 可以暴露可读文件/数据库记录/网页快照等资源,通过 URI 访问:
file:///tmp/data.csv
postgresql://localhost/mydb/users/123
https://example.com/snapshot.html
Claude Code 通过 ListMcpResourcesTool 和 ReadMcpResourceTool 暴露这些能力。
Prompts(提示词模板):MCP Server 可以暴露参数化的提示词模板,注册为斜杠命令(通过 fetchMcpSkillsForClient),与第 12 章的 Skills 系统集成。
14.2.9 插件系统
Claude Code 的插件系统(src/plugins/builtinPlugins.ts)在 MCP 之上提供了更高层的抽象:
插件 = { skills[], hooks[], mcpServers[], commands[] }
一个插件可以同时贡献:斜杠命令 + 生命周期 Hook + MCP Server 配置。插件通过 @builtin 后缀的 ID 区分内置插件(如 Explore Agent)和第三方市场插件。用户可以在 /plugin 界面启用/禁用插件。
14.3 源码索引
| 文件 | 关键函数/类型 | 作用 |
|---|---|---|
src/services/mcp/types.ts |
McpStdioServerConfig、McpSSEServerConfig、ScopedMcpServerConfig、ConfigScope、ConnectedMCPServer |
MCP 配置和连接类型体系 |
src/services/mcp/client.ts |
connectToServer()(memoized)、McpAuthError、buildMcpToolName() 调用处 |
连接单个 MCP Server 的核心逻辑 |
src/services/mcp/config.ts |
getAllMcpConfigs()、getEnterpriseMcpFilePath() |
多层配置加载(user/project/enterprise/dynamic) |
src/services/mcp/MCPConnectionManager.tsx |
MCPConnectionManager、useManageMCPConnections() |
React 上下文,管理所有 MCP 连接的生命周期 |
src/services/mcp/mcpStringUtils.ts |
buildMcpToolName()、mcpInfoFromString()、getMcpPrefix() |
MCP 工具名称空间工具函数 |
src/services/mcp/normalization.ts |
normalizeNameForMCP() |
Server/Tool 名称中特殊字符的规范化 |
src/tools/MCPTool/MCPTool.ts |
MCPTool(buildTool 基础模板) |
MCP 工具的通用 Tool 接口实现 |
src/plugins/builtinPlugins.ts |
registerBuiltinPlugin()、getBuiltinPlugins() |
内置插件注册表 |
14.4 最小化产出物
代码骨架位于
../chapters/14/src/,参考实现位于../chapters/14/solution/。 前置条件:需要ANTHROPIC_API_KEY环境变量(npm start需要)。
本章要实现什么
在 ../chapters/14/src/mcp.ts 中完成 MCP 集成模块。
接口规范(已提供,不要修改):
export function normalizeName(name: string): string
// 将特殊字符替换为下划线
export function buildMcpToolName(serverName: string, toolName: string): string
// 返回 mcp__<server>__<tool> 格式
export function parseMcpToolName(fullName: string): { serverName: string; toolName: string } | null
// 解析完整工具名,无效格式返回 null
export function loadMcpSettings(): { mcpServers?: Record<string, McpStdioServerConfig> }
// 从 ~/.mini-agent/settings.json 加载配置
export async function connectToMcpServer(name: string, config: McpStdioServerConfig): Promise<McpToolDef[]>
// 启动 stdio 子进程,连接 MCP Server,返回工具列表
export async function initMcpServers(settings): Promise<McpToolDef[]>
// 并行初始化所有 MCP Servers,聚合工具列表
你需要实现:
normalizeName():name.replace(/[^a-zA-Z0-9_]/g, '_')buildMcpToolName():mcp__${normalizeName(server)}__${normalizeName(tool)}parseMcpToolName():按__分割,验证前缀为mcploadMcpSettings():读取 JSON 配置文件,失败时返回{}connectToMcpServer():StdioClientTransport → Client.connect → listTools → 映射为 McpToolDefinitMcpServers():并行连接所有 Server,聚合工具列表
关键约束:
- 连接带 10 秒超时(
Promise.race) - 连接失败时打印错误并返回
[](不抛异常) - 工具名格式:
mcp__<server>__<tool>(避免命名冲突)
验收
cd docs/chapters/14
npm install
npm test
卡住时查看 ../chapters/14/solution/mcp.ts。
14.5 本章小结
核心机制回顾
MCP 协议是工具扩展的标准化:不是自己发明插件接口,而是让整个生态系统(GitHub、Postgres、浏览器、Slack...)用同一套协议接入 Agent。这是 Claude Code 能够"无限扩展"工具集的根本原因。
mcp__server__tool 命名空间:前缀设计解决了命名冲突问题——不同 Server 的同名工具(例如两个 Server 都有 search)不会互相覆盖。LLM 看到的是完整的 mcp__github__search,内部调用 Server 时再去掉前缀。
连接复用(memoize):connectToServer() 使用 memoize,对同一 Server 配置只建立一次连接。重新配置或断线重连时,通过清除 memoize 缓存强制重连。
工具是数据,不是代码:MCPTool 本质上是一个动态生成的工具描述对象——name、description、inputSchema、call() 都在运行时从 MCP Server 的响应中填充。这正是第 4 章工具抽象设计的价值所在。
Claude Code vs 本章产出物的差异
| 特性 | Claude Code | 本章产出物 |
|---|---|---|
| 传输类型 | stdio/sse/http/ws/sdk/claudeai-proxy | 仅 stdio |
| 配置来源 | 多层(user/project/enterprise/dynamic) | 单层(~/.mini-agent/settings.json) |
| 断线重连 | 自动(onclose → 清缓存 → 重连) |
不支持 |
| 资源(Resources) | ListMcpResourcesTool + ReadMcpResourceTool |
未实现 |
| 提示词模板 | 注册为斜杠命令(Skills 集成) | 未实现 |
| OAuth 认证 | ClaudeAuthProvider(OAuth 2.0 + 令牌刷新) |
无认证 |
| 输出截断 | truncateMcpContentIfNeeded()(100K 字符上限) |
无截断 |
| 插件系统 | BuiltinPluginDefinition(skills+hooks+mcpServers 组合) |
仅 mcpServers |
架构演进
第 4 章 工具接口(Tool)─────────────────────────────────────────┐
│ MCPTool = 动态实例化的 Tool
第 14 章 MCP 集成 ◄─────────────────────────────────────────────── ┘
├── 协议:JSON-RPC 2.0(MCP 规范)
├── 传输:stdio / SSE / HTTP / WebSocket
├── 发现:tools/list → 自动注册工具
├── 命名:mcp__server__tool(命名空间隔离)
└── 扩展:Resources + Prompts(超越工具的能力)
验收命令
cd chapters/14
npm install
# 验收方式一:使用内嵌 echo server(无需额外配置)
npx tsx src/main.ts
# 输入:"用 echo_text 工具把 'Hello MCP 第14章' 返回给我"
# 预期:Agent 调用 mcp__echo__echo_text,返回 [echo] Hello MCP 第14章
# 验收方式二:单次模式
npx tsx src/main.ts "用 get_time 工具告诉我现在是几点"
# 预期:Agent 调用 mcp__echo__get_time,返回当前 ISO 时间
# 验收方式三:配置外部 MCP server
mkdir -p ~/.mini-agent
cat > ~/.mini-agent/settings.json << 'EOF'
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
}
}
}
EOF
npx tsx src/main.ts "列出 /tmp 目录下的文件"
# 预期:Agent 调用 mcp__filesystem__list_directory,返回目录内容