<!-- title: 把真实API封装成MCP工具 -->

# 把真实API封装成MCP工具：从设计到调试发布的完整闭环

## 概述

本教程面向已经理解 MCP 三原语（工具 / 资源 / 提示词）基本概念、但希望亲手"接驳一个真实业务系统"的开发者。我们将以 **GitHub Issues REST API** 为蓝本，用官方 TypeScript SDK 的 `McpServer` 高层 API，构建一个可被 Claude Code 直接调用的 MCP 服务器，完整覆盖**接口设计 → 输入校验与错误处理 → MCP Inspector 联机调试 → npm 打包 → 接入 Claude Code** 这条真实工程闭环。学完后你能把任意带鉴权的 REST API 安全、可观测地暴露给大模型。

## 前置条件

- 已安装 Node.js 20+ 与 npm（验证：`node -v`、`npm -v`）
- 能读懂 TypeScript 基础语法，了解 `async/await` 与 fetch
- 一枚 GitHub Personal Access Token（Fine-grained 或 Classic，至少授予 `Issues: Read` / `Issues: Write`）
- 已安装 Claude Code 并完成登录（用于第 6 步接入验证）
- 对 MCP 三原语有概念层面的认知（不必精通协议细节）

## 第一步：先设计工具，再写代码

最常见的失败是把 REST 端点 1:1 暴露成工具——这会让模型面对几十个语义重叠的工具，触发率反而下降。正确做法是**站在"任务"而非"HTTP 动词"的视角抽象**。

围绕"管理一个仓库的 Issues"这个真实任务，我们收敛出 4 个工具：

| 工具名 | 对应的 REST 端点 | 设计意图 |
|--------|----------------|---------|
| `list_issues` | `GET /repos/{owner}/{repo}/issues` | 浏览与检索，只读、可分页 |
| `get_issue` | `GET /repos/{owner}/{repo}/issues/{number}` | 查单条详情，含评论摘要 |
| `create_issue` | `POST /repos/{owner}/{repo}/issues` | 写操作，需要谨慎的必填校验 |
| `search_issues` | `GET /search/issues` | 跨仓库检索，体现代理价值 |

设计原则有三条：

1. **粒度对齐任务**：`create_issue` 把标题/正文/标签打包成一个调用，而不是让模型先 `create` 再 `add_labels`。
2. **只读与读写分离**：只读工具永不触发鉴权风险，模型可放心批量调用；写工具的 description 要明示副作用。
3. **description 即提示词**：模型靠 description 决定是否调用，务必写清"做什么、何时用、何时别用"。

```mermaid
flowchart LR
    U[用户: 帮我看看张三提的bug] --> CC[Claude Code]
    CC -->|tools/call search_issues| MCP[mcp-github-issues]
    MCP -->|GET /search/issues| GH[GitHub REST API]
    GH -->|JSON| MCP
    MCP -->|结构化文本| CC
    CC --> U
```

## 第二步：搭建工程脚手架

新建项目并安装官方 SDK。注意 TypeScript SDK 的入口分布在多个子路径下，全部走 `.js` 后缀的 ESM 子模块。

```bash
mkdir mcp-github-issues && cd mcp-github-issues
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node tsx esbuild
npx tsc --init
```

`package.json` 关键字段（后续打包发布会复用）：

```json
{
  "name": "mcp-github-issues",
  "version": "0.1.0",
  "type": "module",
  "bin": { "mcp-github-issues": "./dist/index.js" },
  "files": ["dist"],
  "scripts": {
    "build": "tsc",
    "bundle": "esbuild src/index.ts --bundle --platform=node --format=esm --outfile=dist/index.js",
    "dev": "tsx src/index.ts",
    "inspector": "npx @modelcontextprotocol/inspector node dist/index.js"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.0.0",
    "zod": "^3.23.0"
  }
}
```

**预期结果**：`npm run dev` 应静默启动（stdio 服务器不打印任何 stdout，因为 stdout 是协议通道）。**常见问题**：如果你看到日志输出，说明你把 `console.log` 写到了 stdout——必须改用 `console.error` 或 SDK 的 logging 能力，否则会污染 JSON-RPC 帧。

## 第三步：实现工具并封装真实 API

入口文件 `src/index.ts`，使用 `McpServer` 高层 API：它自动帮你处理 `tools/list`，并把你用 Zod 定义的 schema 转成 JSON Schema 暴露给客户端。

```ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

const TOKEN = process.env.GITHUB_TOKEN;
if (!TOKEN) {
  console.error("[fatal] 缺少 GITHUB_TOKEN 环境变量");
  process.exit(1);
}

const API = "https://api.github.com";

// 统一的请求封装：注入鉴权、处理速率限制、抛出结构化错误
async function gh(path: string, init: RequestInit = {}) {
  const res = await fetch(`${API}${path}`, {
    ...init,
    headers: {
      Authorization: `Bearer ${TOKEN}`,
      Accept: "application/vnd.github+json",
      "X-GitHub-Api-Version": "2022-11-28",
      ...(init.body ? { "Content-Type": "application/json" } : {}),
      ...init.headers,
    },
  });

  if (res.status === 403 && res.headers.get("x-ratelimit-remaining") === "0") {
    const reset = res.headers.get("x-ratelimit-reset");
    throw new Error(`GitHub 速率限制已耗尽，将于 ${reset} (epoch) 重置`);
  }
  if (!res.ok) {
    const body = await res.text();
    throw new Error(`GitHub API ${res.status}: ${body.slice(0, 200)}`);
  }
  return res.json();
}

const server = new McpServer({
  name: "mcp-github-issues",
  version: "0.1.0",
});

// 工具 1：列出某仓库的开放 Issues
server.tool(
  "list_issues",
  "列出指定 GitHub 仓库最近开放的 Issues，按创建时间倒序。用于'看看最近有哪些 issue''某人提了什么'等浏览类任务。",
  {
    owner: z.string().describe("仓库所有者，如 vercel"),
    repo: z.string().describe("仓库名，如 next.js"),
    limit: z.number().int().min(1).max(50).default(10).describe("返回条数，1-50"),
  },
  async ({ owner, repo, limit }) => {
    const data = await gh(
      `/repos/${owner}/${repo}/issues?state=open&per_page=${limit}&sort=created&direction=desc`
    );
    const rows = (data as any[])
      .filter((i) => !("pull_request" in i)) // 排除 PR
      .map((i) => `#${i.number} [${(i.labels ?? []).map((l: any) => l.name).join(",")}] ${i.title} (@${i.user.login})`);
    return {
      content: [{ type: "text", text: rows.join("\n") || "无开放 Issue" }],
    };
  }
);

// 工具 2：创建 Issue（写操作，description 必须明示副作用）
server.tool(
  "create_issue",
  "在指定 GitHub 仓库创建一个新 Issue。属于写操作，会真实写入仓库，调用前应与用户确认。",
  {
    owner: z.string(),
    repo: z.string(),
    title: z.string().min(1).max(256).describe("Issue 标题，必填"),
    body: z.string().max(65536).optional().describe("Issue 正文 Markdown"),
    labels: z.array(z.string()).optional().describe("标签列表"),
  },
  async ({ owner, repo, title, body, labels }) => {
    const created = await gh(`/repos/${owner}/${repo}/issues`, {
      method: "POST",
      body: JSON.stringify({ title, body, labels }),
    });
    return {
      content: [{ type: "text", text: `已创建 #${(created as any).number}: ${(created as any).html_url}` }],
    };
  }
);

// search_issues / get_issue 同理，省略以节省篇幅

const transport = new StdioServerTransport();
await server.connect(transport);
```

**核心要点**：

- `gh()` 是整个封装的灵魂——把鉴权、API 版本头、速率限制、错误归一都收敛在一处，工具函数只关心业务语义。
- Zod schema 同时承担"对模型暴露的参数说明"和"运行时校验"双重职责，`description` 字段会直接进入 JSON Schema 被模型读到。
- 写操作的 description 里写明"属于写操作，调用前应与用户确认"，能显著降低误触发。

## 第四步：输入校验与错误处理的三层防线

REST→MCP 的封装最容易在生产环境翻车的是错误处理。推荐建立三层防线：

1. **Zod 静态层**：`min/max/enum` 在 SDK 内部自动执行，非法入参根本进不了你的 handler。
2. **业务前置层**：在调用 `gh()` 前做语义校验，例如 `labels` 是否在仓库允许的标签集合内。
3. **网络归一层**：上面的 `gh()` 把 4xx/5xx/限流统一翻译成人类可读的 `Error`。

SDK 会把 handler 抛出的 `Error` 自动包装成 `isError: true` 的工具结果返回给客户端，模型据此向用户解释失败原因。**关键原则**：永远不要让异常沉默吞掉——把原始信息（去掉敏感头）透传给模型，模型才能自我纠正重试。

```mermaid
flowchart TB
    C[tools/call 请求] --> Z{Zod 校验}
    Z -->|失败| ERR1[自动 isError 返回]
    Z -->|通过| B{业务前置校验}
    B -->|失败| ERR2[抛出带建议的 Error]
    B -->|通过| N[gh 网络层]
    N -->|限流/4xx/5xx| ERR3[归一化错误信息]
    N -->|2xx| OK[结构化文本返回]
```

## 第五步：用 MCP Inspector 联机调试

这是整个闭环里最高频被忽视、却最能省时间的环节。MCP Inspector 是官方的可视化调试器，能在 Claude Code 之外独立验证你的服务器是否正确握手、schema 是否合法、工具是否真能跑通。

**启动方式**（在项目根目录）：

```bash
npm run build
npx @modelcontextprotocol/inspector node dist/index.js
```

Inspector 会启动一个本地 Web 服务并自动打开浏览器。首次连接时需要把环境变量填入 Inspector 的 **Environment** 面板（例如 `GITHUB_TOKEN=ghp_xxx`），否则你的 `gh()` 会因缺 token 直接退出。

调试工作流按四个面板依次推进：

1. **Connection 面板**：确认 `initialize` 握手成功，看到服务器名与版本号。若卡在这里，通常是 stdout 被日志污染。
2. **Tools 面板 → List Tools**：应看到 4 个工具及其输入 schema。点开任一工具，schema 与 description 是否完整显示。
3. **Tools 面板 → 调用**：在右侧表单填入 `owner=vercel`、`repo=next.js`、`limit=3`，点 Run，观察返回的结构化内容。
4. **Notifications / 日志面板**：查看 raw JSON-RPC 帧，排查错误堆栈。

**典型故障与对策**：

| 现象 | 根因 | 解决 |
|------|------|------|
| 握手成功但 `tools/list` 为空 | 用了低层 `Server` 却没注册 `ListToolsRequestSchema` | 改用高层 `McpServer`，或补全 handler |
| 返回乱码导致 Inspector 崩溃 | handler 里 `console.log` 污染了 stdout | 全部改 `console.error` |
| 调用报 `isError` 但无细节 | `gh()` 把错误吞成空字符串 | 确保 `throw new Error(body)` 带上下文 |
| 偶发 403 | GitHub 速率限制 | 看 `x-ratelimit-remaining` 头，加退避 |

> 生产排障小贴士：MCP 的 host-client-server 三方模型意味着错误可能出在任意一侧。Inspector 的价值在于**隔离掉 host（Claude Code）这一变量**——如果 Inspector 里工具正常，问题就在 Claude Code 的配置或上下文，而非你的服务器。

## 第六步：打包与接入 Claude Code

调试通过后，用 esbuild 打成单文件，避免发布产物里带一堆相对路径依赖：

```bash
npm run bundle
```

随后有两种接入方式。

**方式 A：本地直连（开发期）**

在项目根目录（或任意工作目录）创建 `.mcp.json`，把它提交到版本库，团队成员克隆后即可共享同一套工具：

```json
{
  "mcpServers": {
    "github-issues": {
      "command": "node",
      "args": ["/绝对路径/mcp-github-issues/dist/index.js"],
      "env": { "GITHUB_TOKEN": "${GITHUB_TOKEN}" }
    }
  }
}
```

注意：`env` 里用 `${VAR}` 引用环境变量，避免把 token 硬编码进仓库。

**方式 B：CLI 注册（更灵活）**

```bash
# 注册到当前项目作用域（写入 .mcp.json）
claude mcp add github-issues node ./dist/index.js \
  -e GITHUB_TOKEN=$GITHUB_TOKEN --scope project

# 或注册到用户作用域（全局可用）
claude mcp add github-issues node ./dist/index.js \
  -e GITHUB_TOKEN=$GITHUB_TOKEN --scope user
```

三种作用域的取舍：

| 作用域 | 存储位置 | 是否随仓库共享 | 适用场景 |
|--------|---------|--------------|---------|
| local（默认） | 用户目录的项目段 | 否 | 个人实验 |
| project | `.mcp.json` | 是（入 VCS） | 团队统一工具集 |
| user | `~/.claude.json` | 否 | 个人跨项目复用 |

**发布到 npm 后的最终形态**：先在 `package.json` 确认 `bin` 与 `files` 字段，然后：

```bash
npm publish --access public
```

其他人只需一行接入，无需关心你的源码与构建：

```json
{
  "mcpServers": {
    "github-issues": {
      "command": "npx",
      "args": ["-y", "mcp-github-issues@latest"],
      "env": { "GITHUB_TOKEN": "${GITHUB_TOKEN}" }
    }
  }
}
```

**验证接入**：进入 Claude Code 执行 `/mcp`，应看到 `github-issues` 状态为 connected，并列出 4 个工具。随后直接发问："帮我看看 vercel/next.js 最近 3 个 issue"——模型应自动调用 `list_issues`。

## 实战案例：一个完整的"周报生成器"组合拳

把上述 4 个工具组合，就能完成单靠 REST 调用难以一次性搞定的任务。在 Claude Code 里输入：

> 帮我统计 `anthropics/claude-code` 本周新增的 bug 类 Issue，按负责人分组，生成周报草稿，并以 Markdown 写入 `weekly.md`。

模型的内部编排大致为：

```mermaid
sequenceDiagram
    participant U as 用户
    participant M as Claude Code
    participant S as mcp-github-issues
    participant G as GitHub API

    U->>M: 生成本周 bug 周报
    M->>S: search_issues label:bug created:>=本周
    S->>G: GET /search/issues
    G-->>S: 30 条 JSON
    S-->>M: 结构化文本
    M->>M: 按负责人分组聚合
    M->>U: 写入 weekly.md（文件系统工具）
```

这个案例体现了 MCP 封装的真实价值：模型不必知道 REST 端点、不必处理鉴权与分页、不必解析 GitHub 的 JSON 结构——你用 4 个语义化工具把这些复杂性封装在了服务器一侧。如果以后 GitHub 改了 API，你只需在 `gh()` 一处适配，模型与用户的体验完全不变。

## 进阶优化方向

完成闭环后，可沿三个方向继续打磨：

1. **可观测性**：用 SDK 的 `server.sendLoggingMessage()` 把请求耗时、剩余配额作为日志通知发给 host，在 Claude Code 里可观测。
2. **流式进度**：对耗时操作（如批量检索）发送 `notifications/progress`，让模型感知长任务进度，避免超时误判。
3. **资源模板**：把 `get_issue` 同时暴露为资源模板 `issues://{owner}/{repo}/{number}`，让用户可以 `@` 引用具体 Issue 作为上下文，与工具调用形成互补。
4. **传输升级**：当需要远程共享或多客户端复用时，从 stdio 切到 Streamable HTTP 传输，配合网关做鉴权（注意 OAuth/远程认证是独立话题，需另行设计）。

## 延伸阅读

- [Model Context Protocol 官方规范](https://modelcontextprotocol.io/specification) — 协议原典，工具/资源/提示词与各传输的权威定义
- [MCP Inspector 官方文档](https://modelcontextprotocol.io/docs/tools/inspector) — 调试器全部面板与命令的官方说明
- [TypeScript SDK 仓库](https://github.com/modelcontextprotocol/typescript-sdk) — 高层 `McpServer` 与低层 `Server` 的 examples 目录
- [Debugging MCP（DeepWiki）](https://deepwiki.com/modelcontextprotocol/docs/7.1-debugging-mcp) — host-client-server 三方排障的系统方法论
- [Claude Code MCP 配置文档](https://docs.anthropic.com/en/docs/claude-code/mcp) — `.mcp.json`、`claude mcp add` 与作用域的官方说明

## 训练营适用性评估

| 维度 | 评估 |
|------|------|
| 适合的训练营类型 | **后端开发营 / AI 应用工程营 / DevOps 自动化营**。需要学员具备一定后端与 API 基础，不适合纯零基础营 |
| 建议前置 | 已完成"三原语取舍"类课程，能独立读懂 TypeScript |
| 预计教学时长 | **6–8 学时**：设计 1h + 实现 2h + 调试 2h + 打包接入 1h + 综合实战 1–2h |
| 实操比重 | 高，约 70% 时间为编码与调试，适合工作坊形式 |
| 评估方式 | 学员各自封装一个不同的公开 API（如 Jira、Notion、Linear），用 Inspector 演示通过、并接入 Claude Code 完成一个组合任务作为结业作业 |
| 常见踩坑预警 | stdout 污染、token 硬编码、写工具未做确认、schema description 过于简略导致触发率低——建议在教学中作为反面案例集中讲解 |

## 参考来源

1. [MCP Inspector - Model Context Protocol](https://modelcontextprotocol.io/docs/tools/inspector) — 官方调试工具的使用指南，本文第五步核心依据
2. [TypeScript SDK - GitHub](https://github.com/modelcontextprotocol/typescript-sdk) — 高层 `McpServer` API 与 Zod 集成的官方实现
3. [Debugging MCP Servers: Tips and Best Practices](https://www.mcpevals.io/blog/debugging-mcp-servers-tips-and-best-practices) — host-client-server 三方排障方法论
4. [Testing MCP Servers: Complete Guide to MCP Inspector](https://agnost.ai/blog/testing-mcp-servers-complete-guide/) — Inspector 面板操作与故障对照表
5. [MCP Developer Guide 2026](https://lushbinary.com/blog/mcp-model-context-protocol-developer-guide-2026/) — 从架构到生产部署的综合开发指南

> [!tip] 51学通信知识星球
> **星主：爱卫生**（微信：gprshome201101）——公众号「51学通信」创办者，19 年通信行业运维与讲师经验，著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书（各大电商平台有售）。
> **星球定位**：深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域，主打运维岗位与信令分析。
> **当前规模**：5200 篇主题 / 990 个回答，198 元/年。
> **加入福利**：加入星球后请加爱卫生微信，拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题，另有群内分享。
> **AI 加持**：51学通信知识星球已作为 skill 接入 WorkBuddy，使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例，做总结、找文档和 PPT 等场景全覆盖。

![51学通信知识星球海报-扫码加入](https://wiki.500nian.com/images/zhixingqiu_qrcode.png)
