<!-- title: 给MCP服务器造一套测试体系 -->

# 给MCP服务器造一套测试体系：从Inspector探索到CI自动化

## 概述

本教程面向已经能写出"能跑起来"的MCP服务器、却不知道怎么保证它"在Agent手里不翻车"的开发者。你将学会三层递进的测试策略：用**MCP Inspector**做交互式探索发现明显问题，用**Inspector CLI模式**做CI烟雾测试，用**`@modelcontextprotocol/sdk` 客户端 + Vitest**写断言级自动化测试。覆盖三原语（工具/资源/提示词）、两类错误通道、三种传输，并接入GitHub Actions流水线。学完即可对任何自研MCP服务器建立可复现、可回归的测试体系。

## 前置条件

- **基础**：能独立写出一个最小可运行的MCP服务器（会用一种SDK注册工具）
- **环境**：Node.js `^22.7.5`（Inspector硬性要求），`npx` 可用
- **技能**：会读TypeScript、用过任意测试框架（Vitest/Jest均可，本教程用Vitest）
- **工具**：`jq`（用于命令行断言），`git`（接入CI）

> 时效性提醒：MCP规范在 **2026-04-14 通过 SEP-2577（Final 状态）正式弃用 Roots、Sampling、Logging 三项核心特性**。本教程刻意不依赖这三者——测试的是被官方长期支持的稳定特性（工具、资源、提示词、JSON-RPC错误码、进度通知）。如果你看到老教程大谈 `sampling/createMessage` 或 `roots/list`，那是在教已弃用特性，请谨慎。

---

## 第一步：先理解"MCP服务器到底要测什么"

很多人把MCP服务器当普通REST API测，这是第一个坑。MCP服务器是一份**在运行时由你的服务器和LLM客户端协商的契约**——模型读你的工具描述和JSON Schema，自己决定调哪个工具、传什么参数。Schema写松了，模型传垃圾进来；描述含糊了，模型调错工具；错误返回形状不对，模型直接迷路，而你拿不到堆栈，只看到"Agent变笨了"。

所以MCP测试要同时覆盖**业务逻辑**和**协议信封**两个层面。下面这张矩阵是整个教程的"靶子"，后续每一步都在填它的格子：

| 原语 | 常见翻车点 | 必须断言什么 |
|------|-----------|-------------|
| 工具 Tool | 输入Schema太松/写错 | Schema形状、required字段、属性类型 |
| 工具 Tool | 抛未捕获异常导致崩溃 | 返回结构化错误，而非进程退出 |
| 工具 Tool | 副作用不正确 | `callTool`后文件/数据库状态 |
| 资源 Resource | MIME类型错乱 | `readResource`的mimeType与content |
| 资源 Resource | URI陈旧或缺失 | 列表URI与可读URI一致 |
| 提示词 Prompt | 必填参数缺失 | 服务器清晰报错而非返回空消息 |
| 协议 Protocol | 错误码不符规范 | JSON-RPC错误码与规范一致 |
| 传输 Transport | 连接中断 | 重连/干净关闭行为 |

### MCP最隐蔽的坑：两类错误通道

这是新手最常踩的雷，也是测试必须分别覆盖的点：

- **工具执行错误**：以**正常成功响应**返回，但带 `isError: true` 和可读的 `content`。模型看得到、能自愈（比如换个参数重试）。**业务逻辑失败**（告警码查不到、参数值非法）走这条。
- **协议错误**：由传输层抛出的JSON-RPC错误。调用不存在的工具、参数畸形、服务器内部故障，应以**抛出错误**的形式暴露，带标准错误码（如 `-32601` 方法不存在、`-32602` 参数非法）。

把这两条搞混（比如把业务失败抛成协议错误，或把内部故障包装成 `isError:true`）是MCP服务器最常见bug。后面的测试会分别打这两条通道。

---

## 第二步：准备被测服务器（一个"告警处置手册"服务器）

为了后面所有测试有靶子，我们先写一个覆盖三原语的真实服务器：电信运维场景下的**告警处置手册查询**服务。完整目录结构：

```
alarm-runbook-server/
├── package.json
├── tsconfig.json
├── src/index.ts          # 服务器实现
└── test/
    ├── setup.ts          # 测试客户端工厂
    └── runbook.test.ts   # 自动化测试套件
```

**package.json**（注意 `"type": "module"` 和依赖版本）：

```json
{
  "name": "alarm-runbook-server",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "build": "tsc",
    "start": "node build/index.js",
    "test": "vitest run",
    "smoke": "npx @modelcontextprotocol/inspector --cli node build/index.js --method tools/list"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.18.0"
  },
  "devDependencies": {
    "typescript": "^5.6.0",
    "vitest": "^2.1.0",
    "@types/node": "^22.0.0"
  }
}
```

**tsconfig.json**：

```json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "build",
    "rootDir": "src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["src"]
}
```

**src/index.ts**（被测服务器，刻意同时演示工具级错误与协议级错误）：

```ts
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  ListToolsRequestSchema, CallToolRequestSchema,
  ListResourcesRequestSchema, ReadResourceRequestSchema,
  ListPromptsRequestSchema, GetPromptRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";

// 内存里的"手册库"，真实项目换成数据库/文件
const RUNBOOKS: Record<string, { title: string; steps: string[] }> = {
  "ALARM-1001": { title: "gNB链路中断", steps: ["检查光纤物理连接", "查看NG接口SCTP状态", "联系传输班组确认承载网"] },
  "ALARM-2002": { title: "AMF注册成功率下降", steps: ["查看AMF指标看板", "检查UDM订阅响应", "排查UE鉴权失败码"] },
};

const server = new Server(
  { name: "alarm-runbook-server", version: "1.0.0" },
  { capabilities: { tools: {}, resources: {}, prompts: {} } }
);

// —— 工具 ——
server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [{
    name: "lookup_runbook",
    description: "根据告警码查询处置手册",
    inputSchema: {
      type: "object",
      properties: { alarm_code: { type: "string", description: "告警码，如 ALARM-1001" } },
      required: ["alarm_code"],
    },
  }],
}));

server.setRequestHandler(CallToolRequestSchema, async (req) => {
  const { name, arguments: args } = req.params;
  if (name !== "lookup_runbook") {
    // 未知工具 → 抛错 → 走"协议错误"通道（JSON-RPC -32601）
    throw new Error(`未知工具: ${name}`);
  }
  const code = String(args?.alarm_code ?? "");
  const rb = RUNBOOKS[code];
  if (!rb) {
    // 查不到手册 → 业务失败 → 走"工具级错误"通道（isError: true）
    return { content: [{ type: "text", text: `未知告警码: ${code}` }], isError: true };
  }
  const text = `【${rb.title}】处置步骤:\n` + rb.steps.map((s, i) => `${i + 1}. ${s}`).join("\n");
  return { content: [{ type: "text", text }] };
});

// —— 资源 ——
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
  resources: Object.keys(RUNBOOKS).map((code) => ({
    uri: `runbook://${code}`, name: RUNBOOKS[code].title,
    mimeType: "text/markdown", description: `处置手册 ${code}`,
  })),
}));

server.setRequestHandler(ReadResourceRequestSchema, async (req) => {
  const m = req.params.uri.match(/^runbook:\/\/(.+)$/);
  if (!m) throw new Error(`资源URI无效: ${req.params.uri}`);
  const rb = RUNBOOKS[m[1]];
  if (!rb) throw new Error(`未找到手册: ${req.params.uri}`);
  const md = `# ${rb.title}\n\n## 处置步骤\n` + rb.steps.map((s, i) => `${i + 1}. ${s}`).join("\n");
  return { contents: [{ uri: req.params.uri, mimeType: "text/markdown", text: md }] };
});

// —— 提示词 ——
server.setRequestHandler(ListPromptsRequestSchema, async () => ({
  prompts: [{
    name: "incident_triage", description: "生成告警分诊提示词",
    arguments: [{ name: "alarm_code", description: "告警码", required: true }],
  }],
}));

server.setRequestHandler(GetPromptRequestSchema, async (req) => {
  const code = String(req.params.arguments?.alarm_code ?? "");
  return {
    messages: [{
      role: "user",
      content: { type: "text", text: `请对告警 ${code} 进行分诊，给出影响面、紧急度和首步处置。` },
    }],
  };
});

await server.connect(new StdioServerTransport());
```

**预期结果**：`npm run build && npm start` 后服务器静默运行在stdio上，无报错即说明三原语都已注册成功。

**常见问题**：
- **报错 `Cannot find module .../types.js`**：`moduleResolution` 没设成 `Node16`，SDK用 `.js` 后缀导入ESM子路径，必须用Node16解析。
- **`server.connect` 卡住**：stdio服务器本就不输出，这是正常的；要验证它活着，用下一步的Inspector。

---

## 第三步：用Inspector做交互式探索（UI模式）

Inspector是Anthropic官方的交互式调试工具，被社区称为"MCP界的Postman"。它**不需要全局安装**，直接 `npx` 启动。

```bash
# 编译后，对本地stdio服务器启动Inspector
npx @modelcontextprotocol/inspector node build/index.js

# 需要传环境变量给服务器时用 -e
npx @modelcontextprotocol/inspector -e API_KEY=test-key node build/index.js --verbose

# Python服务器也行
npx @modelcontextprotocol/inspector uv run my_mcp_server
```

启动后控制台打印一个本地URL（默认UI在 `http://localhost:6274`），自动打开浏览器。Inspector的架构是**两个组件**：

- **MCPI（Inspector Client）**：React Web UI（端口6274）
- **MCPP（Inspector Proxy）**：Node代理（端口6277），既是连你服务器的MCP客户端，又是给浏览器供UI的HTTP服务器

> 端口助记：6274 = M-C-P-I，6277 = M-C-P-P（T9九宫格字母映射）。

在UI里你要依次走查四件事，对应四个标签页：

1. **连接握手**：看初始化握手（capability negotiation）是否成功——服务器声明了 `tools/resources/prompts` 三项能力。
2. **Tools标签**：列出 `lookup_runbook`，读它**模型视角下**的name、description、inputSchema。这一步最容易抓到"描述含糊"或"schema类型错"。
3. **填表调用**：填入 `alarm_code=ALARM-1001` 调用，看原始JSON-RPC请求/响应；再试 `alarm_code=NOPE`，确认返回 `isError: true` 而不是崩溃。
4. **Resources / Prompts标签**：列出 `runbook://ALARM-2002` 确认MIME是 `text/markdown`；用 `incident_triage` 填参预览渲染出的消息。
5. **Notifications面板**：抓日志、进度、错误通知（注意：日志通知 `notifications/message` 已被SEP-2577弃用，新代码请改用进度通知或自定义通知，这里只用于排查遗留服务器）。

UI探索的目的不是写测试，而是**快速发现明显问题**——一个描述让人困惑的工具、一个该返回JSON却返回HTML的资源。先在这里把"显而易见的坑"清掉，再去把发现自动化。

**常见问题**：
- **浏览器没自动打开**：手动访问控制台打印的 `localhost:6274`。
- **Docker环境跑不起来UI**：用官方容器，并关掉自动打开：
  ```bash
  docker run --rm -p 127.0.0.1:6274:6274 -p 127.0.0.1:6277:6277 \
    -e HOST=0.0.0.0 -e MCP_AUTO_OPEN_ENABLED=false \
    ghcr.io/modelcontextprotocol/inspector:latest
  ```

---

## 第四步：Inspector CLI模式——CI里的烟雾测试

浏览器UI没法塞进CI。Inspector为此提供了**CLI模式**：加 `--cli` 后它变成命令行客户端，连上服务器、发一条方法调用、把JSON打到stdout、用有意义的退出码退出。这是CI里最便宜的回归防线。

```bash
# 列出所有工具（绝佳烟雾测试）
npx @modelcontextprotocol/inspector --cli node build/index.js --method tools/list

# 带参数调用某个工具
npx @modelcontextprotocol/inspector --cli node build/index.js \
  --method tools/call \
  --tool-name lookup_runbook \
  --tool-arg alarm_code=ALARM-1001

# 列资源和提示词
npx @modelcontextprotocol/inspector --cli node build/index.js --method resources/list
npx @modelcontextprotocol/inspector --cli node build/index.js --method prompts/list
```

因为CLI输出结构化JSON，可以直接管道给 `jq` 做断言：

```bash
# 如果预期的 lookup_runbook 工具消失了，就让构建失败
npx @modelcontextprotocol/inspector --cli node build/index.js --method tools/list \
  | jq -e '.tools[] | select(.name == "lookup_runbook")' > /dev/null \
  || { echo "lookup_runbook 工具丢失"; exit 1; }
```

两种模式选型对照：

| 维度 | UI模式 | CLI模式（`--cli`） |
|------|--------|--------------------|
| 最适合 | 探索、调试 | CI烟雾测试、脚本化 |
| 输出 | 交互式React UI | stdout上的JSON |
| 人在回路 | 是 | 否 |
| 退出码 | 不适用 | 有，可脚本化 |
| 传输 | stdio / SSE / HTTP | stdio / SSE / HTTP |

CLI烟雾测试**便宜且能抓最致命的回归**——服务器起不来、工具莫名消失、握手坏掉。但它**很浅**。要真正的覆盖率，必须用SDK客户端写断言级测试。

**连远程HTTP服务器时**，把命令换成URL：

```bash
npx @modelcontextprotocol/inspector --cli \
  --transport http --server-url http://localhost:3000/mcp --method tools/list
```

---

## 第五步：用SDK客户端写自动化测试（核心）

最强力的测试方式是：在你的测试进程里用一个**真实的MCP客户端**连上服务器，对返回结果做断言。官方 `@modelcontextprotocol/sdk` 导出 `Client` 类和各种传输实现。

先装依赖：

```bash
npm install -D vitest @modelcontextprotocol/sdk
```

核心套路：把编译好的服务器当子进程用stdio拉起，连一个 `Client` 上去，测试结束后拆掉。用一个共享的setup helper保持每个测试干净。

**test/setup.ts**：

```ts
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";

export async function createTestClient() {
  const transport = new StdioClientTransport({
    command: "node",
    args: ["build/index.js"],
    env: { ...process.env, NODE_ENV: "test" },
  });
  const client = new Client(
    { name: "mcp-test-client", version: "1.0.0" },
    { capabilities: {} }
  );
  await client.connect(transport);
  return { client, transport };
}
```

有了helper，"服务器能起、能列出工具"的烟雾测试只需几行——它已经能挡住一整类回归（重构不小心把工具注销了或改名了）：

```ts
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { createTestClient } from "./setup";
import type { Client } from "@modelcontextprotocol/sdk/client/index.js";

let client: Client, transport: { close: () => Promise<void> };
beforeAll(async () => {
  const ctx = await createTestClient();
  client = ctx.client;
  transport = ctx.transport;
});
afterAll(async () => transport.close());

describe("工具契约", () => {
  it("列出预期工具", async () => {
    const { tools } = await client.listTools();
    expect(tools.map((t) => t.name)).toContain("lookup_runbook");
  });
});
```

### 5.1 断言工具Schema（最该自动化的一项）

工具的输入Schema是模型依赖的契约：太松则模型传垃圾，太严或写错则合法调用失败。`listTools` 返回的每个工具都带 `inputSchema`（JSON Schema），可以直接断言它的required字段、属性类型、结构：

```ts
it("lookup_runbook 的输入schema正确", async () => {
  const { tools } = await client.listTools();
  const tool = tools.find((t) => t.name === "lookup_runbook");
  expect(tool).toBeDefined();
  expect(tool?.description).toBeTruthy();

  const s = tool!.inputSchema as {
    type: string;
    properties: Record<string, { type: string }>;
    required?: string[];
  };
  expect(s.type).toBe("object");
  expect(s.properties.alarm_code.type).toBe("string");
  expect(s.required).toContain("alarm_code");
});
```

> 进阶技巧：给每个工具的schema做**快照**，快照diff会让任何意外的schema改动在Code Review里立刻可见。声明了 `outputSchema` 的结构化输出工具，还要断言成功返回的 `structuredContent` 能通过它校验。

### 5.2 分别打两类错误通道（最容易漏的测试）

```ts
describe("错误通道", () => {
  it("未知告警码 → 工具级错误（isError:true），而非崩溃", async () => {
    const res = await client.callTool({
      name: "lookup_runbook",
      arguments: { alarm_code: "NOPE" },
    });
    expect(res.isError).toBe(true);              // 业务失败：可被模型自愈
  });

  it("调用不存在的工具 → 协议级错误（抛出JSON-RPC错误）", async () => {
    await expect(
      client.callTool({ name: "ghost_tool", arguments: {} })
    ).rejects.toThrow();                          // 协议错误：-32601
  });
});
```

这两个测试钉死了第二步强调的"两类错误通道"。如果某次重构有人把"未知告警码"改成抛异常（错误地升级成协议错误），或把"内部故障"包装成 `isError:true`，测试立刻红。

### 5.3 测副作用、资源、提示词

```ts
describe("资源", () => {
  it("readResource 返回正确MIME和内容", async () => {
    const { contents } = await client.readResource({ uri: "runbook://ALARM-2002" });
    const c = contents[0] as { mimeType: string; text: string };
    expect(c.mimeType).toBe("text/markdown");
    expect(c.text).toContain("AMF注册成功率下降");
  });

  it("列表URI与可读URI一致（无陈旧资源）", async () => {
    const { resources } = await client.listResources();
    for (const r of resources) {
      await expect(client.readResource({ uri: r.uri })).resolves.toBeDefined();
    }
  });
});

describe("提示词", () => {
  it("incident_triage 渲染出消息", async () => {
    const { messages } = await client.getPrompt({
      name: "incident_triage",
      arguments: { alarm_code: "ALARM-1001" },
    });
    expect(messages.length).toBeGreaterThan(0);
  });
});
```

有副作用的工具（写文件、改库），在 `callTool` 之后要断言外部状态确实变了，而不仅仅是"没报错"。

**运行**：`npm test`。预期所有用例通过。

**常见问题**：
- **测试卡住不退出**：`afterAll` 里没调 `transport.close()`，子进程没被回收。
- **`callTool` 返回的 `content` 类型断言报错**：`content` 是联合类型，断言前先 `as` 成具体形状，别直接 `res.content[0].text`。
- **CI里stdio连接超时**：服务器启动慢时给transport加 `startupTimeout`，或在测试里加轮询等待。

---

## 第六步：三种传输分别测试

`callTool`/`readResource` 的逻辑相同，但**连接行为不同**。stdio子进程掉线和服务端HTTP连接掉线是两回事。如果服务器要支持多传输，至少各跑一轮冒烟：

```bash
# stdio（前面已做）
npx @modelcontextprotocol/inspector --cli node build/index.js --method tools/list

# streamable HTTP（先起HTTP服务器，再连URL）
npx @modelcontextprotocol/inspector --cli --transport http \
  --server-url http://localhost:3000/mcp --method tools/list

# SSE
npx @modelcontextprotocol/inspector --cli --transport sse \
  --server-url http://localhost:3000/sse --method tools/list
```

SDK侧同样如此：stdio用 `StdioClientTransport`，HTTP用 `StreamableHTTPClientTransport`，两者接同一个 `Client`。一条经验法则——**业务逻辑测试跑stdio（最快、最简单）；连接/重连/会话测试跑HTTP传输**。

---

## 第七步：接入CI流水线

把第四步的CLI烟雾 + 第五步的Vitest套件都挂到PR流水线上。**`.github/workflows/mcp-tests.yml`**：

```yaml
name: mcp-tests
on: [pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '22' }
      - run: npm ci
      - run: npm run build
      # 烟雾：服务器能起、能列工具、能列资源
      - run: npx @modelcontextprotocol/inspector --cli node build/index.js --method tools/list
      - run: npx @modelcontextprotocol/inspector --cli node build/index.js --method resources/list
      # 断言级：三原语 + 两类错误通道
      - run: npx vitest run
```

这样每次PR都会跑：服务器起不来→红；工具丢了→红；Schema漂移→红；错误通道被改坏→红。回归在任何Agent看到它之前就被拦下。

---

## 完整工作流

```mermaid
flowchart TD
    A[写MCP服务器] --> B[Inspector UI 探索<br/>发现明显问题]
    B -->|问题清零| C[Inspector CLI 烟雾测试<br/>CI 最便宜防线]
    C --> D[SDK Client + Vitest<br/>断言级自动化]
    D --> D1[工具Schema断言]
    D --> D2[两类错误通道<br/>isError vs 抛错]
    D --> D3[资源 MIME/URI]
    D --> D4[提示词渲染]
    D1 & D2 & D3 & D4 --> E[三种传输各冒烟<br/>stdio/SSE/HTTP]
    E --> F[接入 GitHub Actions]
    F -->|每次PR| G[拦截回归<br/>在 Agent 看到之前]
    G -.重构迭代.-> B
```

Inspector两组件与三种传输的关系：

```mermaid
flowchart LR
    U[浏览器 React UI<br/>MCPI :6274] -->|HTTP| P[Node 代理 MCPP :6277]
    P -->|stdio 子进程| S1[你的 MCP 服务器]
    P -->|SSE| S2[你的 MCP 服务器]
    P -->|streamable HTTP| S3[你的 MCP 服务器]
    P -->|CLI 模式 stdout JSON| CI[CI / jq 断言]
```

---

## 实战案例小结

本教程全程围绕的**告警处置手册服务器**就是一个完整可运行项目：`src/index.ts` 约80行覆盖三原语、`test/runbook.test.ts` 约60行覆盖五类断言、`.github/workflows/mcp-tests.yml` 接入CI。把它扩展到真实场景，只需：

- 把 `RUNBOOKS` 换成数据库/文件后端，并新增**副作用测试**（查询计数、缓存失效）
- 给工具加 `outputSchema` 结构化输出，并断言 `structuredContent` 校验通过
- 用 `Context7`/`defuddle` 等正式发布的MCP服务器做参照——它们都经历过这套测试范式才敢上线

**验收标准**：`npm test` 全绿、`npm run smoke` 退出码为0、PR流水线通过。三者满足，你就有了一个"对Agent友好且可回归"的MCP工具。

---

## 延伸阅读

- **MCP Inspector 官方文档** —— `modelcontextprotocol.io/docs/tools/inspector`，UI/CLI完整特性
- **MCP 调试指南** —— `modelcontextprotocol.io/docs/tools/debugging`，Inspector之外更广的调试策略
- **Inspector 仓库** —— `github.com/modelcontextprotocol/inspector`，端口/Docker/配置细节
- **SEP-2577** —— `modelcontextprotocol.io/seps/2577-deprecate-roots-sampling-and-logging`，弃用Roots/Sampling/Logging的官方依据，写新代码前必读
- **MCP TypeScript SDK** —— `github.com/modelcontextprotocol/typescript-sdk`，`Client`/`StdioClientTransport` API参考

## 训练营适用性评估

- **适用类型**：MCP开发训练营（中高级）、AI Agent工程实战营、通信运维自动化训练营（本案例的告警手册场景天然契合ICT受众）
- **不适合**：零基础营（需要先会写MCP服务器）；纯前端营
- **前置衔接**：建议排在"Claude Code MCP服务器开发（构建第一个工具）"之后作为质量保障模块，本系列Round 1/2的入门内容是它的前置
- **预计教学时长**：4–6学时。其中原理与矩阵1学时、服务器搭建1学时、Inspector探索1学时、自动化测试2学时、CI接入0.5学时、综合实战0.5学时
- **考核方式**：学员自带一个自研MCP服务器到课，结业时其测试套件覆盖率达标 + PR流水线可复现失败用例即为通过

## 参考来源

1. [MCP Inspector — Model Context Protocol 官方文档](https://modelcontextprotocol.io/docs/tools/inspector) — Inspector UI/CLI功能、特性总览与开发工作流最佳实践
2. [MCP Server Testing: The Complete Guide with MCP Inspector in 2026 — QASkills](https://qaskills.sh/blog/mcp-server-testing-guide-2026) — 三原语测试矩阵、Inspector CLI的CI用法、SDK Client自动化测试代码范式
3. [modelcontextprotocol/inspector — GitHub README](https://github.com/modelcontextprotocol/inspector) — Inspector双组件架构（MCPI/MCPP）、端口6274/6277、Docker部署与mcp.json导出
4. [SEP-2577: Deprecate Roots, Sampling, and Logging](https://modelcontextprotocol.io/seps/2577-deprecate-roots-sampling-and-logging) — 2026-04-14 Final标准，弃用Roots/Sampling/Logging的官方依据
5. [Testing MCP Servers: The Complete Developer's Guide — agnost.ai](https://agnost.ai/blog/testing-mcp-servers-complete-guide/) — 测试工具生态与分层测试策略补充

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