ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

MCP架构拆解:从握手到执行的协议全解析

MCP架构拆解:从握手到执行的协议全解析 很多人第一次接触 MCP是在 Cursor、Claude Desktop、Trae 这类客户端的配置界面里填一条npx命令或者贴一个http://地址然后就看到 AI 可以操纵 Figma、读本地文件、查数据库、甚至调 Burp Suite。但配置能用和真正理解 MCP 之间通常隔着一道坎——那道坎的名字就叫“握手”。有意思的是如果你现在去搜“MCP 握手”大概率搜出来一大堆 TCP 三次握手、TLS 握手的文章跟 MCP 几乎没有关系。这个现象本身就很能说明问题MCP 里的“握手”是一个概念复用但实际机制和应用层语义跟 TCP/TLS 完全不是一回事。如果你分不清它们后面排查问题会非常痛苦。这篇作为入门系列的第二篇我就沿着“从握手到执行”这条主线把 MCP 的架构设计从头到尾拆一遍让你既能读懂 server 端的日志也能自己设计一个能被客户端稳定接入的 MCP server。1. 先分清三种“握手”别把 MCP 握手跟 TCP/TLS 搞混网上关于“握手”的内容至少有三套语境先花五分钟把它们分开后面能少踩一半的坑。1.1 TCP 三次握手建立传输层连接TCP 三次握手解决的是“两个进程之间能不能可靠收发字节流”的问题。客户端发 SYN服务端回 SYN-ACK客户端再回 ACK连接建立。它工作的位置在网络传输层目的是确认双方的收发能力都正常然后才开始传数据。这个过程跟应用层传什么内容没有任何关系HTTP 请求也好、JSON-RPC 消息也好在 TCP 眼里都是字节流。1.2 TLS 握手在传输之上加一层加密协商TLS 握手在 TCP 连接之上进行客户端和服务端交换 Hello 消息、协商加密套件、验证证书、交换密钥参数最后双方各自计算出相同的会话密钥。它的核心诉求是“数据在传输过程中不能被偷看和篡改”。所以 TLS 握手关心的是身份认证和加密参数跟业务逻辑依然没有关系。1.3 MCP 里的握手应用层的能力与版本协商MCP 的握手发生在应用层机制上跟前面两者有很大不同。它是在客户端和服务端之间已经建立好了可通信的通道之后通过一次initialize请求-响应交互交换彼此的协议版本、能力声明、客户端和服务端的标识信息。这次交互完成后客户端还要主动发一个notifications/initialized通知告诉服务端“我的初始化已经完成可以开始处理正式请求了”。三者目的不同、层级不同、失败表现也不一样我经常用下面这个表格跟团队新人讲清楚对比项TCP 三次握手TLS 握手MCP 握手工作层级传输层会话/加密层应用层核心目的建立可靠字节流连接身份认证 密钥协商协议版本 能力协商发起时机建立连接前TCP 连接建立后应用通道就绪后失败时的表现连接超时 / Connection refused证书错误 / SSL 握手失败Initialize 超时 / 返回协议错误是否影响业务语义否否是直接决定后续能力可用性为什么必须先分清这个因为实际联调时报错信息里一旦出现“握手失败”四个字很多人第一反应是去查 TCP 端口通不通、TLS 证书对不对折腾一圈才发现问题出在 MCP 的 initialize 阶段。比如某些工控软件里的“握手错误”可能又是完全不同的另一个机制。先搞清楚你说的握手到底是哪一层排查方向才不会跑偏。MCP 的握手失败绝大多数情况是应用层协议没有协商成功跟端口和证书关系不大。2. initialize 从发起到确认一次握手带出的协议骨架MCP 握手在协议规范里就叫initialize整个过程只有三步客户端发initialize请求服务端返回能力声明客户端再发notifications/initialized通知。看起来简单但这一步带出了整个协议的骨架。2.1 客户端发起的 initialize 请求长什么样假设你写了一个智能体需要接入一个本地数据查询服务。握手的第一条消息是这样的{ jsonrpc: 2.0, id: 1, method: initialize, params: { protocolVersion: 2025-06-18, capabilities: { roots: { listChanged: true }, sampling: {} }, clientInfo: { name: my-agent, version: 0.1.0 } } }注意几个细节。jsonrpc固定是2.0MCP 整个消息层构建在 JSON-RPC 2.0 之上这是跨语言实现低门槛的关键。id是请求序号响应必须原样带上客户端靠它把并发请求和响应一一对应。method是initialize这是客户端发出的第一条消息任何其他请求都不能排在它前面。最关键的是protocolVersion。客户端在这里声明自己能够理解的协议版本服务端会把它当作协商基准。目前规范的版本号主要是日期制形如2025-06-18。服务端返回时只能返回自己支持且不比客户端声明版本更新的版本如果双方完全没有交集握手就会失败。capabilities这一段容易理解错。它不是“功能开关”而是“能力声明”——告诉服务端“我这个客户端将来可能会用到哪些能力”。这里声明了roots和sampling含义分别是客户端可以告诉服务端哪些目录和文件可访问以及在某些场景下允许服务端反向调用客户端的 LLM 采样能力。如果客户端不声明这些服务端将来就不会主动使用这是协议层面的自我保护。2.2 服务端返回的响应里藏了多少信息服务端处理完 initialize 请求后返回这样一条消息{ jsonrpc: 2.0, id: 1, result: { protocolVersion: 2025-06-18, capabilities: { tools: { listChanged: true }, resources: { subscribe: true }, prompts: {} }, serverInfo: { name: data-query-server, version: 1.2.0 }, instructions: 建议先调用 list_tables 查看可用表结构某些查询在非工作时间响应较慢。 } }protocolVersion在这里就是协商结果表示“我能接受这个版本后续都按这个版本来”。serverInfo和clientInfo一样只用于日志展示和调试不参与业务逻辑判断。capabilities则是服务端的能力清单我可以提供工具调用、资源读取、提示词模板这三类服务。很多人忽略instructions字段它在规范里是可选的自由文本但实际价值很大。它可以写使用偏好、边界条件、注意事项相当于服务端给客户端的一份“使用手册”。比如某些工具需要先初始化、某些操作有频率限制都可以写在这里。客户端可以把这段文本原样交给模型让模型知道应该怎么跟这个服务协作。2.3 为什么还要补一个 notifications/initialized 通知服务端返回 initialize 响应之后握手还不能算完成。客户端必须再发一条通知{ jsonrpc: 2.0, method: notifications/initialized }这条消息没有id也不需要服务端回复。它的作用是明确告诉服务端“我作为客户端已经完成了初始化现在开始你可以处理我的正式请求了。”为什么需要多这一步因为服务端从收到 initialize 请求到返回响应中间可能还需要做大量准备工作比如配置加载、运行时初始化、连接池建立。如果在准备工作完成之前就收到业务请求服务端难以判断该不该处理。有了这个显式的initialized通知服务端的处理逻辑就清晰了收到通知之前业务请求要么拒绝、要么排队收到之后一律正常处理。这是把“初始化完成”变成了一个明确的状态信号而不是靠猜测在分布式和本地子进程两种模式下都能保持一致性。3. 能力协商才是握手的灵魂tools、resources、prompts 谁能上岗很多人以为握手只是“打个招呼”其实 MCP 握手的核心价值在于能力协商决定了连接建立后双方到底能做什么。3.1 服务端的三类核心能力MCP 服务端最常用的三种能力是tools、resources和prompts它们解决的是不同类型的问题tools可执行的动作比如查询数据、操作文件、控制浏览器。它能被模型动态调用适合“让 AI 做一件事”。resources可读取的具名数据比如文件内容、数据库元数据、当前系统状态。它适合“让 AI 先了解一个上下文”。prompts预设的提示词模板客户端可以按模板引导模型适合把高频任务固化成标准工作流。一个 server 不一定要实现全部三类。典型的浏览器自动化 server比如 Playwright MCP、Chrome DevTools MCP 这类主要暴露tools让 AI 可以点击、输入、导航、抓取页面数据。而一个文件系统类的 server 可能主要暴露resources让 AI 读取指定目录下的文件内容。还有混合型的比如数据库服务器通常既提供读取 schema 的resources又提供执行查询的tools。服务端在 initialize 响应里怎么声明客户端后续就怎么使用。如果 server 没声明tools客户端再怎么请求tools/list也是空列表。这是一个能力驱动设计的模型而不是命令驱动。3.2 客户端能力roots 和 sampling 到底是什么意思服务端有能力客户端也有能力。MCP 规范里最常见的是两个roots客户端把自己的文件目录访问权限告诉服务端。比如智能体跑在你的本地机器上你允许它访问/home/user/projects/my-app这个目录客户端在握手时声明了 roots 能力之后服务端就能通过roots/list获取到这些目录信息从而知道自己在文件系统层面可以做哪些操作。这个设计解决了“AI 运行在某个环境里但它不知道哪些路径是被授权的”这个问题。sampling服务端在特定场景下可以反过来请求客户端调用模型。典型案例是服务端自己不做模型推理但需要对某个文本做提取或摘要时可以发起一个采样请求由客户端调用它绑定的 LLM 返回结果。这个能力在隐私敏感或者需要客户端自有模型上下文时非常有用但实际落地场景相对少多数客户端默认不开启。从架构角度看roots是典型的“授权前置”在握手阶段就把权限边界划清楚避免服务端在运行时反复试探哪些文件可以读、哪些不能。这种前置声明比运行时再问要可靠得多也方便客户端做权限控制界面——比如 IDE 集成时会弹窗让你确认“是否允许此 MCP server 访问当前项目目录”。3.3 为什么协议版本优先于能力协商一个比较容易忽略的细节是能力声明不是独立的它受制于protocolVersion。MCP 规范还在快速演进不同版本对能力字段的命名和语义可能有调整。比如早期版本里某些能力字段的写法在新版本里可能被改名或者调整结构。协商顺序是客户端先声明自己支持的最高版本服务端选择一个它能支持且不超过客户端版本的版本返回。之后双方再按这个版本的解释规则来解析 capabilities。如果客户端和服务端支持的版本没有任何交集按规范应当返回错误而不是继续携带不兼容的能力清单往下走。我见过一些实现为了省事在版本不匹配时强行继续通信结果后续请求到处踩坑排查起来非常难受。版本协商这一步做得规范后面会省很多事。4. 工具调用的完整执行链路从模型选择到结果回填架构拆到这里我们进入标题里的另一部分“执行”。一次 MCP 工具调用的链路比很多人想象的长它包含发现、选择、调用、回填四个阶段。4.1 阶段一tools/list 发现可用工具连接建立并完成握手后客户端通常会先请求一次工具列表{ jsonrpc: 2.0, id: 2, method: tools/list }服务端返回类似这样的结果{ jsonrpc: 2.0, id: 2, result: { tools: [ { name: query_sales, description: 查询指定日期范围的销售数据按渠道和品类返回汇总金额与环比变化。日期格式为 YYYY-MM-DD结束日期不能早于开始日期。, inputSchema: { type: object, properties: { start_date: { type: string, description: 起始日期格式 YYYY-MM-DD }, end_date: { type: string, description: 结束日期格式 YYYY-MM-DD } }, required: [start_date, end_date] } } ] } }这个列表是模型决定“用什么工具”的唯一信息来源。客户端会把它转成模型能理解的函数描述然后模型根据用户的问题和上下文的工具描述决定是否调用、调用哪个、传什么参数。这里有个非常实际的规律description的质量直接决定模型选工具的准确率。MCP 没有给模型额外的暗号通道工具描述就是它了解你工具的说明书。你把description写成“查询销售额”模型大概率不清楚参数怎么填、返回什么样的数据。但写成上面这个例子——包含用途、参数格式、约束边界模型就能很好地做出决策。我给内部工具写描述时有一个标准把它当作给一个聪明但完全没有背景知识的实习生写任务说明。要包含“这个工具做什么”“输入大概长什么样”“有什么边界条件”“输出大概是什么”。实践下来选工具准确率提升非常明显。4.2 阶段二模型生成参数客户端发起 tools/call下一步是客户端把模型决定好的参数封装成tools/call请求{ jsonrpc: 2.0, id: 3, method: tools/call, params: { name: query_sales, arguments: { start_date: 2025-06-01, end_date: 2025-06-30 } } }服务端拿到请求后根据name找到对应的工具处理器校验arguments是否符合inputSchema执行实际逻辑比如查询数据库、读取文件、调用外部 API。这里的参数校验严格程度由各实现决定但规范的inputSchema设计就是希望服务端能按 JSON Schema 做基础校验尤其是required和类型检查。4.3 阶段三执行结果如何返回工具执行完服务端返回结果。这里有两个容易踩坑的细节content数组的格式以及isError字段的语义。{ jsonrpc: 2.0, id: 3, result: { content: [ { type: text, text: 6月销售总额 2,340,000 元环比 12%其中线上渠道占比 58%。 } ], isError: false } }content是一个数组每个元素是一个内容块类型可以是文本、图像、音频等。客户端拿到之后会把它拼回模型的上下文让模型基于工具输出继续组织回答。isError用来区分正常结果和错误结果。即使工具内部抛了异常服务端也应该尽量以正常的 JSON-RPC 响应返回把isError设为true而不是在协议层面制造错误。这样做有一个好处错误信息会以文本形式进入模型上下文模型能“看到”执行失败的真实原因从而自行调整策略——比如换参数重试、换工具、或者坦白地告诉用户失败了。如果服务端直接返回协议层错误模型看不到内部信息整个环节就断了。4.4 并发调用与 id 匹配实际使用中一个模型会话里经常同时需要多个工具的结果比如既要查销售额又要查库存。好的客户端会同时发出多个tools/call请求分别使用不同的id然后根据响应的id把结果回到各调用方。这是 JSON-RPC 2.0 的基础能力MCP 直接继承了这个设计。这里有件小事容易被忽略id不一定非得是递增数字可以是字符串但同一连接内不能重复。如果你自己实现了一个简易客户端用一个全局自增计数器是最稳妥的。我之前见过有实现用时间戳尾部做 id并发一高就撞号结果两个请求的结果互相串了查了整整一天才发现是 id 重复导致的。5. stdio 与 HTTP两种传输模式下的握手与执行差异MCP 的一个优秀设计是把传输层和协议层解耦。同样是 JSON-RPC 2.0 消息既可以通过本地进程的标准输入输出传输也可以通过 HTTP 远程传输。这两种方式对握手和执行链路的影响不小。5.1 stdio 模式本地子进程的天然主场stdio 模式下MCP server 是一个独立进程客户端通过子进程的标准输入写 JSON-RPC 请求从标准输出读响应。Cursor 里配置npx命令启动的 MCP server绝大多数走的是这个模式。这种模式有几个关键点进程生命周期即连接生命周期。server 进程被拉起时连接开始进程退出时连接结束。端到端延迟极低适合本地工具型 server。日志输出位置有硬性约束所有业务日志必须写到stderr而不是stdout。因为stdout被协议占用你往 stdout 里打个console.log客户端解析 JSON-RPC 时直接报错连接彻底崩溃。第一次写 stdio MCP server 的人十有八九栽在日志污染 stdout 这个问题上。WebSocket/HTTP 模式下这个限制就不存在了这也是两种传输模式在实现上最大的差异之一。5.2 HTTP 模式远程 server 的会话粘合方式HTTP 模式规范里叫 Streamable HTTP适合远程部署的 server。客户端通过 POST 请求与 server 通信初始化握手路径是 POST 到 server 的 MCP 端点。协议设计上MCP 本身是无状态的但业务场景往往需要识别“这个请求来自谁”。HTTP 模式用Mcp-Session-Id这个响应头解决会话粘合问题第一次请求通常就是 initialize发出后服务端在响应头里返回Mcp-Session-Id。客户端后续所有请求都要把这个 session id 带上。服务端通过 session id 维护该客户端的状态上下文。远程场景还有鉴权问题。配置文件里那种带 token 的 URL就是这个场景的典型token 作为 Bearer Token 放在请求的 Authorization 头里服务端校验通过后才开始处理 initialize。这也意味着这种带 token 的地址本质上是敏感凭据不应该写死在公开代码里。5.3 传输方式怎么选我的选择标准很简单本地优先、单用户使用、要操作文件系统或本地应用 → 用 stdio配置一条npx命令就行网络配置全免。需要部署到服务器、供多个客户端或远程调用 → 用 HTTP 模式配套鉴权、TLS、日志监控。想要同一个 server 同时支持两种访问方式 → 可以同时实现两个入口内部复用同一个核心逻辑。在架构设计上传输层和协议处理层最好保持独立。这样同一个工具注册表、同一个能力协商逻辑既能通过 stdio 被本地 Cursor 调用也能通过 HTTP 被远程服务接入两边行为一致性有保障。6. 联调 MCP server 时最容易翻车的五个细节协议背后的架构思路不难理解真正让人头疼的往往是联调阶段的细节。这里列几个我踩过、也帮人排查过的坑。6.1 initialized 通知漏发有些实现返回 initialize 响应之后以为握手完成直接开始发业务请求。但服务端可能还在等notifications/initialized。结果就是业务请求被忽略或者直接被拒。排查手段很简单在服务端日志里确认收到 initialized 的时间点再对照业务请求的到达时间。顺序错了问题就出在这里。6.2 协议版本号对不上客户端声明2025-06-18服务端只支持2024-11-05版本协商直接失败。这类问题常见于陈旧镜像、半年前部署的依赖、或者客户端和服务端升级不同步。解决方式是升级依赖或者在做协议降级兼容。但不建议在实现里强行忽略版本号继续通信协议版本不一致意味着字段语义可能已经变化强行通信只会积累更多隐性 bug。6.3 工具描述写得太废模型全程乱选工具描述质量差不算协议错误但它的破坏力比协议错误更大。协议错误至少报错明显描述质量差是模型悄悄用错工具、填错参数下游拿到的结果当然也是错的用户感知却是“AI 变笨了”。所以我在团队里强制要求新工具上线的标准之一就是 description 要通过评审包含边界条件、正面和反面参数示例、常见误用场景。6.4 慢工具把客户端拖到超时MCP 规范没有内置进度上报机制一个耗时 30 秒的工具可能直接让客户端超时。实际处理有几种经验方案把长任务拆成多个短步骤每个步骤单独作为工具暴露。服务端在工具内部尽快返回一个任务 ID再提供状态查询类工具让客户端轮询。做服务端实现时如果工具执行超过了预设时间优先返回中间状态和提示别让调用方一直空等。6.5 本地调试时日志污染 stdoutstdio 模式下往 stdout 打印任何日志都会破坏协议流客户端的解析器立刻报错。这是本地开发最隐蔽的坑因为程序“看起来还在跑”但客户端就是连不上。解决办法很粗暴调试期间关闭所有 stdout 输出日志统一走 stderr。检查顺序可以这样来先确认进程有没有起来再看 stdout 是否干净再确认握手消息是否正常最后看工具列表有没有返回。这条链路走一遍绝大多数连接问题都能定位。我个人现在接入一个新 MCP server 时流程已经固化成固定的三步先看它支持什么传输模式再用本地 stdio 拉起并打印前几条消息确认握手、initialized 通知、tools/list 三件事都通了才会进入业务对接。这套流程帮我省掉了大量来回试错的时间。回到标题那句话MCP 的架构设计从握手到执行本质上就是“先协商再调用”这个朴素原则在 AI 工具生态里的具体落地。把这个原则理解透了你再看任何 MCP server 的实现都不会觉得晦涩。
返回列表