首页 › tutorials › 把真实API封装成MCP工具

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

概述

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

前置条件

第一步:先设计工具,再写代码

最常见的失败是把 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 决定是否调用,务必写清"做什么、何时用、何时别用"。
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 子模块。

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 关键字段(后续打包发布会复用):

{
  "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 暴露给客户端。

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);

核心要点:

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

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

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

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

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 是否合法、工具是否真能跑通。

启动方式(在项目根目录):

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 打成单文件,避免发布产物里带一堆相对路径依赖:

npm run bundle

随后有两种接入方式。

方式 A:本地直连(开发期)

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

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

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

方式 B:CLI 注册(更灵活)

# 注册到当前项目作用域(写入 .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 字段,然后:

npm publish --access public

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

{
  "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。

模型的内部编排大致为:

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/远程认证是独立话题,需另行设计)。

延伸阅读

训练营适用性评估

维度 评估
适合的训练营类型 后端开发营 / 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 — 官方调试工具的使用指南,本文第五步核心依据
  2. TypeScript SDK - GitHub — 高层 McpServer API 与 Zod 集成的官方实现
  3. Debugging MCP Servers: Tips and Best Practices — host-client-server 三方排障方法论
  4. Testing MCP Servers: Complete Guide to MCP Inspector — Inspector 面板操作与故障对照表
  5. MCP 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学通信知识星球海报-扫码加入