摘要:总结MCP开发中最常见的10个错误,包括传输配置问题、工具定义格式错误、生命周期管理遗漏、参数校验缺失等,每个错误附复现场景和解决方案,帮助开发者避坑。
新手避坑指南MCP开发中最常见的10个错误
我把过去几个月写MCP Server踩过的坑整理成了这篇文章。这10个错误都是我亲自踩过的,有些花了几分钟解决,有些折腾了大半天。每个错误我都会给出错误现象、原因分析和解决方案,希望能帮你少走弯路。按类别分成环境配置、协议理解、代码实现和调试排查四组。
错误一 stdout输出污染协议消息
错误现象
Server连上Claude Desktop后没有任何响应,Claude提示服务器连接失败。终端单独运行server也看不到任何错误,但Inspector连接后报JSON解析错误。
原因分析
stdio传输模式下,server的stdout是JSON-RPC消息的唯一通道。你在代码里用console.log()(TypeScript)或print()(Python)输出调试信息,这些文字混进了stdout的消息流里。客户端收到这些非JSON内容后解析失败,直接判定连接异常。
我自己犯过一次特别蠢的错误。我在工具回调里加了一行console.log("收到请求", params)想看参数对不对,结果加了这行之后所有工具调用都失败了。因为stdout里多了一段"收到请求 …"的纯文本,紧跟在正常的JSON-RPC响应后面,客户端把两段内容拼在一起解析,当然报错。
解决方案
TypeScript里所有日志用console.error()写到stderr。Python里用print(..., file=sys.stderr)或者配置logging库输出到stderr。stderr的内容不会干扰stdout上的协议消息,客户端可以选择捕获或忽略stderr日志。
// 错误写法,会破坏协议console.log("处理请求",params);// 正确写法,输出到stderrconsole.error("处理请求",params);# 错误写法print("处理请求",params)# 正确写法print("处理请求",params,file=sys.stderr)# 或者用loggingimportlogging logging.info("处理请求 %s",params)如果你用的是HTTP传输模式,stdout随便写都没事,因为HTTP模式的响应走HTTP body,不走stdout。但养成用stderr的习惯总没错。
错误二 package.json缺少ESM声明
错误现象
TypeScript项目编译没问题,运行时直接报错Error [ERR_REQUIRE_ESM]: require() of ES Module。
原因分析
MCP SDK的导入路径是@modelcontextprotocol/sdk/server/mcp.js,以.js结尾的ESM模块。Node.js判断一个模块是CommonJS还是ESM,看package.json里的"type"字段。没声明"type": "module"时,Node默认按CommonJS处理,遇到ESM导入就报错。
解决方案
package.json里加上"type": "module"。
{"name":"my-mcp-server","version":"1.0.0","type":"module","scripts":{"build":"tsc","start":"node build/index.js"}}同时tsconfig.json的module和moduleResolution都要设成Node16或NodeNext,跟ESM声明保持一致。
错误三 zod版本与SDK不匹配
错误现象
安装依赖后TypeScript编译报一堆类型错误,错误信息涉及zod的内部类型,比如Type 'ZodString' is not assignable to type ...。
原因分析
@modelcontextprotocol/sdk内部依赖zod 3.x的API。如果你装了zod 4.x,两个版本的类型定义不兼容,TypeScript编译器就炸了。zod 4对类型系统做了大改,很多内部类型结构变了。
解决方案
锁死zod 3.x版本。
npminstall@modelcontextprotocol/sdk zod@3如果你项目里其他依赖已经引入了zod 4,用npm overrides强制降级。
{"overrides":{"zod":"3.23.8"}}或者把MCP Server单独拆成一个子项目,避免依赖冲突。
错误四 工具返回格式不符合MCP规范
错误现象
工具在Inspector里能调用,也返回了结果,但Claude Desktop里调用后显示"工具执行出错"或者结果为空。
原因分析
MCP规范要求工具返回一个特定结构的对象,content字段是数组,每个元素包含type和text(或其他类型)。很多人直接返回字符串或自定义对象格式,客户端解析不到content就判定失败。
我见过三种典型错误写法。第一种直接返回字符串。第二种返回{ text: "结果" }忘了包content数组。第三种content数组里的元素少了type字段。
解决方案
严格按规范返回。content数组里每个元素必须有type字段,文本结果用type: "text"。
// 错误写法1 直接返回字符串return"查询结果";// 错误写法2 忘了content数组return{text:"查询结果"};// 错误写法3 少了type字段return{content:[{text:"查询结果"}]};// 正确写法return{content:[{type:"text",text:"查询结果"}]};如果工具执行失败,加上isError: true字段,客户端会据此判断是否把错误信息展示给用户。
return{content:[{type:"text",text:"ID不存在"}],isError:true};错误五 initialize握手未完成就发请求
错误现象
自己写MCP客户端时,连接server后立即调用tools/call,server返回错误码-32600 Invalid Request,或者直接没响应。
原因分析
MCP协议规定握手必须按顺序完成。客户端先发initialize请求,等server返回initialize响应后,客户端必须发一个notifications/initialized通知,然后才能进入正常操作阶段。在server收到initialized通知之前,它不应该处理任何业务请求。
很多人写客户端时漏掉了发initialized通知这一步,或者initialize响应还没回来就迫不及待地发tools/list。
解决方案
严格按三步走。
// 第一步 发送initialize请求send({jsonrpc:"2.0",id:1,method:"initialize",params:{protocolVersion:"2025-06-18",capabilities:{},clientInfo:{name:"my-client",version:"1.0.0"}}});// 第二步 等到收到initialize响应后,发送initialized通知// 通知没有id字段,不需要响应send({jsonrpc:"2.0",method:"notifications/initialized"});// 第三步 现在才能发业务请求send({jsonrpc:"2.0",id:2,method:"tools/list"});用官方SDK的话这些都帮你处理好了,但了解底层流程有助于排查问题。
错误六 环境变量未在配置中传递
错误现象
Server在终端直接运行一切正常,配到Claude Desktop或Inspector里就连不上数据库,报认证失败。
原因分析
stdio模式下,host应用启动server子进程时,只继承有限的环境变量。你终端里有DATABASE_URL、API_KEY这些变量,但子进程不一定能继承到。具体继承哪些变量跟操作系统和host实现有关。
解决方案
在host的配置文件里显式声明环境变量。Claude Desktop的配置如下。
{"mcpServers":{"myserver":{"command":"node","args":["/absolute/path/to/build/index.js"],"env":{"DATABASE_URL":"postgresql://localhost/mydb","API_KEY":"your-api-key"}}}}Inspector里在连接面板的Environment区域添加键值对。
我建议用.env文件管理环境变量,开发时用dotenv库加载。但部署到host时,host的env配置优先级更高,因为子进程可能读不到.env文件(工作目录不确定)。
错误七 Windows路径反斜杠导致连接失败
错误现象
Windows上开发MCP Server,配置到Claude Desktop后死活连不上,报找不到文件或模块。
原因分析
Claude Desktop的配置文件是JSON格式。Windows路径用反斜杠分隔,比如C:\Users\name\project。但JSON里反斜杠是转义字符,C:\Users里的\U会被当成转义序列处理,导致路径损坏。
解决方案
两种写法都行。第一种用双反斜杠。
{"mcpServers":{"myserver":{"command":"node","args":["C:\\Users\\name\\project\\build\\index.js"]}}}第二种用正斜杠,Windows的Node.js和Python都支持正斜杠。
{"mcpServers":{"myserver":{"command":"node","args":["C:/Users/name/project/build/index.js"]}}}我推荐用正斜杠,简单不容易出错。command字段里的可执行文件路径也要注意,uv和node最好写完整路径,因为host的PATH环境可能跟终端不一样。
错误八 能力协商不匹配导致-32602错误
错误现象
Server运行正常,但调用特定功能时客户端返回错误码-32602 Invalid params。比如server想请求客户端做sampling,报错说不支持。
原因分析
MCP协议在initialize握手时做能力协商。客户端和server各自声明自己支持的能力,只有双方都声明的能力才能使用。如果server发了sampling请求但客户端没在initialize里声明sampling能力,就会报-32602错误。
这个错误很隐蔽,因为握手本身是成功的,server也正常启动了,只有用到特定功能时才暴露。
解决方案
检查initialize握手时双方声明的能力。用Inspector连接server,在通知面板里查看initialize请求和响应的完整内容。
客户端能力包括roots(提供文件系统根目录)、sampling(支持LLM采样)、elicitation(支持服务端向用户提问)。Server能力包括prompts、resources、tools、logging、completions。
如果你的server需要用sampling,确保客户端声明了这个能力。Claude Desktop支持sampling,但一些轻量客户端可能不支持。代码里做防御性处理。
// 检查客户端是否声明了sampling能力if(!clientCapabilities?.sampling){// 降级处理,用本地逻辑代替samplingreturnfallbackResult;}错误九 stdio消息包含嵌入换行符
错误现象
Server返回包含多行文本的工具结果时,客户端偶尔解析失败。短文本没问题,长文本或包含换行的文本容易出问题。
原因分析
stdio传输的消息按换行符分隔。协议规范明确要求消息MUST NOT包含嵌入的换行符。如果你在JSON-RPC消息的文本内容里有\n,并且消息序列化时这些换行符没被正确转义,就会把一条消息截断成两条,客户端解析第二条时失败。
通常JSON.stringify会正确转义字符串里的换行符为\n字面量,所以大多数情况没问题。但如果你手动拼接JSON字符串,或者用了某些会保留原始换行符的序列化方式,就会出问题。
解决方案
永远用JSON.stringify序列化消息,别手动拼JSON字符串。确保消息在写入stdout时是单行的。
// 正确做法 用JSON.stringify自动转义换行符constmessage=JSON.stringify({jsonrpc:"2.0",id:1,result:{content:[{type:"text",text:"第一行\n第二行\n第三行"}]}});// JSON.stringify会把\n转义成\\n,输出是单行process.stdout.write(message+"\n");# Python同理importjson message=json.dumps({"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"第一行\n第二行"}]}})# json.dumps默认转义换行符sys.stdout.write(message+"\n")sys.stdout.flush()错误十 请求超时未处理导致连接挂起
错误现象
工具执行时间较长(比如调用外部API),客户端等了很久没响应,最终连接卡死或超时断开。Server这边其实还在执行,但结果发回去时客户端已经不听了。
原因分析
MCP协议建议所有请求都设置超时。客户端等不到响应会认为请求失败,可能发送取消通知或直接断开连接。如果你的工具执行一个耗时30秒的API调用,而客户端超时设为10秒,就会出问题。
解决方案
两方面处理。第一,工具内部做好超时控制,别让单个请求卡太久。
asyncfunctioncallExternalAPI(url:string):Promise<string>{constcontroller=newAbortController();// 设置10秒超时consttimeout=setTimeout(()=>controller.abort(),10000);try{constresponse=awaitfetch(url,{signal:controller.signal});returnawaitresponse.text();}catch(err){if(errinstanceofError&&err.name==="AbortError"){return"请求超时,请稍后重试";}throwerr;}finally{clearTimeout(timeout);}}第二,长时间任务用进度通知。MCP支持在工具执行过程中发送notifications/progress通知,告诉客户端进度。客户端收到进度通知可以重置超时计时器。
server.registerTool("long_task",{description:"执行耗时任务",inputSchema:{steps:z.number().int().positive().describe("执行步数"),},},async({steps},context)=>{constresults=[];for(leti=0;i<steps;i++){// 执行每一步awaitdoStep(i);results.push(`步骤${i}完成`);// 发送进度通知// context里可以访问session发通知// progressToken从请求的_meta字段获取}return{content:[{type:"text",text:results.join("\n")}],};});错误归类总结
把上面10个错误按类别归一下。
| 类别 | 错误编号 | 共性问题 |
|---|---|---|
| 环境配置 | 二、三、六、七 | 依赖版本、模块系统、环境变量、路径格式 |
| 协议理解 | 五、八、九 | 握手顺序、能力协商、消息格式 |
| 代码实现 | 一、四 | 输出通道、返回格式 |
| 调试排查 | 十 | 超时处理、进度反馈 |
环境配置类的错误最多,占了4个。这些错误的特点是代码逻辑没问题,但因为运行环境差异导致失败。解决办法是固定开发规范,zod锁版本、ESM必声明、路径用绝对正斜杠、环境变量显式传。
协议理解类的错误最隐蔽,因为握手看起来成功了,只有用到特定功能才暴露。养成用Inspector查看完整initialize握手消息的习惯,能提前发现能力不匹配的问题。
小结
这10个错误覆盖了MCP开发中最常见的翻车场景。环境配置类注意zod锁版本、ESM声明、环境变量传递和路径格式。协议理解类注意握手三步走、能力协商和消息换行符。代码实现类注意stdout只发协议消息、工具返回用标准content数组。调试排查类注意超时控制和进度通知。核心原则就是别跟规范较劲,严格按协议文档来,能避开绝大部分坑。
相关推荐
- 5分钟跑通你的第一个MCP Server(Python版)
- 安全防护基础:认证授权、输入消毒、权限控制
- 测试与调试:MCP Inspector、单元测试、集成测试