☰
MCP协议全解析:从握手流程到LangGraph多Server编排实战
2026/10/8 4:37:24 网站建设 项目流程

MCP(Model Context Protocol,模型上下文协议)已经不只是在 AI 圈子里流行,我最近搜技术关键词时,几乎每个细分领域都在冒 MCP 相关词:IDA MCP、x64dbg MCP、Unreal Engine 5.8 MCP、Altium Designer 的 AI 接口 MCP,甚至连项目管理和地图服务都在做 MCP 接入。这个趋势背后有一个很核心的问题:MCP 并不是“把函数丢给大模型”这么简单,它有一套完整的协议握手、能力协商和工具调用语义,而这正是从“能跑通 demo”到“能在 LangGraph 里稳定调度多个 MCP Server”之间真正的分水岭。这篇文章记录我实际跑通全链路的经验和踩坑结论,从协议层握手拆解讲到 LangGraph 多 Server 调用,适合三类读者:要自己写 MCP Server 的、想在 LangGraph 中编排多个 MCP 工具的、以及被各种 MCP 热词吸引想搞清楚原理的。

1. 先把协议模型说清楚:为什么 MCP 像 AI 世界的 USB-C

第一次接触 MCP 的人容易把它跟函数调用插件混为一谈,但我觉得最贴近的类比是 USB-C:它不是一个具体的功能插头,而是定义了一套统一的接口标准和供电协议,让显示器、硬盘、手机、扩展坞都能通过同一根线对接。MCP 做的就是 AI 应用侧的那根线——它让 Host(Claude Desktop、Cherry Studio、Cursor、自研 Agent 应用)能通过同一套协议去对接文件系统、数据库、调试器、设计工具、行情数据源。

1.1 三个角色一台戏

要理解 MCP,先分清三个角色。Host 是你运行的 AI 应用或 Agent 运行时,它是发起对话的一方;Host 里会内嵌一个 MCP Client,负责与外部服务建立连接并收发 JSON-RPC 消息;MCP Server 则是真正提供能力的一方,它对外暴露三类原语:工具(Tools)、资源(Resources)和提示词模板(Prompts)。

很多人以为大模型直接连接 MCP Server,其实不是:模型只负责在对话里生成“想调用某个工具的意图”,Host 里的 Client 解析这个意图,去调用 Server 上的工具,再把执行结果回填给模型,模型继续推理。理解这个三角色关系,后面遇到“为什么模型没调用我注册的工具”这类问题时才更容易定位。比如在 LangGraph 里,模型绑定工具并生成 tool_calls,真正执行工具的是运行时,而不是模型本身,这个认知偏差往往是排障时绕弯路的根源。

1.2 传输层三兄弟

MCP 规范迭代到现在,传输层大致收敛为三条路:stdio、Streamable HTTP,以及已经标记废弃但仍大量存在的 SSE。

传输层适用场景特点典型例子
stdio本地子进程标准输入输出传 JSON-RPC,启动快、不暴露网络端口,进程生命周期跟宿主绑定npx 拉起的文件系统 Server
Streamable HTTP远程服务双向流式 HTTP,支持 OAuth、多客户端会话,是当前 HTTP 传输的标准形态云端数据服务、团队共享 MCP
SSE(HTTP+SSE)老项目遗留单向 Server 推送,客户端通过 POST 发消息,官方已不推荐新项目使用早期远程 MCP 示例代码

选型上我的习惯是:本地工具、需要直接访问文件或调试器进程的,用 stdio,因为它跟着宿主进程走,不暴露端口;跨进程、跨机器的服务用 Streamable HTTP,它能做 OAuth、维持会话状态;遇到老项目还挂着 SSE 的,尽快迁移。别小看这个选择,后面在 LangGraph 里同时挂 stdio 和远程 HTTP 时,两者生命周期完全不同,处理不当就会出现各种幽灵连接。

1.3 为什么不建议跳过协议层

如果你只是把别人写好的 MCP Server 接到 Cherry Studio 里用,确实可以不知道握手细节。但要做两件稍微进阶的事——自己写 Server、或者在 LangGraph 里做多 Server 编排——就必须把握手流程吃透。我排障时见过最典型的问题:某自研 SDK 在 initialize 还没有返回时就发了 tools/list,被服务端直接拒绝;另一个问题是客户端声明不了 roots 能力,导致要扫描目录的 Server 一直报“无根目录”。这些如果不理解协议生命周期,光看日志只能瞎猜。

2. 握手全流程拆解:从 initialize 到 initialized 之间发生了什么

MCP 的地基是 JSON-RPC 2.0,它把所有消息分成三类。第一类是请求(Request):必须带 id、method 和 params,且必须收到响应,比如 initialize、tools/list;第二类是响应(Response):带与请求对应的 id,成功时是 result,失败时是 error 结构;第三类是通知(Notification):没有 id,不需要对方响应,比如 notifications/initialized。这个“通知不需要回执”的设计很关键:握手阶段的 initialized 就是个通知,客户端发出去就算完成流程,不需要等服务端确认,少做这一步会导致服务端认为连接还没就绪。

2.1 JSON-RPC 2.0 的三种消息形态

错误码这一块也别忽略。对齐 JSON-RPC 标准,-32700 是解析错误,-32600 是请求本身无效,-32602 参数校验失败,-32603 内部错误;MCP 还在保留区间里扩展了资源、工具相关的错误码,具体值以服务端实现为准。实际调试时,很多“工具调用失败”其实是参数类型没对上——JSON Schema 里写 integer,你传了 number,SDK 的校验器不会帮你转,直接报参数校验错误。

在 Streamable HTTP 传输下,客户端和服务端还可以通过 SSE 帧做流式响应,长任务可以边执行边推结果。底层消息格式不变,变化的只是传输方式。这也就是为什么热词里会出现“使用 MCP 工具流式输出内容到文件”:Host 侧收到分块的 JSON-RPC 响应后,会边收边写盘,而不是等全部内容完成后再一次性落地。

2.2 initialize 请求与能力协商逐字段看

握手活动从客户端发出 initialize 请求开始:

{ "jsonrpc": "2.0", "id": 0, "method": "initialize", "params": { "protocolVersion": "2025-06-18", "capabilities": { "roots": { "listChanged": true }, "sampling": {} }, "clientInfo": { "name": "my-client", "version": "1.0.0" } } }

服务端的响应大致长这样:

{ "jsonrpc": "2.0", "id": 0, "result": { "protocolVersion": "2025-06-18", "capabilities": { "tools": { "listChanged": true }, "resources": { "subscribe": true, "listChanged": true }, "prompts": { "listChanged": true } }, "serverInfo": { "name": "demo-server", "version": "0.1.0" } } }

请求里最值得关注的是 protocolVersion 和 capabilities。protocolVersion 客户端会填它支持的版本,服务端返回时选择双方都能接受的最新版本;目前主流实现里能看到 2024-11-05、2025-03-26、2025-06-18 这串版本号。如果完全不兼容,服务端在 error.data 里推荐可用的版本列表,客户端据此重试。

capabilities 则是能力协商的核心:客户端声明它支持 roots(允许服务端读取宿主指定的根目录)和 sampling(允许服务端反过来向模型请求采样补全),服务端声明自己支持 tools、resources、prompts、logging、completion 等。我建议每个自研 SDK 都在握手响应里打印 serverCapabilities,后面很多“为什么某方法不能调”都能在上面找到答案。比如服务端没声明 resources,客户端却去调 resources/read,服务端必然报 method not found。

2.3 握手完成后,调用节奏是怎样的

握手不是收到响应就算完。规范的定义是:客户端发送 initialize 请求并收到成功响应后,还必须再发一个 notifications/initialized 通知,这之后才进入正常的业务消息阶段。注意时间顺序——如果客户端在 initialized 通知之前就发 tools/list,有些严格实现会直接拒绝;我在自行实现一个轻量客户端时踩过这个顺序的坑。

用伪代码看这个流程更清晰:

# 握手阶段伪代码 await request("initialize", {...}) response = await wait_response() await notify("notifications/initialized") # 业务阶段 tools = await request("tools/list") for tool in tools: result = await request("tools/call", {"name": tool.name, "arguments": args})

tools/list 返回值是一组工具描述,每个工具包含 name、description 和 inputSchema(JSON Schema 格式)。模型看到的就是这份清单,所以 description 写得越具体,模型选择准确率越高。tools/call 则是真正执行:传 name 和 arguments,得到的响应包含 content 数组和 isError 标志。注意 isError 为 true 也不等于协议错误,它是服务端在业务层面的失败,比如文件不存在,这类失败在 LangGraph 里需要单独处理,后面第 5 章会讲。

2.4 流式、取消与会话状态:协议里的高级开关

Streamable HTTP 传输层支持把一次 tools/call 的结果分块推送,这就是“MCP 工具流式输出”的底层能力:长文档生成、文件写出、日志解析这类任务,模型不需要等全部结果都完成才看到内容,Host 侧可以边收边写盘边渲染。实现上有两种协作方式:一种是服务端把结果用 SSE 帧逐步发送,另一种是靠 progress token 上报异步任务进度。

比较容易被忽略的是取消。MCP 支持 cancel 通知,客户端在调用超时或用户主动中断时发出。我在 LangGraph 里跑长任务时,会给远程 HTTP Server 设置合理的 request_timeout,并编写取消逻辑,不然一个慢工具会把整条 Agent 链路拖死。高级一点的还会用到 session id 维持会话状态,多个客户端进程共享同一个远程 Server 会话的时候尤其重要。

3. 三种原语的分工:Tools 是手,Resources 是眼睛,Prompts 是剧本

MCP 提供的不是单一的函数调用通道,而是三种能力原语。我一直用三句话向同事解释:Tools 是模型可以主动执行的操作,作用是改变系统状态,比如写文件、发请求、执行调试命令;Resources 是只读数据源,模型预先读取或按需拉取上下文,比如日志内容、代码片段、行情数据,它回答的是“这个系统里有什么”;Prompts 是可复用的提示词模板,本质上是把一套针对特定任务的提问框架暴露给 Host,回答的是“这个系统想让你怎么问”。

3.1 一张表分清三种能力

原语方向类比切入方式典型用途
Tools请求-响应手tools/list、tools/call执行写操作、命令、状态变更
Resources请求-响应、订阅眼睛resources/read、resources/list、resources/subscribe读取数据、提供上下文
Prompts请求-响应剧本prompts/list、prompts/get复用会话开场和任务模板

要注意的是,很多初学 MCP 的人只把 Tools 当全部,这其实丢了一半以上能力。面向只读场景,比如代码阅读、数据分析、文档摘要,Resources 的成本远低于 Tools:一个是把数据推给模型,一个是让模型反复试错式地调用工具拿数据。前者稳定可控,后者既费 token 又不可预测。

3.2 用文件系统 Server 把三种原语串起来理解

拿文件系统场景举例。一个完整的文件系统 MCP Server,通常会这样分配能力:文件内容、目录结构用 Resources 暴露,URI 形如 file:///path/to/file;移动、复制、删除、写入这类会改变磁盘状态的操作放在 Tools 里;Prompts 则定义类似“总结这个项目的 README 和目录结构”这种模板。模型在对话里需要大文件时,Host 先 resources/read 把内容注入上下文;需要批量重命名时,模型才发起 tools/call。

这种区分不是随意设计的,它直接服务于安全边界。一个只读代码分析 Agent 只需要把 Resources 挂给它,不暴露任何写操作工具,模型再聪明也动不了磁盘;而如果你的 Agent 必须做文件整理,那就只暴露相关几个工具,不要把所有工具都放开。权限最小化应该从协议原语这一层就开始做,而不是在 Agent 代码里靠提示词约束。

3.3 Resources 为什么总被低估,以及实战要点

mcp resource 实战现在成了热词,说明大家已经开始不满足于只会 tools。实战中我总结出几个要点:

  • 第一,Resource URI 是寻址核心,file://、memory://、log:// 等 scheme 表示不同来源,服务端用 templates 声明一类资源的模式,客户端可以预先展示给用户选择,比让模型猜路径高效得多。
  • 第二,调用 resources/read 拿到的是 content 数组,text 类型最常见;现代规范里还支持返回图片等类型的资源内容,适合文档类 AI 产品。
  • 第三,如果数据会变化,关注 capabilities.resources.subscribe。服务端一旦声明支持订阅,客户端可以注册监听,在数据变更时通过通知被动更新,免去轮询开销。

还有一种实用姿势:把大 schema、示例样本、术语表全部以 resources 暴露,模型先 read 再决定后续动作,能显著减少工具调用轮次。我在做数据中台 Agent 时就把表结构清单和枚举字典做成 resource 模板,实测下来上下文更稳、调用次数明显下降。

4. 手把手搭一个文件查询 MCP Server,并用 Inspector 验证握手

官方 SDK 目前最活跃的是 TypeScript 和 Python 两个方向。我的建议很直接:如果你要做通用工具、数据查询类的 Server,选 TypeScript,因为官方示例多、类型定义跟规范同步快,inputSchema 可以直接用 Zod 推理出来,省掉手写 JSON Schema 的很多坑;如果核心逻辑在 Python 生态里,比如 pandas 数据处理、torch 模型推理,那就用 Python SDK,异步支持也不错。两者都能跑 stdio 和 Streamable HTTP。语言本身不是关键,关键是别在 Server 里塞一堆与协议无关的重量级框架。

4.1 环境准备

新建一个项目目录,装最小依赖:

npm init -y npm i @modelcontextprotocol/sdk zod typescript tsx

4.2 一个最小可用的 stdio Server

下面这个工具只做一件事:读取文本文件开头若干行,适合快速预览大文件。虽然是 demo,但握手逻辑、错误处理、返回格式都是生产级的写法:

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: "demo-file-head", version: "0.1.0", }); server.registerTool( "read_file_head", { description: "读取文本文件开头若干行,适合快速预览大文件", inputSchema: { path: z.string().describe("文件绝对路径"), lines: z.number().optional().describe("读取行数,默认 20"), }, }, async ({ path, lines }) => { try { const fs = await import("node:fs"); const data = fs.readFileSync(path, "utf-8"); const head = data.split("\n").slice(0, lines ?? 20).join("\n"); return { content: [{ type: "text", text: head }] }; } catch (e) { return { content: [{ type: "text", text: `发生错误: ${e.message}` }], isError: true, }; } } ); const transport = new StdioServerTransport(); await server.connect(transport);

新版 SDK 里 handler 返回 content 数组,老版本有些可以随便返回字符串,但建议统一用 content 数组格式,保证跨版本行为一致。工具执行失败时一定要给 isError: true,否则宿主会以为工具成功,模型会被错误结果带偏。

4.3 用 MCP Inspector 可视化验证握手

写完 Server 别急着接 LangGraph,先用官方调试器 Inspector 过一遍。启动方式很简单:

npx @modelcontextprotocol/inspector npx tsx server.ts

浏览器打开后会以 stdio 模式启动你的 Server,并在页面上自动完成一次完整握手。Inspector 最大的价值是让你看到三件事:一、initialize 的参数和响应是否正确;二、tools/list 返回的 Schema 是否完善;三、调一个工具后返回的 content 结构会不会被 Host 正常解析。我曾在 Inspector 里发现 inputSchema 里 lines 字段用了 number,但调用时工具收到的值里有小数,最后是靠给 handler 内部做一次整数处理才压掉这个问题。

4.4 stdio 模式下的三个经典坑

  • 坑一:console.log 是毒药。stdio 传输的本质是标准输入输出各成一个 JSON-RPC 消息管道,你一旦在服务端代码里随便 console.log,日志就会混进 stdout,宿主的 JSON 解析器直接挂掉。所有调试日志必须走 console.error 或独立日志文件。
  • 坑二:子进程清理。Host 退出时,npx 起的 Server 进程不一定跟着退出。我在本地跑了一周多,发现好几条残留 node 进程。凡是提供 stdio Server,都要监听 SIGINT/SIGTERM 并做优雅关闭。
  • 坑三:npx 首次启动慢。因为要现场下载依赖,首次握手可能要几十秒,很多调试误判成超时。建议 CI 或演示环境先把包装好,或者直接用 node 指向已安装的入口。

5. LangGraph 多 Server 调用:合并工具只是开始,真正的难点在于管理

LangGraph 对我来说不只是 LangChain 的升级版,它把 Agent 变成了一个有状态、可控制执行流的图。当你要调用的工具来自好几个 MCP Server 时,最关键的问题不是“能不能合并工具列表”,而是“如何让不同上下文使用正确的工具子集”。用一颗 ReAct Agent 挂几十个工具,模型选择准确率会明显下降,而且排障极难。有了 LangGraph,你可以按职能拆成多个子 Agent:一个挂代码库或调试器 MCP,一个挂文件系统 MCP,一个挂业务数据 MCP,再由 supervisor 节点统一调度。这也决定了后面连接管理上的复杂度。

5.1 用 MultiServerMCPClient 一把拉起多个 Server

langchain-mcp-adapters 提供了 MultiServerMCPClient,可以把多个 MCP Server 包装成 LangChain 的 BaseTool。下面这个例子同时拉起一个本地 stdio 文件系统 Server 和一个远程 Streamable HTTP Server:

import asyncio from langchain_mcp_adapters.client import MultiServerMCPClient from langchain_openai import ChatOpenAI from langgraph.prebuilt import create_react_agent async def main(): client = MultiServerMCPClient( { "fs": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"], "transport": "stdio", }, "math": { "url": "http://localhost:8000/mcp", "transport": "streamable_http", }, } ) tools = await client.get_tools() model = ChatOpenAI(model="gpt-4o") # 姿势 A:所有工具交给一个 ReAct Agent,适合工具少、链路短的场景 agent = create_react_agent(model, tools) result = await agent.ainvoke( {"messages": [{"role": "user", "content": "统计 /tmp 目录下的文件数量"}]} ) print(result["messages"][-1].content) await client.aclose() asyncio.run(main())

get_tools() 的原理是:并行建立各 Server 的 MCP 连接,完成 initialize 握手,拿到工具清单,再包装成 LangChain 的 BaseTool。stdio 型以 command + args 指定,远程型给 url 并指定 transport。连接建立完成后返回的是一份合并清单。

5.2 在 LangGraph 节点里绑定工具的实践

拿到 tools 后,进入 LangGraph 最常用的两条路。第一条是用 prebuilt 的 create_react_agent 快速起一个 ReAct Agent,模型配置好之后自动处理 tool_calls 循环;第二条是自定义 StateGraph,在某个节点里把与当前任务相关的工具通过 bind_tools 绑给模型,手工执行循环。工具少、链路短用第一条;需要严格控制状态的用第二条。

我自己的习惯是:即使只用 create_react_agent,也会把工具按 Server 分组后分别传给不同的子 Agent,而不是一股脑塞进一个。这样每个子 Agent 看到的工具数量少,description 可以写得更贴近该子 Agent 的任务语义,整体调用准确率会明显上来:

fs_agent = create_react_agent(model, fs_tools) data_agent = create_react_agent(model, analysis_tools)

如果希望图节点里某个工具只执行一次并把结果写进 State,可以把这个工具当成普通可调用对象,在 node 函数里显式调用。这种方式在做“固定先用 MCP 拉数据,再交给模型总结”的编排时最可靠,因为它跳过了模型选择工具的不确定性。

5.3 五个真实踩过的坑

  • 坑一:工具名冲突。两个 Server 都有 search 或 get_order 是常态。adapter 可能会提供命名配置,但我在实际项目里更常用的是 get_tools 之后统一 rename,保证最终进图的名字全局唯一,否则 ReAct 的 tool_calls 会落到错误的 Server。
  • 坑二:isError 不等于异常。远程 MCP Server 调用了但业务失败时,返回值可能带着错误标志,如果被当成普通文本回填给模型,模型会误以为操作成功。要在工具调用后检查结果里的错误标记,或者在外层包装一个复合工具,失败时返回清晰的修复建议,让模型可以自己纠错。
  • 坑三:鉴权上下文不会自动传递。远程 MCP Server 大多需要 OAuth 或自定义 token,比如 Codex 接 Figma MCP 时那种授权流程、Dify 浏览器 MCP 里的登录态。这些内容属于调用方上下文,LangGraph 不会自动带上。正确做法是在外部完成授权,拿到短时凭证再塞进连接的 headers 或环境变量,不要让每个工具请求去走一次耗时授权。
  • 坑四:stdio 子进程生命周期。每拉起一个 stdio Server 就是一条子进程,如果每次 Agent 运行都 new 一个 client,又不显式 aclose(),进程会被本地 Host 拖住不放。我后来统一在应用退出时批量清理,并把 MultiServerMCPClient 作为长生命周期对象复用。
  • 坑五:超时和流式处理。远程 HTTP Server 的毫秒级抖动在单次调用里无所谓,但在多步 Agent 里会被指数级放大。一定要给模型和工具调用都设置 timeout,并对支持流式的 Server 开启流式接收,避免长结果全部堆在内存里。

以上五个坑我都在同一周内踩过,有些甚至从日志里搜不到原因,列表放在这,你直接照着避。

6. 从热词看 MCP 的渗透路线:调试器、游戏引擎与业务系统的共同逻辑

把 IDA MCP、x64dbg MCP、UE5.8 MCP、Altium Designer AI 接口 MCP、同花顺 MCP、百度地图 MCP、禅道 MCP 放在一起看,你能看到一个很清晰的分层:底层基础设施先接入,然后是专业工具,再然后是业务系统。逆向工具链把反汇编文本和调试器操作暴露给模型,让 LLM 参与恶意样本分析;游戏引擎把编辑器自动化能力暴露出来,模型可以操作场景;EDA 软件把规则检查和布局动作接进去;行情工具、地图、项目管理则负责提供实时数据和业务动作。这套逻辑其实非常统一——凡是需要“让模型在真实系统里读数据、做操作”的地方,都在用 MCP 抹平对接成本。

6.1 这类高权限 Server 的安全边界

但要注意,MCP 的方便是拿权限换的。一个能控制调试器、编辑工程文件甚至修改 PCB 布局规则的 Server,本质上就是给模型开了一个系统级入口。我个人的安全底线是这样:

  • 来源可信。只接官方或信誉良好的 Server,第三方的先读源码再决定要不要运行,尤其是 stdio 型 Server,因为它在本地直接起子进程。
  • 权限收敛。Server 内部尽量用白名单目录或命令;在 Agent 层,Tools 与 Resources 分开,读工具永远只挂读工具,写工具单独评估后再放开。
  • 传输安全。远程 Server 必须走 HTTPS 和 OAuth 或带过期时间的凭证,不要明文放 token。
  • 行为审计。对每个工具调用记录入参、出参摘要和调用方 Agent,出现异常回滚才有依据。

我把这四条写在团队 MCP 使用规范的第一页。对逆向、调试器这类高危能力,我只会放在隔离运行的沙箱环境里,连宿主机文件系统都不共享。

6.2 我的选型建议与一句底线

从协议握手到 LangGraph 多 Server 调用这一条链路走完,我的最终结论其实很简单:MCP 的能力并不稀缺,稀缺的是你对连接的理解和控制。能用官方 SDK 就用官方 SDK,能收敛权限就收敛权限,能走 HTTPS 和 OAuth 就不要明文裸奔;编排上,能拆 Agent 就不要把工具全部塞给一个模型,能长生命周期复用连接就不要反复建连。

最后一个个人体会是:MCP 现在还处在类似早期 REST 的阶段,协议还没有完全冻结,未来版本大概率还会演进。与其背规范全文,不如真正亲手把一个 Server 从握手跑到多 Server 编排,踩过一次坑,胜过读一百篇文档。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询