别让大模型陪你跑测试:MCP Server 的分层测试实践
别让大模型陪你跑测试MCP Server 的分层测试实践前言前面几篇把 MCP 的安全话题聊得差不多了从整体的安全边界到工具描述投毒、供应链再到权限模型安全三部曲算是收了尾。从这篇开始转向工程化——一个 MCP Server 从能跑到敢上线中间缺的东西。第一个话题是测试。MCP Server 的测试有个天然的坑整条链路的大脑是大模型而大模型是不确定的。同一句输入这次调工具、下次直接回答参数这次是合法 JSON、下次缺个字段。很多人因此干脆不写测试上线全靠手动聊两轮——聊完感觉没问题就发了。但回头看我们修过的 bug绝大多数根本轮不到大模型出场工具 schema 写错导致客户端解析失败、工具抛异常没被捕获导致连接挂掉、stdio 里混进了日志导致消息流错乱。这些问题在协议层和工具层就能测干净不需要任何模型参与。这篇文章讲我怎么把 MCP Server 拆成三层来测协议层、工具层、传输层各测各的大模型只出现在最后一步冒烟。文中代码基于 TypeScript 官方 SDKmodelcontextprotocol/sdk全部实测跑通。一、先分层大模型到底在哪一环一个 MCP Server 从下到上可以拆成三层传输层字节怎么进来出去。stdio 模式是标准输入输出按行分隔HTTP 模式是 Streamable HTTP。这一层的 bug 表现为消息收不到/收到乱的。协议层JSON-RPC 2.0 的方法分发。initialize 握手、tools/list 列工具、tools/call 调工具加上错误码的约定。这一层的 bug 表现为客户端握手失败/列出来的工具缺字段。工具层你真正写的业务函数。schema 定义、输入校验、执行逻辑、错误返回。日常 bug 大部分在这层。分层之后测试策略就清楚了工具层跑普通单元测试协议层用 SDK 的客户端直连测传输层做一条集成链路最后才让真 LLM 跑一遍冒烟。反过来一上来就用 Claude/Cursor 连着聊是最慢的调试方式每次失败你都不知道是哪层的问题。二、协议层用 InMemoryTransport 直连不碰进程不碰网络SDK 里有个专门为测试准备的宝贝InMemoryTransport.createLinkedPair()。它创建一对内存里直连的传输管道一端接你的 Server一端接 SDK 的 Client——完整走协议但没有子进程、没有端口、没有网络单测跑起来毫秒级。import { Server } from modelcontextprotocol/sdk/server/index.js; import { Client } from modelcontextprotocol/sdk/client/index.js; import { InMemoryTransport } from modelcontextprotocol/sdk/inMemory.js; async function makePair() { const server new Server( { name: todo-server, version: 1.0.0 }, { capabilities: { tools: {} } } ); const client new Client({ name: test-client, version: 1.0.0 }); const [clientTransport, serverTransport] InMemoryTransport.createLinkedPair(); await Promise.all([ server.connect(serverTransport), client.connect(clientTransport), ]); return { server, client }; }client.connect()内部会自动完成 initialize 握手。所以第一组测试就白送了如果这段代码能跑通说明握手和能力协商没问题。把它写进 beforeEach后面的测试都建立在这个前提上。2.1 测 tools/listschema 是契约不是注释工具清单是客户端注入给大模型的菜单schema 写错轻则模型传错参数重则客户端直接拒绝渲染这个工具。用 Ajv 之类的校验器把每个工具的inputSchema过一遍import Ajv from ajv; test(每个工具的 inputSchema 都是合法的 JSON Schema, async () { const { client } await makePair(); const { tools } await client.listTools(); expect(tools.length).toBeGreaterThan(0); const ajv new Ajv({ strict: false }); for (const tool of tools) { expect(() ajv.compile(tool.inputSchema)).not.toThrow(); expect(tool.description?.length ?? 0).toBeGreaterThan(10); } });这里有个容易忽略的点inputSchema必须是type: object的完整 JSON Schema而你用 Zod 定义工具时 SDK 会帮你转换——转换结果对不对测一下才知道。我曾经把一个.describe()写漏了schema 转出来能过编译但模型永远猜不对那个参数的用途这种问题 schema 校验 描述非空检查就能拦住。2.2 测 tools/call分清两种失败MCP 有个新手必踩的概念区分协议错误和工具执行错误是两条路。请求一个不存在的工具、参数不符合 schema → JSON-RPC 协议错误客户端会收到异常工具执行了但业务上失败查不到数据、文件不存在→ 正常返回但isError: true错误信息在 content 里。两种都要测test(不存在的工具返回协议错误, async () { const { client } await makePair(); await expect(client.callTool({ name: no_such_tool, arguments: {}, })).rejects.toThrow(); }); test(工具业务失败返回 isError 而不是抛异常, async () { const { client } await makePair(); const result await client.callTool({ name: get_todo, arguments: { id: not-exist }, }); expect(result.isError).toBe(true); const text (result.content as Array{ type: string; text: string }) .filter((c) c.type text) .map((c) c.text) .join(); expect(text).toContain(not found); });第二种尤其重要。如果工具实现里直接throwSDK 会把它包装成协议错误返回——有的客户端遇到协议错误会把整个会话打断而isError结果会被当成普通的工具输出交还给模型模型还能据此重试或换路。业务失败也应该是模型可读的输入这个设计意图要靠测试固化下来。三、工具层把业务函数拆出来别隔着协议测逻辑协议层测试解决能不能被正确调用工具内部的逻辑还是老老实实写单元测试。前提是结构上可测工具注册和业务逻辑分离。// 业务函数纯逻辑不碰 MCP export async function getTodo(id: string): PromiseTodo { const todo await store.find(id); if (!todo) throw new TodoError(Todo ${id} not found); return todo; } // 注册层薄壳只做 schema 绑定和错误翻译 server.setRequestHandler(CallToolRequestSchema, async (request) { try { // 按 name 分发到业务函数 ... } catch (e) { if (e instanceof TodoError) { return { content: [{ type: text, text: e.message }], isError: true }; } throw e; // 未预期的错误继续抛交给 SDK 的兜底 } });业务函数测输入边界、并发、存储交互都是常规操作。想强调的是错误翻译层值得单独测一遍每个业务错误类型最终在协议层看到的是什么形态isError 文本还是意外抛出。上篇讲权限模型时提过错误信息会进模型上下文这里同理——错误文本就是给模型读的 API 文档得像对待正常输出一样对它做断言。四、传输层stdio 的两个经典坑各值一条集成测试stdio 模式下传输层最容易出两个事故我都真实踩过坑一日志污染协议通道。stdio 模式里 stdout 是协议专用通道任何库往 stdout 打印一行日志那一行会被当成一条 JSON-RPC 消息解析直接报错或者更糟——悄悄错位。测法很简单把 server 当子进程拉起来往 stdin 喂标准消息流逐行校验 stdout 的每一行都是合法 JSONimport { spawn } from node:child_process; test(stdout 的每一行都是合法 JSON-RPC, async () { const child spawn(node, [dist/server.js], { stdio: pipe }); const lines: string[] []; child.stdout.on(data, (chunk) { lines.push(...chunk.toString().split(\n).filter(Boolean)); }); child.stdin.write(JSON.stringify({ jsonrpc: 2.0, id: 1, method: initialize, params: { protocolVersion: 2025-06-18, capabilities: {}, clientInfo: { name: test, version: 1.0.0 }, }, }) \n); await new Promise((r) setTimeout(r, 500)); for (const line of lines) { expect(() JSON.parse(line)).not.toThrow(); } child.kill(); });这条测试配上一条import 顺序即日志开关的团队约定入口文件第一行把日志重定向到 stderr基本能杜绝这类事故。坑二进程不优雅退出。客户端断开后 server 还挂着占着端口占着文件句柄。测法是发起断开后断言子进程在超时时间内退出顺便把 SIGTERM 处理里的清理逻辑关连接、刷缓冲覆盖到。HTTP 传输相对简单Streamable HTTP 有官方的 StreamableHTTPClientTransport起一个本地端口做一轮完整握手即可不展开。五、最后一步才请出大模型一条冒烟测试就够了前三层都绿了之后才让真模型进场跑端到端起一个真实 server 进程用真实客户端或 Inspector headless 模式连上给一句固定 prompt断言三件事模型确实决定调用了目标工具而不是自己编了答案工具输出回到对话后最终回答里包含关键事实从工具结果里来的不可能是模型先验知识的那种比如刚才创建的待办 ID整个过程没有协议错误日志。LLM 的不确定性意味着断言必须宽容不断言具体措辞不断言调用路径细节只断言调没调和事实有没有进来。这条测试跑一次几十秒它不负责发现 bug——前三层已经把 bug 清完了——它负责确认拼起来之后世界仍然正常。写在最后总结一下这套分层测试的性价比排序协议层 InMemoryTransport 直连零环境依赖、毫秒级、覆盖握手/schema/错误语义是性价比最高的一层没有理由不写工具层单元测试常规工程重点是错误翻译层的断言传输层集成测试就防 stdio 日志污染和不优雅退出这两个惯犯LLM 端到端冒烟一条就够断言宽容跑在 CI 的最后一步。原则一句话把大模型挡在测试金字塔的最顶端。它是最贵的测试资源也是最不稳定的断言工具——能用 JSON Schema 断言的事情别让 token 替你判断。下一篇继续工程化话题可观测性——server 上线之后你怎么知道模型都在拿你的工具干什么。

相关新闻

ASM1042 PCIe Switch驱动深度调优:ACS与AER稳定性实战指南

ASM1042 PCIe Switch驱动深度调优:ACS与AER稳定性实战指南

简介:本资源为ASM1042 USB 3.0主控制器专用驱动程序包,面向使用Windows 7 64位系统的硬件维护人员、装机工程师及USB外设兼容性调试用户,专用于解决ASM1042芯片识别失败、传输速率不达标、设备频繁断连等典型问题。压缩包共13个文件&#xff…

2026/10/13 11:37:09 阅读更多 →
生产级Agent Runtime构建指南:编排、状态管理与故障排查

生产级Agent Runtime构建指南:编排、状态管理与故障排查

去年我在某个项目里接了一个很典型的任务:让大模型自动处理客服工单,从语义理解、信息提取,到调用工单系统、CRM、知识库,最后生成回复草稿。最初我天真地以为写一个循环调用模型接口就能搞定,结果上线第一周就遭遇了工…

2026/10/13 11:37:09 阅读更多 →
Wireshark抓包权限问题全解:从Permission denied到setcap

Wireshark抓包权限问题全解:从Permission denied到setcap

1. 报错现场:界面正常,一按抓包就被拒绝第一次在 Ubuntu 桌面版上装好 Wireshark,满心欢喜地点下蓝色鲨鱼鳍开始抓包,结果界面底部弹出一行小字:Permission denied。这个场景几乎每个 Linux 新手都经历过,而…

2026/10/13 11:36:08 阅读更多 →

最新新闻

从零搭建ELK日志分析平台:架构设计、组件选型与实战调优

从零搭建ELK日志分析平台:架构设计、组件选型与实战调优

说实话,我在接手公司日志平台之前,排查故障靠的是“三件套”组合拳:登录服务器、grep关键字、tail -f盯屏幕。最开始那几年业务量不大,这招勉强够用,可一旦服务器数量超过两位数、日志格式五花八门,这套土办…

2026/10/13 12:58:10 阅读更多 →
数论基础实战:从紫书第10章到Kuangbin专题14的题型拆解

数论基础实战:从紫书第10章到Kuangbin专题14的题型拆解

如果你正卡在紫薯第10章数学,准备转战Kuangbin专题14数论基础,我给你一句忠告——书上的公式全会了,不代表你看到题目能动手。我当年啃完紫书第10章,带着一堆“看起来都能推导”的公式点开专题14,第一题就把我按在地上…

2026/10/13 12:58:10 阅读更多 →
Conda报错排查指南:从repodata失败到依赖冲突的完整解决

Conda报错排查指南:从repodata失败到依赖冲突的完整解决

两行红字一贴,小白当场懵,老手也得先深吸一口气。Collecting package metadata (current_repodata.json): failed和Solving environment: failed这对组合,几乎每个用过 Anaconda 的人都撞见过:刚装完环境准备conda install pandas…

2026/10/13 12:58:10 阅读更多 →
基于Halton序列的图像加密解密算法详解与Matlab实现

基于Halton序列的图像加密解密算法详解与Matlab实现

做图像加密解密这个方向的,八成都有过这种感受:教材里的经典算法讲得头头是道,真到自己动手复现,却总在“看起来像那么回事”和“实际效果经不起推敲”之间反复横跳。我之前在做基于Halton序列的图像加密解密项目时,最…

2026/10/13 12:58:10 阅读更多 →
Kettle 9.3 源码编译实战:从 GitHub 构建可部署的 PDI 生产环境

Kettle 9.3 源码编译实战:从 GitHub 构建可部署的 PDI 生产环境

简介:本资源为Kettle(Pentaho Data Integration)最新9.3版本的官方安装包百度网盘直链,面向数据工程师、ETL开发人员及大数据初学者,解决国内用户因网络限制难以获取国际开源ETL工具的问题。资源以1个14KB的docx文档形…

2026/10/13 12:58:10 阅读更多 →
Agent上线最后一公里:评测门禁、可观测性埋点与AgentOps闭环落地指南

Agent上线最后一公里:评测门禁、可观测性埋点与AgentOps闭环落地指南

1. 从"能跑"到"敢上线":Agent 交付的最后一公里到底卡在哪做过 Agent 项目的人大概都有过这种体验:本地 Demo 跑得飞起,工具调用、多轮对话、RAG 检索全都正常,演示给同事看的时候掌声一片。可真到了要往生产…

2026/10/13 12:57:09 阅读更多 →

日新闻

物联网系统集成避坑指南:从选型到验收的关键要点

物联网系统集成避坑指南:从选型到验收的关键要点

做了这么多年物联网系统集成的项目,我最大的感受是,大部分坑不是出在技术前沿,而是出在一些看起来很平常的地方。设备选了便宜的、协议没问清楚、现场没摸透、验收标准写得含糊——这些细枝末节,最后都会变成延期、超支和扯皮的大…

2026/10/13 0:00:57 阅读更多 →
ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

如果你玩ESP32有一阵子了,很难不碰到舵机控制这道坎。做智能小车要转方向,做机械臂要抬关节,做云台要偏航俯仰,背后都是同一个东西在干活:舵机。而我见过太多人卡在“代码能编译、舵机就是不听话”这个阶段&#xff0c…

2026/10/13 0:00:57 阅读更多 →
CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

1. 为什么CAN总线至今仍是工业现场的“硬通货”?——从汽车底盘到智能温室的真实需求你有没有拆过一辆老款燃油车的中控台?或者调试过某高校实验室里那台跑着Modbus协议却频频丢帧的PLC控制柜?又或者,在某个农业物联网项目里&…

2026/10/13 0:00:57 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/13 2:54:56 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/12 0:16:38 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/13 1:58:08 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 21:05:09 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 14:48:49 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 18:11:17 阅读更多 →