<!-- title: MCP工具调试测试与发布实战 -->

# MCP工具开发：从Inspector调试到本地发布实战

## 概述

本教程面向已掌握 MCP 协议基本概念、想亲手构建并交付一个可用 MCP 工具的开发者。我们将聚焦于"构建自己的 MCP 工具"中常被忽略的工程化环节——**可视化调试、自动化契约测试、项目级分发**。学完后你能独立把一个自研 MCP 服务器从"能跑"推进到"可交付"，并接入 Claude Code 项目供团队复用。

## 前置条件

- Node.js 20+ 与 npm（本教程主用 TypeScript SDK，最贴近 Claude Code 生态）
- 已安装 Claude Code CLI，能正常 `claude --version`
- 了解 JSON-RPC 的请求/响应/通知三要素，看过 `initialize`/`tools/list`/`tools/call` 的报文结构
- 一个真实的小痛点：你想让 Claude Code 帮你做某件"读数据→处理→返回"的事，但没有现成工具

如果你对协议本身还不熟，建议先读官方 modelcontextprotocol.io 的 Quickstart，再回到本文。本教程刻意避开"工具/资源/提示词三原语怎么选"这种设计话题，专注交付链路。

---

## 第一步：拆分出可复用的 server 工厂

很多人第一次写 MCP 服务器，把"创建 server"和"启动 stdio 传输"写进同一个 `main()`。这会让后面调试和测试都无从下手。第一条工程原则：**把 server 的构造和传输的连接拆开**。

```ts
// src/server.ts —— 只负责"造服务器"，不碰 stdio
import { McpServer } from "@modelcontextprotocol/sdk/mcp.js";
import { z } from "zod";
import fs from "node:fs/promises";

export function createServer(): McpServer {
  const server = new McpServer({ name: "md-linkcheck", version: "1.0.0" });

  server.tool(
    "check_url",
    "检查单个 URL 的 HTTP 状态码",
    { url: z.string().url() },
    async ({ url }) => {
      const status = await head(url);
      return { content: [{ type: "text" as const, text: `${url} -> ${status}` }] };
    }
  );
  return server;
}

export async function head(url: string, timeoutMs = 5000): Promise<number> {
  const ctrl = new AbortController();
  const t = setTimeout(() => ctrl.abort(), timeoutMs);
  try {
    const res = await fetch(url, { method: "GET", redirect: "follow", signal: ctrl.signal });
    return res.status;
  } catch {
    return 0; // 0 = 网络/超时
  } finally {
    clearTimeout(t);
  }
}
```

```ts
// src/index.ts —— 生产入口，连接 stdio
import { StdioServerTransport } from "@modelcontextprotocol/sdk/stdio.js";
import { createServer } from "./server.js";

async function main() {
  const server = createServer();
  await server.connect(new StdioServerTransport());
}
main();
```

**预期结果**：`npm run build` 后 `node ./build/index.js` 启动即阻塞等待，没有任何输出。**没有输出是对的**——stdio 传输下，stdout 被协议独占，任何 `console.log` 都会污染 JSON-RPC 报文。

**常见问题**：如果你在 server 里写了 `console.log`，第一个工具调用就会报 `Unexpected token` 解析错误。改成 `console.error`（走 stderr）或用 SDK 的 `server.server.sendLoggingMessage(...)`。

---

## 第二步：MCP Inspector 可视化调试

写完代码别急着接 Claude Code，先用 **MCP Inspector** 单独验证。这是官方提供的交互式调试器，能在浏览器里逐个调用工具、查看原始报文。

```bash
# 指定你的服务器入口，Inspector 会作为父进程拉起它
npx @modelcontextprotocol/inspector node ./build/index.js
```

**预期结果**：终端打印两个本地端口（默认 UI 在 `http://localhost:6274`，代理在 `6277`），浏览器自动打开。界面左侧显示连接状态与 `initialize` 握手结果，右侧出现 **Tools** 标签，列出 `check_url`。

操作要点：
1. 顶部 **Connection** 区可临时改环境变量、命令参数，无需改代码重启。
2. 点 **List Tools** 确认工具清单与 schema 是否正确序列化。
3. 进 **Tools → check_url**，在参数框填 `{"url": "https://example.com"}` 点调用，下方返回文本内容。
4. 切到 **JSON-RPC** 视图，能看到完整的请求/响应报文——这是定位"工具没出现""参数校验失败"最快的手段。

**常见问题**：Inspector 里工具列表为空，多半是 server 在 `initialize` 之后才注册 tool，或某处抛了未捕获异常导致进程退出。打开终端看 Inspector 拉起 server 时的 stderr，通常一眼可见。

---

## 第三步：用客户端做契约测试（vitest）

Inspector 是手测，CI 里需要一个能自动跑的契约测试。SDK 提供了内存传输，让你在同一进程内用真实 MCP **客户端**去调你的 server，端到端覆盖 `tools/list` 与 `tools/call`。

```ts
// test/contract.test.ts
import { describe, it, expect } from "vitest";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
import { createServer } from "../src/server.js";

async function connect() {
  const server = createServer();
  const client = new Client({ name: "test", version: "1.0" }, { capabilities: {} });
  const [clientSide, serverSide] = InMemoryTransport.createLinkedPair();
  await Promise.all([client.connect(clientSide), server.connect(serverSide)]);
  return client;
}

describe("md-linkcheck 契约", () => {
  it("暴露 check_url 工具", async () => {
    const client = await connect();
    const { tools } = await client.listTools();
    expect(tools.map((t) => t.name)).toContain("check_url");
  });

  it("check_url 返回 example.com 状态码", async () => {
    const client = await connect();
    const res = await client.callTool({ name: "check_url", arguments: { url: "https://example.com" } });
    const text = (res.content as any[])[0].text;
    expect(text).toMatch(/example\.com -> \d+/);
  });

  it("非法 URL 触发协议级错误", async () => {
    const client = await connect();
    const res = await client.callTool({ name: "check_url", arguments: { url: "not-a-url" } });
    expect(res.isError).toBe(true);
  });
});
```

`package.json` 关键配置：

```json
{
  "type": "module",
  "scripts": { "build": "tsc", "test": "vitest run" },
  "dependencies": { "@modelcontextprotocol/sdk": "^1.0.0", "zod": "^3.23.0" },
  "devDependencies": { "typescript": "^5.4.0", "vitest": "^1.6.0" }
}
```

**预期结果**：`npm test` 三项全绿。**关键价值**：这三条覆盖了"工具注册正确""正常路径返回符合结构""入参校验按 schema 拦截"三个契约点。任何破坏性改动都会立刻在 CI 暴露，而不是等到接进 Claude Code 后才让 LLM 撞上。

> Python 版同理：FastMCP 可用 `mcp` 包的 `create_connected_server_and_client_session` 做进程内联调，pytest 驱动 `client.call_tool(...)` 断言。

---

## 第四步：用 .mcp.json 接入项目并分发

调试、测试都过了，最后把它接到 Claude Code。项目级分发用 `.mcp.json`，放在仓库根目录，团队成员克隆后即可复用。

```json
// 仓库根目录 .mcp.json
{
  "mcpServers": {
    "md-linkcheck": {
      "command": "node",
      "args": ["./mcp-servers/md-linkcheck/build/index.js"],
      "env": { "USER_AGENT": "md-linkcheck/1.0" }
    }
  }
}
```

首次在含 `.mcp.json` 的项目里启动 Claude Code，会提示你**审批（trust）**这些项目级服务器——这是安全门禁，防止恶意仓库静默拉起任意进程。审批通过后即可使用。

也可以用 CLI 管理（不写文件）：

```bash
# --scope project 写入 .mcp.json；--scope user 全局；--scope local 仅本机
claude mcp add md-linkcheck --scope project -- node ./mcp-servers/md-linkcheck/build/index.js

claude mcp list                 # 列出所有已配置
claude mcp get md-linkcheck     # 查看单个配置与最近一次连接状态
```

进入会话后用 **`/mcp`** 命令实时查看连接状态、工具数量。连接失败会在此直接显示原因。

**常见问题**：
- **工具在 `/mcp` 里是 disconnected**：八成是 `args` 里的相对路径或 `node` 不在 PATH。`claude mcp get` 会显示上次失败的报错。
- **本地能用、CI 报找不到**：相对路径基准是 Claude Code 启动时的 cwd，确保 `.mcp.json` 里的路径相对仓库根稳定，或用绝对路径。

---

## 第五步：CI 冒烟测试与发布清单

把契约测试挂进 CI，再加一条"产线入口冒烟"——直接拉起 `build/index.js` 发一个 `initialize`，确认进程不立即崩溃：

```yaml
# .github/workflows/mcp.yml（节选）
- run: npm ci && npm run build && npm test
- name: smoke
  run: |
    node ./build/index.js < /dev/null &
    PID=$!
    sleep 1
    kill -0 $PID && echo "server alive"
```

发布前对照清单逐项确认：

| 检查项 | 通过标准 |
|---|---|
| stdout 纯净 | 全程无 `console.log`，仅 stderr 日志 |
| 入参 schema | 用 zod 约束，非法输入返回 `isError` |
| 契约测试 | `tools/list` 与 `tools/call` 双覆盖 |
| Inspector 验证 | 至少手测一次正常/异常路径 |
| `.mcp.json` 相对路径 | 仓库根克隆即可用 |
| README 含调用示例 | 给出 `claude mcp add` 一行命令 |

---

## 实战案例：md-linkcheck-mcp 完整版

把上面的片段组合，再加一个批量检查工具，就是一个能直接用的项目：

```ts
server.tool(
  "check_links",
  "检查 Markdown 文件内所有外链可达性，返回每个 URL 状态码",
  {
    file: z.string().describe("Markdown 文件路径"),
    timeout: z.number().default(5000).describe("单 URL 超时毫秒"),
  },
  async ({ file, timeout }) => {
    const md = await fs.readFile(file, "utf8");
    const urls = [...new Set([...md.matchAll(/\]\((https?:\/\/[^)\s]+)/g)].map((m) => m[1]))];
    const results = await Promise.all(urls.map(async (url) => ({ url, status: await head(url, timeout) })));
    const broken = results.filter((r) => r.status === 0 || r.status >= 400);
    return {
      content: [{
        type: "text" as const,
        text: `共 ${urls.length} 条外链，异常 ${broken.length} 条：\n` +
              broken.map((r) => `- [${r.status}] ${r.url}`).join("\n"),
      }],
    };
  }
);
```

接入后，在 Claude Code 里直接说："用 md-linkcheck 检查 `README.md` 的外链"，LLM 会自动调用 `check_links`，你不必再手写命令。

---

## 工作流程总览

```text
flowchart LR
    A[createServer 工厂] --> B[npx inspector 手测]
    B -->|工具清单/报文正常| C[vitest 契约测试]
    B -->|报错| A
    C -->|全绿| D[.mcp.json 项目分发]
    C -->|失败| A
    D --> E[/mcp 验证连接]
    E --> F[CI 冒烟+发布清单]
    F --> G[团队复用]
```

---

## 常见问题速查

- **工具不显示**：99% 是 server 启动后崩溃或 stdout 被污染。Inspector 的 stderr 是第一现场。
- **调用一直转圈不返回**：handler 里有同步阻塞 I/O 或未 `await`。MCP 工具必须是异步非阻塞的。
- **本地好使、换机器挂掉**：检查 Node 版本（需要 20+ 的全局 `fetch`）与 `.mcp.json` 路径基准。
- **想限制工具权限**：在 `createServer` 里按环境变量做特性开关，让生产工具集最小化，符合最小暴露原则。

---

## 延伸阅读

- 官方文档站 Quickstart 与 TypeScript SDK：modelcontextprotocol.io
- MCP Inspector 仓库与用法：`@modelcontextprotocol/inspector`
- Claude Code MCP 配置参考：官方 docs 的 "MCP" 章节
- 高级场景：Streamable HTTP 远程传输、OAuth 认证（适用于多用户托管）

---

## 训练营适用性评估

- **适用类型**：企业内训"AI 工程化"模块、社区 MCP 开发工作坊的中段（已过入门、未到生产部署）
- **教学时长**：约 3–4 学时（1 学时搭骨架、1 学时调试测试、1 学时分发与排障，留 1 学时动手改造）
- **难度定位**：中级。要求学员写过 npm 包或至少一个 CLI 工具
- **延伸作业**：把示例的 HTTP HEAD 换成只取响应头、增加并发限流、给 server 加一个 `resources` 暴露检查报告

## 参考来源

1. [MCP Server Testing and Quality Assurance: Unit Tests, Integration Flows, and the Inspector Workflow — Daniel Vaughan](https://codex.danielvaughan.com/2026/05/26/mcp-server-testing-quality-assurance-unit-integration-inspector/) — 系统梳理单元/集成/Inspector 三层测试策略
2. [MCP Server Testing Frameworks: Unit, Integration, and Conformance Validation — Daniel Vaughan](https://codex.danielvaughan.com/2026/05/30/mcp-server-testing-frameworks-unit-integration-conformance-validation/) — 契约/一致性校验框架与 inline-snapshot 实践
3. [Testing MCP Servers: A Complete Guide — Kai Gritun](https://www.kaigritun.com/mcp/testing-mcp-servers) — 用 Inspector 与集成测试验证服务器的入门指南
4. [How to Test MCP Servers Before They Break Your CI — dev.to](https://dev.to/aiwithanton/how-to-test-mcp-servers-before-they-break-your-ci-p7b) — 把 MCP 测试纳入 CI 流水线的工程建议
5. [modelcontextprotocol 官方文档](https://modelcontextprotocol.io) — SDK、Inspector、`.mcp.json` 配置权威来源

> [!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)
