首页 › tutorials › MCP工具调试测试与发布实战

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

概述

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

前置条件

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


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

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

// 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);
  }
}
// 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 单独验证。这是官方提供的交互式调试器,能在浏览器里逐个调用工具、查看原始报文。

# 指定你的服务器入口,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。

// 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 关键配置:

{
  "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,放在仓库根目录,团队成员克隆后即可复用。

// 仓库根目录 .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 管理(不写文件):

# --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,确认进程不立即崩溃:

# .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 完整版

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

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,你不必再手写命令。


工作流程总览

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[团队复用]

常见问题速查


延伸阅读


训练营适用性评估

参考来源

  1. MCP Server Testing and Quality Assurance: Unit Tests, Integration Flows, and the Inspector Workflow — Daniel Vaughan — 系统梳理单元/集成/Inspector 三层测试策略
  2. MCP Server Testing Frameworks: Unit, Integration, and Conformance Validation — Daniel Vaughan — 契约/一致性校验框架与 inline-snapshot 实践
  3. Testing MCP Servers: A Complete Guide — Kai Gritun — 用 Inspector 与集成测试验证服务器的入门指南
  4. How to Test MCP Servers Before They Break Your CI — dev.to — 把 MCP 测试纳入 CI 流水线的工程建议
  5. modelcontextprotocol 官方文档 — 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学通信知识星球海报-扫码加入