MCP Server 开发踩坑:我用 3 行代码解决了 TypeScript 工具函数无法被 LLM 识别的问题
MCP Server 开发踩坑我用 3 行代码解决了 TypeScript 工具函数无法被 LLM 识别的问题搞了一下午终于搞明白为什么我的 MCP Server 工具函数死活不被 Claude 识别。坦白讲MCP 协议本身的文档写得挺清楚但 TypeScript SDK 在工具注册这个环节藏了一个不大不小的坑。记录一下免得后面的人继续踩。问题工具注册了但 LLM 说没有可用工具上周接到一个需求要把内部的一些运维脚本封装成 MCP Server让同事们通过 Claude 或 Cursor 直接调用。按照官方文档定义了工具、写好了 handler、跑起了stdio传输层。本地测试一切正常——mcp-server启动没问题日志也显示工具注册成功。但一连接到 Claude Desktop问题来了Claude 明确说没有可用的工具。我反复检查server.setRequestHandler(ListToolsRequestSchema, ...)写了server.setRequestHandler(CallToolRequestSchema, ...)写了工具列表里明明有我的函数就是识别不了。排查问题出在 zod schema 的describe上一开始我怀疑是传输层的问题换了stdio和sse两种模式结果一样。然后怀疑是 Claude Desktop 的缓存重启了三次还是一样。直到我打开 MCP Inspector 仔细对比官方示例和我的代码终于发现了差别。问题出在z.object()的字段描述上。我的代码是这样的简化后constExecCommandSchemaz.object({command:z.string(),// 没有 describetimeout:z.number(),// 没有 describe});server.setRequestHandler(ListToolsRequestSchema,async(){return{tools:[{name:exec_command,description:执行 shell 命令,inputSchema:zodToJsonSchema(ExecCommandSchema),},],};});看起来没问题但 MCP 协议要求inputSchema必须能被 LLM 理解每个字段的用途和约束。如果字段没有describeClaude 拿到的是一个裸的 JSON Schema它不知道command该填什么、timeout的单位是什么。LLM 对模糊的工具描述会直接拒绝调用而不是瞎猜。解决3 行代码搞定修复方案简单到离谱——给每个字段加上describeconstExecCommandSchemaz.object({command:z.string().describe(要执行的 shell 命令例如 ls -la),timeout:z.number().describe(命令超时时间单位秒默认 30),});就加了这两行.describe()Claude 立刻就能识别并调用工具了。说到底MCP 不是给你人看的是给 LLM 看的。LLM 依赖字段描述来理解工具的语义没有描述就等于白注册。进阶让工具描述更LLM 友好解决了基础问题后我又测试了几轮总结出让 LLM 更好识别的几个技巧1. 描述里带示例不要写目标路径要写目标路径例如 /var/log/nginxpath:z.string().describe(目标文件路径例如 /var/log/nginx/access.log),LLM 看到示例就知道该填什么格式大幅降低幻觉概率。2. 枚举值用z.enum()而不是裸字符串// 不好level:z.string().describe(日志级别可选 debug/info/warn/error),// 更好level:z.enum([debug,info,warn,error]).describe(日志级别),z.enum()会自动生成 JSON Schema 的enum字段LLM 看到这个会严格从列表里选而不是自由发挥。3. 可选字段明确标记retry:z.number().optional().describe(失败重试次数默认不重试),optional()会让 schema 生成required: []的语义LLM 知道这不是必填项不会硬编一个值进去。踩坑记录坑 1以为 zod 的description和describe是一回事zod 本身有z.string({ description: ... })的写法但这个是给 zod 内部用的不会体现在生成的 JSON Schema 里。必须用.describe()才能正确输出到inputSchema。坑 2Claude 的缓存比想象中持久调试的时候改了代码Claude Desktop 还是报没有工具。后来发现在 Claude 设置里点刷新 MCP 工具才生效单纯重启客户端不够。坑 3工具名不要用驼峰用下划线Claude 对execCommand和exec_command的理解没有本质区别但下划线更符合工具命名的惯例。而且 MCP Inspector 里显示也更整齐。坑 4description 字段不要空工具级别的description如果留空Claude 会直接忽略这个工具。哪怕你觉得工具名已经很清楚了也要写一句描述比如执行远程服务器命令并返回输出。写在最后MCP 协议的初衷很好——让 LLM 能安全、结构化地调用外部工具。但协议规范和实际 SDK 实现之间总有一些约定俗成的细节文档不会明说。这次踩坑给我的经验是给 LLM 用的接口描述比实现更重要。你写的代码能跑通只是第一步让 LLM 能正确理解并调用才是 MCP Server 真正可用的标准。如果你也在开发 MCP Server建议直接用 MCP Inspector 做一轮完整测试看看 LLM 拿到的 schema 到底长什么样。很多时候问题不在代码逻辑而在 LLM 看不到你想让它看到的东西。有问题欢迎评论区交流。