做Agent开发这两年,我最头疼的不是模型能力,而是工具怎么接。MCP协议出现后,局面才真正改观:一套基于JSON-RPC 2.0的标准,把零散的函数调用统一成工具层协议,让Agent能按同一套规则发现和调用外部能力。无论你是做AI应用集成、Agent框架编排,还是给现有产品加一个能自主行动的助手,都需要理解MCP到底在哪个层面解决了问题,又该怎么落地。
这篇文章我不打算只念概念。我会从MCP选择JSON-RPC 2.0作为协议底座的原因讲起,把Prompts、Resources、Tools三大原语逐个拆开,再给出一套可以直接抄走的TypeScript实现方案。接着用真实项目里最容易翻车的“工具注册不上”“Agent执行超时”等问题做排查复盘,最后聊聊MCP和Function Calling、Computer Use、Skill这些概念的实际边界。适合那些已经写过Function Calling、但想进一步摆脱“每个模型一套函数格式”的读者,也适合准备把MCP Server接入IDE、设计工具和业务系统的开发者。
1. MCP是什么:Agent为什么需要一个统一的“工具层插座”
1.1 工具调用最混乱的那一年
在MCP协议流行之前,给Agent接工具是一件极其繁琐的事。假设同时遇到一个支持函数调用的模型和一个你正在自研的流程引擎,前者要求把工具定义成JSON Schema塞进请求,后者可能需要单独实现一套回调接口。如果对接的是企业内部服务,还要自己设计鉴权、参数校验、错误重试和工具发现的机制。几个工具倒还好,一旦到了二三十个,光维护工具描述和请求转发就忙不过来。
更麻烦的是,每次换模型厂商,工具定义格式就可能变化。有人为了让同一个Agent跑在不同模型上,不得不写一堆适配层。这个问题的本质不是因为工程师能力不够,而是“Agent调用工具”这件事缺少中间标准。模型厂商只定义了自己API的函数调用格式,但没有定义一套“工具层服务”的统一描述方式。Agent和工具之间,始终存在一个谁说了算、怎么对齐的空白地带。
1.2 MCP给自己的定位:标准化工具访问层
MCP(Model Context Protocol)的定位,用一句话概括:给Agent一个标准化的外部能力接入层。这个协议把“Agent需要看什么数据、能执行什么操作、按什么模板交互”这三种需求抽象成三大原语,再通过JSON-RPC 2.0在客户端和服务端之间传递。客户端的角色是模型和应用,服务端的角色是外部工具、数据源或业务系统。
打个比方可能更好理解:以前每个设备厂商都造自己的充电口,手机、耳机、鼠标全不通用;MCP就是那个USB-C接口,只要设备都遵守MCP协议,任何支持MCP的客户端都可以一键接入。这个统一的意义在于,工具开发者只需要实现一个MCP Server,就可以同时被Claude、Cline、各种自研Agent和IDE使用。用户不再需要为每一个AI客户端单独开发一套工具适配器。
从架构分层来说,MCP关心的不是某个模型内部如何推理,而是“模型所在的客户端”和“外部能力提供方”之间的服务契约。它把工具的发现、调用、数据读取、交互模板、上下文管理、消息通知都标准化了。哪怕底层模型从GPT换到Claude再到自研模型,只要客户端层实现了MCP,上层的工具接入方式基本不用改动。
1.3 标准化之后解决的核心痛点
第一个痛点是工具发现。以前工具列表散落在代码和文档里,MCP通过tools/list、resources/list、prompts/list这类方法,让客户端能动态知道服务端当前暴露了哪些能力。第二个痛点是协议多样性。JSON-RPC 2.0的消息格式在所有语言中处理起来都很简单,MCP借此统一了请求、响应、通知和错误结构。第三个痛点是上下文隔离。MCP把数据读取抽象成Resources,把执行动作抽象成Tools,不会让Agent随意执行一个未经过定义的命令。
第四个痛点是复用成本。我见过很多团队在内部实现了“半套工具协议”,换一个客户端又要重来。MCP把工具层单独拆出来之后,一个MCP Server可以被多个客户端复用,企业内部的工具服务也终于有了统一的接入边界。这也是为什么它很快在IDE插件、调试工具、代码生成、设计工具协作这些场景铺开。
2. 协议底座:JSON-RPC 2.0为什么能撑起Agent工具层
2.1 为什么不是REST,不是WebSocket裸消息
MCP最初设计时面对一个选择:消息格式用REST风格还是直接Socket通信?最终选定了JSON-RPC 2.0,这个选择是有道理的。JSON-RPC 2.0是一个非常轻量的请求-响应协议,消息按行处理,天然适合进程间通信和网络传输。它支持三种消息类型:请求、响应和通知。其中通知不需要接收方返回结果,刚好适合MCP里面“日志输出”“资源变更提醒”这类场景。
REST的核心是“资源地址+HTTP动词”,更擅长暴露网页资源接口,但MCP的通信模式不只是客户端请求服务端。MCP服务端也需要主动向客户端推送通知,比如工具列表变更、日志消息、资源更新。如果只用REST,那么服务端主动推送要么靠轮询,要么另起一套长连接,等于额外造轮子。WebSocket裸消息虽然支持双向通信,但缺少消息ID、错误码、方法名这些标准化工具,最后团队还是要自己定一套封装。JSON-RPC 2.0刚好补齐这些,它自带id关联、method调用、error对象和通知语义。
所以MCP的选择不是“JSON比二进制好”这么简单。真正原因是JSON-RPC 2.0足够通用,双向调用方便,跨语言实现成本低。对C++、Python、TypeScript、Java这些不同技术栈的工具开发者来说,只要会处理JSON对象,就能实现一个MCP端点。这大大降低了生态接入门槛。
2.2 Message信封:request、response、notification、error一次看懂
MCP的消息本身没有发明新协议,而是在JSON-RPC 2.0外面包了一层语义化方法名。常见的请求结构是这种样子:
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "get_weather", "arguments": { "city": "杭州" } } }id是客户端生成的请求标识,服务端返回的响应必须带上同一个id,这样客户端在异步场景里才知道哪个响应对应哪个请求。method表示要调用的MCP方法,比如initialize、tools/list、tools/call、resources/read。params则按方法不同携带不同参数。
响应有两种结果:一种是正常返回:
{ "jsonrpc": "2.0", "id": 1, "result": { "content": [ { "type": "text", "text": "杭州:晴,28℃" } ] } }另一种是错误返回。JSON-RPC 2.0定义了标准的错误码范围:-32700表示解析错误,-32600表示无效请求,-32601表示方法不存在,-32602表示参数无效,-32603表示内部错误。MCP在运行时还会出现业务层面的错误,比如工具执行失败,协议层面依然返回成功,但在工具结果中标记isError: true。这一点很多人第一次接触会踩坑。
通知消息比较特殊,它没有id,也不需要响应。MCP里的notifications/initialized、notifications/tools/list_changed都是通知。客户端发完通知之后不需要等服务端确认,这也符合“我告诉你一声,你知道了就行”的定位。
2.3 传输层设计:stdio和Streamable HTTP各管一段
MCP协议并不强制绑定某一种传输方式,实践中最常见的两种是本地进程用的stdio和网络场景用的Streamable HTTP。stdio模式适用于MCP Server和客户端在同一台机器上。客户端启动子进程,通过标准输入给服务端发送JSON-RPC消息,服务端处理完通过标准输出返回结果。这种模式的优点是启动快、无需监听端口,本地文件操作和命令行工具类MCP Server基本都用它。
但stdio模式有个容易让人崩溃的细节:日志不能写到标准输出。很多开发者在MCP Server里用console.log打印调试信息,结果协议消息被日志污染,客户端解析直接失败。正确做法是把日志写到stderr或独立文件。我在troubleshoot阶段经常提醒自己:stdio只负责传输协议消息,别把业务日志往里塞。
远端场景使用的是Streamable HTTP。这个传输方式相比早期HTTP+SSE方案更简洁,客户端和服务端通过HTTP POST交换JSON-RPC消息,服务端可以用Server-Sent Events把消息流式推给客户端,适合MCP Server跑在远程服务器上。服务端还可以通过sessionId维护会话状态,让多次请求共享上下文。正因为MCP有了这个传输层,不同团队才可以将自己的Agent工具服务部署在云上,而不必要求每个客户端都跑到本地拉代码。
3. 三大原语拆解:Prompts、Resources、Tools是如何协同的
3.1 三大原语全景与分工
MCP协议在语义层面定义了三大原语:Prompts、Resources、Tools。很多初学者把它们都理解成“给模型用的接口”,这样会乱。我的理解方式一直是用“阅读、执行、模板”三个词去对号入座。Resources代表可读取的数据,比如一份文档、一个数据库查询结果;Tools代表可执行的动作,比如发送邮件、创建工单;Prompts代表可复用的交互模板,帮助开发者或用户快速进入某类任务场景。
从客户端视角看,这三种原语都是通过JSON-RPC方法暴露出来的。服务端在握手时先声明自己支持哪些能力,客户端再调用resources/list、tools/list、prompts/list把能力清单拉回来。区别在于数据流方向和使用方式:Resource是内容检索,通常被嵌入上下文;Tool是执行命令,执行结果要返回给客户端;Prompt则是“一段按模板拼出来的消息列表”,由客户端决定是否交给模型继续处理。
用一个场景串联会更好懂。假设你要开发一个“发布检查助手”,Resources可以暴露一份《发布检查清单》文档;Tools可以提供一个“更新检查项状态”的执行能力;Prompts则预先写好“请基于发布清单帮我做上线评审”的完整模板。Agent先读Resource拿到清单内容,再按Prompt组织用户问题,最后通过Tool更新状态。三大原语不是三个独立功能,而是围绕同一个业务目标的三个切面。
3.2 Resources原语:把数据源变成Agent的“可寻址文件系统”
Resources的核心模型是URI。每个资源都通过一个URI来标识,类似file:///etc/config.json、https://example.com/docs/guide、或者自定义的docs://release-checklist。服务端通过resources/list告诉客户端自己有哪些资源,客户端通过resources/read读取指定资源内容。资源内容可以是纯文本,也可以是图片的base64编码,这给多模态Agent留了余地。
Resource还支持模板。比如客户端想看某篇具体文档,不一定需要把所有文档全部列出来,可以声明一个docs://{id}模板,客户端按需填充参数再读取。这个设计很像REST里的路径参数,但封装在MCP协议层,客户端不需要理解业务URL规则,只要知道协议用法就行。
我自己的经验是,Resources适合存那些相对稳定、不需要“执行副作用”的内容。比如产品文档、代码规范、日志片段、知识库文档。读取操作应该是幂等的,最好不影响外部系统状态。如果某个资源需要实时查询,可以让服务端在resources/read内部完成查询并返回格式化结果,但对客户端来说它只是拿到了一段内容,具体来源被隐藏了。
3.3 Tools原语:给模型提供可执行的标准化函数入口
Tools是所有MCP能力里最受关注的一块。一个Tool的定义通常包括名称、描述、参数Schema,以及服务端具体的执行逻辑。这个结构看起来和模型厂商的Function Calling非常像,但区别在于MCP把“函数列表的发现”和“函数的执行”做成了协议接口。框架SDK和AI客户端可以通过标准方法自动发现工具,而不需要每个开发者单独去阅读工具提供方的文档。
一个工具调用的链路是这样:客户端首先发送tools/list获取工具列表,模型根据用户问题决定调用哪个工具,然后客户端发送tools/call,把工具名和参数发给MCP Server,服务端执行后返回结果。举个具体例子:
{ "jsonrpc": "2.0", "id": 10, "method": "tools/call", "params": { "name": "query_order", "arguments": { "orderId": "A20250101" } } }服务端返回内容时可以携带普通文本,也可以携带结构化JSON。真正让我觉得MCP设计比较贴心的地方是isError字段。如果工具执行遇到业务错误,比如“订单号不存在”,服务端依然可以在协议层面返回成功,但把isError设为true并给出错误文本。这样模型能看见错误信息,客户端也不会把工具异常误判成MCP连接断开。
在实现上,工具不要做得太细碎。把三个操作合成一个更粗粒度的“工具”,比如“批量更新状态”而不是“更新状态一”“更新状态二”,可以减少模型选错工具的几率。同时工具描述要写清楚适用场景,否则Agent在几十个工具里容易懵。
3.4 Prompts原语:服务端主动发起的“最佳实践交互模板”
Prompts在MCP里容易被误解成普通的自然语言提示词。其实它更应该被理解成一个结构化的“模板服务”。服务端可以定义多个Prompt,每个Prompt有名称、描述和参数。客户端通过prompts/get获取渲染后的消息列表,这些消息可以直接拼进对话上下文。
比如一个代码评审MCP可以定义prompt.review_code模板,参数包括language和branch。客户端调用后,返回一条user消息:“请用资深工程师视角,对branch为xxx的Go代码变更做评审,重点看并发安全、错误处理和性能隐患。”模板的价值在于,让接入同一个MCP Server的所有客户端都能复用同一套提示策略,不需要每个用户自己复制粘贴prompt。
在实际项目中,Prompt原语还能承担一部分“技能封装”的作用。团队可以把某类常见业务场景的最佳提问方式、思考框架、输出规范都沉淀成Prompt。模型在处理时不需要重新理解复杂背景,只要客户端把Prompt取出来发给模型即可。当然,Prompt模板写得太死也会限制模型,因此我通常会把模板设计成带变量的半结构化文本,保留模型自由发挥的空间。
3.5 除了三大原语,还需要关注的协议细节
MCP里还有一些原语之外的内容,它们对构建稳定的Agent工具层同样重要。采样请求(sampling)允许MCP Server反过来请求客户端调用LLM完成一次补全,这主要用于服务端需要模型判断的场景。Roots是客户端告诉服务端“当前项目根目录在哪”的机制,方便服务端定位项目文件。还有logging能力,服务端可以将日志消息按级别发给客户端,方便联调时看问题。
动态能力变化也值得一提。如果一个MCP Server在运行过程中新增了工具,它可以发送notifications/tools/list_changed通知客户端重新拉取工具列表。这意味着Agent工具层不是一次配置就静态不变的,而是可以随着业务状态动态演进。理解这点之后,你会明白MCP其实非常强调“运行时发现”,这也是它能支撑复杂工具生态的基础。
4. 从零搭建一个MCP Server:架构设计、代码实现与协议联调
4.1 语言与SDK选型
目前MCP官方SDK覆盖了TypeScript、Python、Java、C#、Go等主流语言,社区还有Rust、PHP等实现。选择哪种语言不是看哪个更“流行”,而是看你的MCP Server需要接什么系统。如果Server主要面向IDE插件和前端工具,用TypeScript最顺手;如果Server要处理数据分析或机器学习流程,Python更容易集成;如果服务部署在Java中间件里,我建议直接用Java SDK而不是另起子进程,这样能少一层通讯开销。
我自己的默认推荐是TypeScript SDK。原因很简单:SDK文档最完整、生态示例最多,而且MCP Server经常要嵌入到各种基于Node的AI工具中。在运行时里,MCP Server只是一个个模块,不需要额外起Docker容器,调试也方便。早期MCP SDK的API迭代比较快,写代码前先确认本地安装的@modelcontextprotocol/sdk版本,避免看到老文档后误用废弃API。
4.2 用TypeScript SDK实现一个“发布检查助手”Server
接下来我们实现一个实际例子:一个围绕开发发布场景构建的MCP Server。它提供一个Tool用来标记检查项结果,一个Resource暴露发布检查清单,一个Prompt生成上线评审模板。先初始化项目:
mkdir release-mcp-server cd release-mcp-server npm init -y npm install @modelcontextprotocol/sdk zod npm install -D typescript @types/node新建src/index.ts,内容如下:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; const server = new McpServer({ name: "release-checklist-server", version: "0.1.0" }); server.tool( "update_check_item", "更新某个发布检查项的通过状态", { itemName: z.string().describe("检查项名称,例如:数据库备份"), passed: z.boolean().describe("该检查项是否已通过") }, async ({ itemName, passed }) => { return { content: [ { type: "text", text: `${itemName}:${passed ? "通过" : "未通过"}` } ] }; } ); server.resource( "release-checklist", "docs://release-checklist", async (uri) => ({ contents: [ { uri: uri.href, text: "一、数据库备份完成\n二、灰度策略确认\n三、监控告警已配置\n四、回滚方案就绪" } ] }) ); server.prompt( "review-release", "生成发布评审用的提示模板", { version: z.string().describe("版本号") }, ({ version }) => ({ messages: [ { role: "user", content: { type: "text", text: `请基于发布检查清单,评估版本 ${version} 是否满足上线条件。` } } ] }) ); const transport = new StdioServerTransport(); await server.connect(transport);这段代码里,server.tool的第一个参数是工具名,第二个是工具描述,第三个是参数Schema,第四个是实际执行的异步函数。工具参数用zod定义后,SDK会在请求进来时做参数校验。如果客户端传错参数,SDK会直接返回JSON-RPC参数无效的错误,省去手工判断的麻烦。server.resource注册的是一个静态资源,实际项目中它的回调逻辑可以是查数据库、读文件或把搜索结果整理成文本。server.prompt注册的是一个可复用的交互模板。
编译运行:
npx tsc node dist/index.js这一行命令不会给标准输出打印任何内容,因为MCP Server在stdio模式下正在等待客户端通过标准输入发送JSON-RPC消息。你可以用MCP Inspector调用它,也可以把它配置到支持MCP的客户端里。注意实际连接后只要保持在后台监听即可,不要误以为“没有输出就代表没运行”。
4.3 Client侧接入:Claude Desktop、Cherry Studio及其他客户端
MCP Server写好后,需要被客户端加载。如果你使用Claude Desktop这类原生支持MCP的客户端,配置入口一般在客户端的配置文件里,本质上就是一份MCP Server注册表。下面是一个典型的stdio配置:
{ "mcpServers": { "release-server": { "command": "node", "args": ["/absolute/path/to/dist/index.js"], "env": {} } } }关键点有三个:command必须是服务端进程的真实启动命令;args里的路径要用绝对路径,别写相对路径;如果是打包后的程序,先手动在终端跑一下确认没有报错,再填进配置。Cherry Studio这类带图形管理界面的客户端操作会友好很多,你可以直接在设置界面新增MCP Server,填入命令和参数,界面会展示工具列表是否加载成功。
远程MCP Server的配置则是填一个HTTP地址,并且通常需要处理鉴权。很多企业级MCP Server会做OAuth授权,客户端第一次连接时会弹出授权流程。遇到工具注册不上的情况,先不要怀疑配置格式,把服务端地址直接放到浏览器或调试工具里访问一下,往往能发现是网络不通还是授权过期。
4.4 不看SDK也能看懂协议:手把手走一次JSON-RPC消息交互
SDK封装得太好,容易让人忽略底层协议。我曾经在调试一个跨语言MCP Server时,因为客户端和服务端SDK版本差异导致工具列表始终为空,最后只能手工构造JSON-RPC消息定位问题。学会看底层消息顺序,排错效率会高很多。
MCP通信的第一步永远是initialize请求。客户端发送自己的名称、版本,以及支持的协议版本和客户端能力。服务端返回它选定的协议版本和服务端能力。紧接着客户端要发送notifications/initialized通知,告诉服务端握手完成。下面是一组示意报文:
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"manual-client","version":"1.0.0"}}}服务端正常响应后,客户端再发:
{"jsonrpc":"2.0","method":"notifications/initialized"}之后才能发送tools/list:
{"jsonrpc":"2.0","id":2,"method":"tools/list"}如果看到响应里返回一个空数组或空对象,说明服务端当前没有暴露任何工具。这时候我会去检查服务端代码中注册的工具名是否拼错,或者服务端是否因异常提前退出。手动构造消息能直接定位是客户端问题还是服务端问题,而不是在两边日志里反复纠结。
5. 协议边界对比:MCP、Function Calling、Computer Use、Skill有什么区别
5.1 和Function Calling的真实边界
很多人问:“MCP不就是Function Calling换个格式吗?”这个理解方向对,但不完整。Function Calling更像模型API内部的一种参数机制,由模型根据历史对话生成工具调用参数,由开发者把工具定义和模型推理绑定在一起。MCP则是独立于模型的互联协议,它更像一个中间市场:工具方以MCP Server的身份入驻,Agent客户端负责统一发现和调用。
也就是说,Function Calling关注的是“模型如何决定调函数”,MCP关注的是“客户端如何稳定访问各种外部能力”。一个Agent的完整链路可以是:模型根据对话决定调用工具,客户端把这次调用翻译成MCP请求发给Server。MCP不替代Function Calling,而是为Function Calling后面那一大堆工程问题提供标准答案。
5.2 和Computer Use的分工:操作界面 vs 暴露接口
MCP和Computer Use是最近经常被放到一起比较的两个词,但二者的目标层次完全不同。Computer Use更接近“让模型像人一样操作图形界面”,模型通过截图、鼠标点击、键盘输入来完成任务。这种方案适合那些没有API、没有命令行、只能靠人手的遗留系统,但代价是运行慢、脆、不可控。
MCP是让模型通过结构化接口直接操作服务,不需要理解图标位置和输入框坐标。比如一个Web应用如果提供了“创建工单”的MCP工具,Agent可以直接按字段调用;如果用Computer Use方案,它可能要先截屏找到工单按钮,再模拟点击。前者可靠、可审计,后者灵活、覆盖面广。两者互补,但边界很清楚:能提供接口的系统优先用MCP,没有接口的系统才值得考虑Computer Use方案。
5.3 与Skill的区别:内容包和协议服务不能混为一谈
Skill相关的概念在Agent框架中经常出现,它通常是一组提示词、示例和少量辅助代码的集合,用于教会模型完成某一类专业任务。Skill强调的是“模型行为模式”的封装。MCP则强调“服务能力”的标准化暴露。两者不是非此即彼,而是可以配合。一个Agent可以有一个“代码评审Skill”,里面有详细的评审提示词;这个Skill内部又调用了一个基于MCP的代码扫描工具,从外部系统获取需要被评审的代码片段。
实际项目中如果分不清楚,最简单的判断方法是:接入方是一个纯模型产品,还是一个新服务。如果你的目标是复用一套业务能力和上下文,优先做MCP Server;如果你的目标是复用一段模型推理时的思考路径和风格,优先沉淀Skill。现在很多Agent框架也在把MCP Server注册后的工具能力和Skill结合起来,形成“技能路由表”,未来它们之间的边界可能会越来越模糊。
5.4 MCP在IDE、设计工具和安全测试场景里的生态现状
MCP生态最明显的变化是IDE插件和设计工具快速接入。像Figma、Unity、Cocos Creator、MATLAB这类专业软件,第三方团队陆续封装了MCP Server,让AI助手可以查询设计稿、读取场景资源或执行脚本。比如Figma MCP出现后,AI代码生成工具可以直接读取设计稿上的图层结构,把设计信息转化为代码上下文,极大减少了手动复制图层的操作。
安全测试工具的MCP化也是一个趋势。Burp Suite这类工具暴露MCP服务后,Agent可以在授权测试过程中自动读取请求响应、分析漏洞线索。Playwright MCP则把浏览器自动化能力开放给Agent,让模型能自己打开网页执行操作并读取页面结果。这些生态案例说明,MCP已经不只是“技术圈自嗨的协议”,而是逐渐成为AI客户端和外部专业工具之间的事实接入标准。
6. 常见问题与排查技巧实录
6.1 工具注册不上、工具列表为空怎么办
工具注册不上是MCP接入里最高频的问题。遇到时我会按顺序排查:先看MCP Server进程是否真的启动成功。stdio模式下,直接在终端手动执行配置里的command和args,如果运行后立刻报错,配置里填得再漂亮也没用。再看客户端有没有成功读取工具列表。打开MCP Inspector连上Server,主动发送一次tools/list,如果列表为空,问题多半在Server侧注册逻辑。
协议版本也可能导致工具列表“突然消失”。有些老Server只兼容旧版协议,新的客户端用最新protocolVersion去握手,Server不支持就会断开或返回空结果。排查时可以抓握手时的protocolVersion字段,服务端返回的版本如果和客户端不匹配,去升级Server SDK或固定客户端协议版本。
对于Figma MCP、Playwright MCP这类需要外部授权的Server,工具注册不上还可能是token过期。这类Server启动没问题,默认工具也存在,但在真正执行某些操作时要重新走授权。表现为客户端能看到工具列表,但工具调用返回401或认证失败。遇到这情况不要马上怀疑代码,先去对应平台刷新授权状态。
6.2 Agent执行超时和“provider did not respond in time”类错误怎么处理
在代理平台或IDE插件里运行Agent时,如果底层MCP Server处理耗时过长,客户端经常会报出类似“agent execution provider did not respond in time”的错误。这类报错表面上是Agent执行器问题,实际很多时候是某个MCP Server任务没有及时返回。JSON-RPC请求是有关联超时时间的,MCP Server如果在执行一个外部API调用时卡住了,超过客户端等待窗口,就会被判定为执行失败。
我的处理办法分三步:先给MCP Server增加日志输出,记录每个tools/call的进入时间和完成时间;再找到耗时超过5秒的工具,看它是可以优化还是必须长耗时;如果工具确实要几分钟才返回,就不要让MCP调用同步等待,把耗时任务改造成“提交任务后返回任务ID,再通过另一个工具查询任务状态”的异步模式。这类异步任务模式对Agent反而更友好,因为模型可以在等待时继续处理其他工作。
6.3 用好MCP Inspector和原始报文排查问题
MCP Inspector是排查MCP问题最好的工具之一。它可以加载一个本地Server或连接远程Server,图形化展示工具、资源和Prompt列表,也允许开发者手动发送JSON-RPC请求。我平时很多“工具列表为空”“资源读取失败”的定位,都是先通过它发送一条原始请求缩小排查范围的。
使用Inspector时注意区分stdio和HTTP两种场景。stdio模式让Inspector启动一个子进程进程并接管它的输入输出;HTTP模式填远程地址即可。查问题时我会先看Console面板里的原始消息,因为SDK层往往会把错误吞掉,原始消息里反而能看到协议版本的协商情况、错误码和Server返回的完整错误描述。再结合Server的stderr日志,基本能确定是SDK封装问题还是代码逻辑问题。
6.4 安全与权限:工具层越标准,越要管好执行入口
MCP让工具接入变得标准,也意味着Agent自动执行动作的门槛比以前更低。以前一个工具函数藏在业务代码里,至少要经过内部代码评审才会被调用;现在只要MCP Server暴露了Tools,任何能连上该Server的客户端都可能触发执行。因此工具的入参校验、身份鉴权、操作审计,必须在Server层面做扎实。
我会给每个工具都加参数白名单。比如update_check_item虽然只是更新状态,也要校验传入的itemName是否在合法集合里,否则容易被恶意或错误参数“造出”奇怪数据。MCP Server只负责协议接入,真正的权限判断要落在业务服务里,不要指望协议本身帮你做安全隔离。远程MCP Server开启鉴权,本地stdio Server也要通过环境变量传递API Key,严禁把密钥硬编码在工具调用参数里。最后,所有工具执行都应该记录审计日志,否则Agent出了问题很难回溯。
6.5 问题排查速查表
| 现象 | 可能原因 | 快速排查方向 |
|---|---|---|
| 工具列表为空 | Server未启动成功或未注册工具 | 手动执行启动命令,发送tools/list |
| 工具注册不上 | 配置路径错误、鉴权过期、协议版本不兼容 | 检查MCP配置绝对路径,进入MCP Inspector看握手结果 |
| 工具调用无响应 | Server执行阻塞,JSON-RPC请求超时 | 给Server加日志,确认tools/call进入时间和返回时间 |
| 无法读取Resource | URI写错或资源回调抛异常 | 用resources/list获取真实URI,再发送resources/read |
| stdout日志污染 | console.log输出到标准输出 | 将调试日志改到stderr或独立文件 |
| Prompt不生效 | 客户端没有主动调用prompts/get | 确认客户端是否实现了Prompt原语,而非把所有消息透传给模型 |
做Agent项目之后我心里有一个越来越清晰的判断:模型的聪明程度只会越来越强,但工具接入的工程能力会一直决定项目能走多远。MCP一开始看起来又是一个新名词,但真正用起来之后,你会发现它解决的是“模型怎么和外部世界协作”这个老问题。如果现在再让我重做一次工具接入,我不会再为每个客户端分别写适配层,而是先把能力边界拆成Resources、Tools和Prompts,再按MCP协议暴露出去。这套思路放到任何业务系统里都适用,也值得你亲手做一个小Server试一遍。