
MCPModel 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 应用侧的那根线——它让 HostClaude 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 拉起的文件系统 ServerStreamable HTTP远程服务双向流式 HTTP支持 OAuth、多客户端会话是当前 HTTP 传输的标准形态云端数据服务、团队共享 MCPSSEHTTPSSE老项目遗留单向 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你传了 numberSDK 的校验器不会帮你转直接报参数校验错误。在 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 和 inputSchemaJSON 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 tsx4.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 一把拉起多个 Serverlangchain-mcp-adapters 提供了 MultiServerMCPClient可以把多个 MCP Server 包装成 LangChain 的 BaseTool。下面这个例子同时拉起一个本地 stdio 文件系统 Server 和一个远程 Streamable HTTP Serverimport 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(modelgpt-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 编排踩过一次坑胜过读一百篇文档。