MCP服务迁移实测:标准输出如何干扰通信
文章用TypeScript v2 SDK重建天气MCP服务,覆盖工具注册、编译、Inspector测试及Claude Code接入。实测发现,客户端容忍了一条带换行的杂散日志,但未换行文本会导致通信问题。
AI 深度解读
一个 console.log 就能弄坏 MCP 服务吗?作者把天气工具迁到 TypeScript SDK v2 后,专门测了这条常见告诫:完整的杂散文本行被客户端跳过了,没有换行的输出却让下一次调用以 Connection closed 失败。
这篇值得读的地方,是把搭建、独立测试、接入 Claude Code 串成了一条可排查的路径。但实验结论有版本边界,Inspector 部分也没有实际运行,不能把整篇都当成同等强度的验证结果。
stdout 为什么有时没事,有时直接断连
本地 stdio 模式下,客户端启动 MCP 服务,通过 stdin 和 stdout 交换 JSON-RPC 消息。stdout 是通信通道,普通日志混进去,就可能破坏消息解析。
作者使用官方 2.4.0 客户端和 Claude Code 2.1.302 测试,发现两种输出的结果不同:
- 一条带换行的
console.log杂散文本被跳过,调用继续成功。 process.stdout.write("fetching...")没有换行,文本粘到下一条 JSON 消息前面,随后调用失败。
差别在消息边界。独立一行的非协议文本,这两个客户端能够容忍;没有换行的文本会污染后续消息,解析器拿到的就不再是合法 JSON。
客户端能容忍一次杂散输出,不等于服务端可以往 stdout 写日志。
作者仍然建议使用 console.error,把日志送到 stderr,并检查依赖是否在导入时打印内容。服务连接成功、第一次调用却断开时,stdout 污染应当优先排查。远程 HTTP 服务没有这种标准输出与协议消息共用通道的问题。

实验环境也要一起记住:Windows 11、Node 22.22.3、@modelcontextprotocol/server 2.4.0、Zod 4.6.7、TypeScript 7.0.2。官方客户端与服务协商出的协议版本是 2026-07-28。其他客户端是否同样宽容,原文没有验证。
工具能运行,还要让模型用得对
MCP 服务就是一个提供工具、资源或提示模板的普通程序,模型由客户端负责。同一个服务可以接入支持协议的不同客户端,差别主要在注册配置。
作者选择 TypeScript SDK v2 的 @modelcontextprotocol/server。按原文记录,2026 年 11 月 10 日,旧包 @modelcontextprotocol/sdk 的 npm latest 仍指向 v1 的 1.32.2。迁移时,registerTool 替代 tool(),serveStdio 替代 new StdioServerTransport() 加 server.connect()。
项目要求 Node 20 以上,安装 @modelcontextprotocol/server、zod,开发依赖为 typescript、@types/node。package.json 要设 "type": "module";发布成命令时再配置指向 ./build/index.js 的 bin。作者的 TypeScript 配置使用 ES2022、Node16、strict: true,从 src 输出到 build,并启用 skipLibCheck。其中 "types": ["node"] 不能漏:按文中 SDK 说明,TypeScript 6.0 起不再自动纳入 @types/*,而 SDK 类型引用了 Buffer。
下面这段原始实现展示了工具注册、参数校验和 stdio 启动之间的关系:
#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/server";
import { serveStdio } from "@modelcontextprotocol/server/stdio";
import * as z from "zod/v4";
const NWS_API_BASE = "https://api.weather.gov";
function createServer() {
const server = new McpServer({ name: "weather", version: "1.0.0" });
server.registerTool(
"get_alerts",
{
description:
"Get weather alerts in force right now for a US state. Only works for US states.",
inputSchema: z.object({
state: z.string().length(2).describe("Two-letter US state code, for example CA or NY"),
}),
},
async ({ state }) => {
const code = state.toUpperCase();
const res = await fetch(`${NWS_API_BASE}/alerts/active?area=${code}`, {
headers: { "User-Agent": "weather-mcp/1.0", Accept: "application/geo+json" },
});
if (!res.ok) {
return {
content: [{ type: "text", text: `Alert lookup failed: HTTP ${res.status}` }],
isError: true,
};
}
const data = (await res.json()) as {
features?: { properties: { event: string; headline: string } }[];
};
const features = data.features ?? [];
if (features.length === 0) {
return { content: [{ type: "text", text: `No active alerts for ${code}` }] };
}
return {
content: [
{
type: "text",
text: features.map((f) => `${f.properties.event}: ${f.properties.headline}`).join("\n"),
},
],
};
},
);
return server;
}
serveStdio(createServer);
console.error("weather MCP server running on stdio");
重点看 /alerts/active?area=${code}:描述承诺的是当前生效预警,请求路径就必须与之相符。作者此前比较 Wyoming 数据时,/alerts 返回 119 条,/alerts/active 返回 7 条。这个数字属于当时的结果,说明的是接口语义差异。
Zod schema 同时生成模型可读的 JSON Schema、执行入参校验,并提供处理函数的参数类型。作者传入 state: "California" 时,处理函数没有执行,SDK 返回了带 isError: true 的校验错误。
但长度为 2 不代表一定是有效州代码。工具描述写得清楚,也不能替代业务校验。
接入之前,先证明服务自己能工作
作者实际使用官方 Client 和 StdioClientTransport 调用了 listTools()、callTool(),并通过 versionNegotiation: { mode: "auto" } 协商协议。Texas 查询返回了浓雾预警,Wyoming 返回无生效预警。
MCP Inspector 则是依据文档整理,作者明确没有运行。它要求 Node 22.19.0 以上,高于服务自身的最低要求。网页入口是 npx @modelcontextprotocol/inspector node build/index.js;CI 可用 npx @modelcontextprotocol/inspector --cli node build/index.js --method tools/list;终端界面使用 --tui。
Inspector 还涉及凭证存储:默认使用系统钥匙串,没有钥匙串的环境会写入 ~/.mcp-inspector/secrets.json,未提供密钥时不加密。
执行 npx tsc 或配置好的 npm run build 后,用下面的命令注册编译产物:
claude mcp add --transport stdio weather -- node /absolute/path/to/weather-mcp/build/index.js
关键是 --:前面属于 Claude Code 参数,后面才是启动服务的命令。--env KEY=value 应放在它前面,路径使用绝对路径,避免工作目录变化导致启动失败。
随后用 claude mcp list 检查 ✔ Connected,用 claude mcp get weather 查看配置。默认 local scope 只对当前项目中的自己生效,存储在 ~/.claude.json;--scope user 跨项目使用,--scope project 写入项目根目录 .mcp.json,供团队共享并审批。删除本地配置使用 claude mcp remove weather -s local。
启动慢时,文中给出的写法是 MCP_TIMEOUT=10000 claude;PowerShell 对应 $env:MCP_TIMEOUT=10000; claude,等待时间为 10 秒。远程共享服务改用 streamable HTTP,通过 claude mcp add --transport http <name> <url> 注册,但作者只测试了 stdio。
版本差异也可能影响排查。原文写作时称当前版本为 2.1.322,实测却是 2.1.302;其后 2.1.311 修过项目条目定位与错误显示,2.1.318 修过配置锁等待,2.1.321 修过空登录记录导致列表无输出的问题。
我的判断
我认可先独立测试、再接入 AI 客户端的顺序。它能把服务启动、协议通信和模型选工具分开检查,比直接在对话里反复试更容易定位问题。
不过,示例还不是完整的可靠性方案。它处理了 HTTP 非成功状态,却没有捕获 fetch 网络异常或 JSON 解析异常,也没有超时控制。原文建议可预期失败返回 isError: true,示例只覆盖了其中一部分。
作者主张按用户意图设计工具,例如用 find_overdue_invoices 替代多步接口拼接,并返回精简答案。我赞同这个方向,但原文没有工具数量与调用准确率的对照实验,「更少更好」仍是经验判断,不能据此给所有服务设数量上限。
同样,「每次请求都会加载所有工具定义」不宜外推到所有客户端实现。不过控制返回体积确有具体约束:文中列出 Claude Code 超过 10,000 tokens 会警告,默认上限为 25,000,可通过 MAX_MCP_OUTPUT_TOKENS 调整;文本超过 50,000 字符会存文件,除非工具声明自己的 anthropic/maxResultSizeChars。
能拿来做什么
- 给 Node MCP 服务接日志库时,把输出明确配置到 stderr,并检查依赖导入阶段的打印;遇到
Connection closed,先运行注册命令中的实际启动程序。 - 在 CI 中先验证
tools/list,再通过官方客户端覆盖合法参数、非法参数和上游失败。连通检查通过,只证明服务能通信。 - 把工具描述当成接口契约审查:检查调用条件、参数限制和返回数据是否一致,尤其防止接口返回历史数据、描述却承诺实时状态。
- 团队共享时明确选择
.mcp.json或远程 HTTP,密钥通过环境变量传入;对外部内容保留提示注入审查,因为自建服务同样可能把不可信文本带进模型上下文。