第 14 章:插件与 MCP 集成

# 第 14 章:插件与 MCP 集成

> 内置工具是起点,不是终点——真正强大的 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`(项目级):

```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`:

```typescript
// 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 工具统一加前缀:

```typescript
// 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:

```typescript
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):

```typescript
// 简化的 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 集成模块。

**接口规范**(已提供,不要修改):

```typescript
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,聚合工具列表
```

**你需要实现**:
1. `normalizeName()`:`name.replace(/[^a-zA-Z0-9_]/g, '_')`
2. `buildMcpToolName()`:`mcp__${normalizeName(server)}__${normalizeName(tool)}`
3. `parseMcpToolName()`:按 `__` 分割,验证前缀为 `mcp`
4. `loadMcpSettings()`:读取 JSON 配置文件,失败时返回 `{}`
5. `connectToMcpServer()`:StdioClientTransport → Client.connect → listTools → 映射为 McpToolDef
6. `initMcpServers()`:并行连接所有 Server,聚合工具列表

**关键约束**:
- 连接带 10 秒超时(`Promise.race`)
- 连接失败时打印错误并返回 `[]`(不抛异常)
- 工具名格式:`mcp__<server>__<tool>`(避免命名冲突)

### 验收

```bash
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(超越工具的能力)
```

---

## 验收命令

```bash
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,返回目录内容
```

---

## 下一章

→ [第 15 章:完整 Coding Agent 集成](15-full-integration)