MCP 工具列表为空?先排查标准输出
文章解释 stdio 模式下普通日志污染 JSON-RPC 输出,如何导致解析失败、工具不可见或连接中断。配套 MIT 许可的 TypeScript 模板还针对异常隔离、密钥配置和 CI 缺失等问题提供防护。
AI 深度解读
MCP server 明明连上了,客户端却看不到工具,问题可能出在业务代码之外:一条启动日志就能污染协议通信,缺少 API key 也可能让服务提前退出。作者做过 LinkedIn、Twitter/X、Medium 和 Kaggle 的 MCP server,把遇到的接线问题整理成了 TypeScript、MIT 许可的 mcp-server-starter。
它的价值在于把日志、参数校验、错误处理和 CI 这些容易漏掉的环节提前收好。不过,原文提供的是实现思路和使用入口,没有给出故障率或不同模板的对照测试。
连上了却没有工具,先查 stdout
使用 stdio 传输时,stdout 是 MCP 的协议通道,每个字节都必须属于 JSON-RPC 消息。随手写一句 console.log("starting..."),就会把普通文本塞进协议流。客户端可能报 JSON 解析错误、显示空工具列表,或者直接断开连接。
在 stdio MCP server 里,日志写到哪里会影响协议能否正常工作。
模板让 src/logger.ts 统一写 stderr,Claude Desktop 和 Cursor 会把它作为服务日志展示。LOG_LEVEL 支持 debug、info、warn、error。
同时,ESLint 的 no-console 规则只放行 console.error,让误写的 console.log 在 lint 和 CI 阶段就被拦住。启动日志里的密钥也只展示类似 sk-t…7890 的掩码。
这几层分别解决输出位置、代码约束和敏感信息暴露,不能靠一句「记得别打印」替代。
参数错误和上游失败,应该停在哪一层
模板使用 SDK 1.x 的 McpServer.registerTool API,通过 zod 的 inputSchema 校验工具参数。SDK 会先校验,再执行 handler;.describe() 则给模型提供填写参数时的说明。
下面这段定义同时约束搜索词和结果数量:
export const exampleSearchInput = {
query: z.string().trim().min(1, 'query must not be empty').max(200).describe('Text to search for'),
limit: z.number().int().min(1).max(50).default(10).describe('Maximum number of results (1-50)')
};
重点看 query 的 .trim().min(1):只有空格的输入也过不了校验。query 最长 200 个字符,limit 必须是 1–50 的整数,默认 10。
进入 handler 后,执行过程由 runTool() 包裹。作者称,handler 抛出的错误会转换成带 isError: true 的 MCP 结果,返回简短、可供模型采取下一步行动的信息;完整错误和堆栈写入 stderr。
这里保护的是工具执行的错误边界。原文没有展示 runTool() 的实现,也没有讨论进程级异常、超时或重试,不能据此认定所有故障都能被兜住。
缺少密钥时,为什么还要让工具可见
src/config.ts 延迟读取 EXAMPLE_API_KEY。没有配置密钥,服务依然启动并列出工具;调用依赖密钥的工具时,才返回明确的 ConfigError,提示把密钥加入 MCP 客户端配置的 env 块。
这个取舍能把「工具有没有注册」和「凭据能不能用」分开排查。密钥可以放在客户端的 env 中,也可以放入已被 .gitignore 排除的本地 .env;maskSecret() 用于展示配置状态。
下面是原文的启动和检查命令:
git clone https://github.com/Dishant27/mcp-server-starter.git my-mcp-server
cd my-mcp-server
npm install && npm run build
EXAMPLE_API_KEY=your-key npm run inspect # 打开 MCP Inspector
最后一行会为这次检查提供 EXAMPLE_API_KEY,并启动 MCP Inspector;your-key 需要换成实际值。
新增工具需要复制 src/tools/exampleSearch.ts,在 src/tools/index.ts 注册,然后重新构建。README 还提供了 Claude Desktop 和 Cursor 配置。GitHub Actions 会在每次 push 和 PR 时,分别用 Node 20、22 执行 npm ci、typecheck、lint 和 build。
我的判断
这套模板最值得借鉴的是把约定变成约束:stdout 的使用有 lint 把关,参数有 schema 把关,工具失败有统一返回格式。对于第一次写 MCP server 的程序员,这些比多放几个示例工具更实用。
但「多数 starter 会坏」属于作者的经验判断,原文没有样本和对照数据。密钥掩码及 .gitignore 也只覆盖部分泄露路径,不能替代完整的凭据管理。
延迟校验密钥适合本地接入和调试;如果团队要求部署时立即发现配置缺失,启动即校验也有合理性。取舍取决于更看重工具发现体验,还是部署检查的及时性。
能拿来做什么
- 写 Node 或 TypeScript 的 stdio MCP server 时,先统一日志入口,再用
no-console限制 stdout 输出,把协议污染提前挡在 CI。 - 给 AI 调用的工具定义输入时,同时写清 zod 约束和
.describe(),覆盖空字符串、数量范围及默认值。 - 遇到工具列表为空时,先检查 stdout 和启动阶段的配置读取,再用 MCP Inspector 验证注册与调用,逐步缩小故障范围。