☰
MCP协议与LangGraph实践:从握手到多Server调用的完整指南
2026/10/8 21:01:22 网站建设 项目流程

这两年“MCP”这个词在AI工程圈子里快被聊烂了,但它到底是什么、为什么能解决工具调用乱象、以及如何把它真正塞进Agent工作流里,很多人仍然是一知半解。我自己从早期给Claude写自定义工具适配器,到后来把项目全面切到Model Context Protocol,再到用LangGraph把多个MCP Server串成一条完整链路,中间踩了不少坑。这篇分享就围绕“从协议握手到LangGraph多Server调用”这条主线,把MCP的通信机制、传输层选型、多Server接入实践经验一次性讲透,适合正在做AI Agent落地、想摆脱重复工具适配的开发者参考。

所谓的“MCP”,简单说就是给AI模型和外挂工具之间做了一套标准接口。以前每接一个数据源或工具,都要写一套私有协议,AI应用和工具之间全是密密麻麻的胶水代码。而MCP定义了client和server之间的JSON-RPC通信规范,让“AI应用”和“工具”解耦,这相当于给AI工具生态定了一个USB-C标准,接一次线,到处能用。这篇文章的核心就是想让你在读完以后,能自己搭建一个MCP Server,并且理解如何在LangGraph里同时调度多个Server,而不至于被各种隐性问题卡住。

1. MCP 协议基础与握手流程拆解

1.1 为什么需要MCP:模型工具调用的“USB-C时刻”

我们先回想一下没有MCP的日子。你要做一个能查数据库的Agent,就得写一个Python函数,内部建立数据库连接,再手动把查询结果拼成prompt返回给模型。你要再接一个GitHub工具,又得写一套OAuth流程和API调用。这样的集成方式不仅重复,而且每个工具的接口风格都不一样,有的返回JSON,有的返回XML,有的还需要分页处理。时间久了,光维护这些“胶水代码”就让人头大。

MCP把问题拆成了三层:Host(宿主应用,比如Claude Desktop、LangGraph App)、Client(协议客户端,负责和Server建立会话)、Server(暴露工具和数据源的服务进程)。所有能力都通过统一的三种原语暴露:Tools(可执行的函数调用)、Resources(可读取的数据资源)、Prompts(可复用的提示模板)。你只需让Client和Server完成一次标准握手,之后所有工具调用都走同一套JSON-RPC报文。这种设计最大的好处是“连接一次,处处可用”,换哪个AI应用都行,换哪个LLM也能用。

我在实际工程里感受很深:以前接一个工具要画两三天,现在只要别人给我一个MCP Server的地址或者命令,我把它写进配置,十分钟内就能在Agent里调用。这种标准化带来的成本削减,才是MCP真正让人兴奋的地方。

1.2 一次完整握手的报文级拆解

MCP的握手不是简单的TCP三次握手,它是在传输层之上做一次“能力协商”。整个过程基于JSON-RPC 2.0,请求、响应、通知三种消息类型都有严格的格式约束。我贴一段最核心的握手报文,你们感受一下。

Client发送initialize请求:

{ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2024-11-05", "capabilities": { "roots": { "listChanged": true }, "sampling": {} }, "clientInfo": { "name": "my-agent", "version": "1.0.0" } } }

Server返回响应:

{ "jsonrpc": "2.0", "id": 1, "result": { "protocolVersion": "2024-11-05", "capabilities": { "tools": { "listChanged": true }, "resources": { "subscribe": true, "listChanged": true } }, "serverInfo": { "name": "my-mcp-server", "version": "0.2.0" } } }

之后Client会再发一条notifications/initialized通知,到这里才算是正式进入可通信状态。

很多人容易忽略一个细节:初始化参数里有个protocolVersion,双方会协商一个共同支持的协议版本。比如Client声明支持2024-11-05,Server端实际支持的是更早的版本,那Server会在响应里回一个自己最高支持的版本,后续请求都按这个版本解析。如果两边的版本完全不交叉,官方建议Server返回一个兼容旧版的版本号,但对于某些严格校验的SDK来说,协议版本不匹配会直接导致连接失败,这点在排查问题时尤其重要。

1.3 能力协商与初始化超时

握手的关键产出不是“连接建立”,而是“能力清单”。MCP的capabilities字段明确告诉对方“我能做什么”。在Client侧,可以声明roots(允许访问的目录根)、sampling(模型采样);在Server侧,更常见的是tools、resources、prompts和logging。我最初踩过一个坑:写了个自定义Server,明明定义了工具,却忘了在capabilities里声明tools,结果客户端怎么都拉不到工具列表。后来养成习惯,每次写Server首先检查capabilities是否完整。

初始化超时也是高频问题。MCP的SDK里一般都有默认超时配置,比如Python SDK的initialize默认超时是10秒。如果Server启动很慢(比如要加载模型、初始化数据库连接),Client可能就因为超时直接断开。我建议在Server的main函数里把耗时初始化操作放到异步任务中,保证握手响应能秒回,再在后台慢慢准备具体依赖。

还有一点是关于生命周期:MCP会话不是无限期的,Client可以随时发送shutdown请求,Server回复后两边再彻底关闭。一些长任务则依赖后续的流式消息继续传输,会话状态在服务端会保留。理解这个生命周期模型,对定位“调用到一半连接断了”之类的问题很有帮助。

2. 传输层与 Server 配置实操

2.1 stdio、SSE、Streamable HTTP 三选一

MCP的传输方式不是唯一的,目前主流有三种:stdio、SSE、Streamable HTTP。我直接给一张对比表,方便你按场景快速做决策。

传输方式通信方向适用场景优点缺点
stdio双向,通过标准输入输出本地子进程,单机工具零网络依赖,权限隔离好无法远程调用,进程生命周期难管理
SSE服务端向客户端单向推送事件远程服务,老式浏览器实现简单,兼容性好客户端请求受限,流式交互较弱
Streamable HTTP双向,支持流式响应生产级远程服务,长时间任务支持POST/GET双向,可流式返回需要处理鉴权,部署配置复杂

我的选择习惯是:本地调试、开发环境用stdio,把MCP Server作为子进程启动,简单直接还能看到忠实日志。等到上线,尤其是要跨机器调用时,果断换成Streamable HTTP。现在官方SDK已经把它定为推荐传输方案,老旧的SSE模式能不用就不用。

2.2 最小可运行的 MCP Server 配置

我们不讲空理论,直接写一个能跑的MCP Server。以Python官方SDK为例,实现一个“读取指定文件内容”的工具。先装SDK,然后创建server.py。

import asyncio from mcp.server.models import InitializationOptions import mcp.server.stdio from mcp.server import NotificationOptions, Server from mcp.types import Tool, TextContent server = Server("file-reader") @server.list_tools() async def list_tools() -> list[Tool]: return [ Tool( name="read_file", description="读取指定路径的文件内容", inputSchema={ "type": "object", "properties": { "path": {"type": "string"} }, "required": ["path"] } ) ] @server.call_tool() async def call_tool(name: str, arguments: dict) -> list[TextContent]: if name == "read_file": path = arguments.get("path", "") try: with open(path, "r", encoding="utf-8") as f: content = f.read() return [TextContent(type="text", text=content)] except Exception as e: return [TextContent(type="text", text=f"error: {str(e)}")] raise ValueError(f"未知工具: {name}") async def main(): async with mcp.server.stdio.stdio_server() as (read_stream, write_stream): await server.run( read_stream, write_stream, InitializationOptions( server_name="file-reader", server_version="0.1.0", capabilities=server.get_capabilities( notification_options=NotificationOptions(), experimental_capabilities={}, ), ), ) if __name__ == "__main__": asyncio.run(main())

如果你用的是Claude Desktop,可以直接在配置文件里加一个mcpServers条目,把启动命令填进去。在LangGraph里接入时,则通过MultiServerMCPClient来加载,这个我们在后面细讲。

2.3 多Server的命名空间与工具冲突

当你有多个Server时,最头大的问题不是启动多个进程,而是工具名冲突。比如GitHub MCP里有个工具叫list_issues,你自己的工单系统MCP也有一个同名工具。如果一股脑把所有工具直接注册到Agent,模型可能就稀里糊涂调错了。

解决这类冲突的标准做法是给每个Server加命名空间前缀。很多MCP Client的封装都支持这个能力,比如MultiServerMCPClient可以给每个server指定transport参数,并把返回工具按server名/工具名的方式重命名。你看下面这个加载代码:

from langchain_mcp_adapters.client import MultiServerMCPClient client = MultiServerMCPClient( { "github": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-github"], "transport": "stdio", }, "filesystem": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"], "transport": "stdio", } } ) tools = await client.get_tools()

在这个封装里,工具名会变成github.list_issues和filesystem.read_file,这样LangGraph里的ToolNode就不会因为同名工具而混淆。如果你用的是自己的SDK,一定要在通信层加一层工具名映射,不要心存侥幸。

3. LangGraph 接入多 Server 的完整实践

3.1 LangGraph 在 MCP 场景中的定位

LangGraph是LangChain生态里的图编排框架,它的核心思路是让你用“节点 + 边”的方式定义Agent的执行流程。那为什么多Server场景适合用LangGraph?因为MCP只解决了“工具协议”问题,但它没有解决“工具调度”问题。如果只是把几十个MCP工具一股脑塞给模型,模型往往会陷入选择困难,或者按错误顺序调用工具。用LangGraph,你可以显式规定:先调用哪个Server的工具,拿到结果后再进哪个节点,甚至可以根据条件动态选择分支。

我在项目里就是把MCP Server当成“技能包”,LangGraph当成“大脑调度器”。大脑决定什么时候用哪个技能包,技能包本身则是一堆MCP工具。这样既保留了MCP的即插即用优势,又保证了流程的可控性和可观测性。

3.2 把 MCP Server 接入 LangGraph 流程

LangGraph本身并没有“MCP原生支持”,但LangChain官方提供了一个适配器包langchain-mcp-adapters,它能把MCP工具转换成LangChain的StructuredTool,然后塞进LangGraph的ToolNode。

下面是一个完整的示例:

from langchain_mcp_adapters.client import MultiServerMCPClient from langchain_openai import ChatOpenAI from langgraph.prebuilt import create_react_agent from langgraph.checkpoint.memory import MemorySaver client = MultiServerMCPClient( { "github": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-github"], "transport": "stdio", }, "db": { "url": "https://your-server.com/mcp/db", "transport": "streamable_http", "headers": {"Authorization": "Bearer xxx"}, } } ) tools = await client.get_tools() model = ChatOpenAI(model="gpt-4o", temperature=0) graph = create_react_agent(model, tools, checkpointer=MemorySaver())

一个典型的ReAct Agent就搭好了。这里的create_react_agent内部会自动生成ToolNode节点,模型会根据任务调用带前缀的工具名。如果你需要手动控制流程,可以自己定义节点来调用某个具体工具。

3.3 多 Server 调用的架构设计

多Server接入不仅是个技术操作,更是个架构决策。我概括为两种模式:

第一种是“合并模式”,把不同Server的工具全部丢进同一个ToolNode。适合工具数量少、工具间无明显依赖关系的场景,比如查天气MCP加一个做数学计算的MCP。优点是实现快、模型自由度高,缺点是一旦Server多,工具列表就非常长,影响模型决策速度。

第二种是“分治模式”,每个Server对应一个独立的ToolNode,甚至每个节点只允许调用某一类工具。比如GitHub相关操作单独一个节点,数据库操作单独一个节点,中间由路由节点控制跳转。适合工具数量大、业务流程固定的场景,比如企业内部工单处理系统。

我实际做一个Repo分析Agent的时候,就用了分治模式。三个Server分别是:GitHub MCP(拉取仓库信息)、本地文件系统MCP(读取项目文件)、SQL MCP(查询历史数据)。LangGraph的流程是:先用GitHub工具列出Issue,再根据Issue中的路径用文件系统工具读取代码,最后把结果写入SQL。这种模式下,模型不需要在几十个工具里挑来挑去,每个节点只看到相关的几个工具,准确率高了很多。

3.4 流式输出与状态管理

多Server调用往往意味着长时间的多步骤任务,流式输出和状态管理就非常重要了。LangGraph提供了astream_events方法,可以实时推送每个节点的事件,包括工具开始调用、工具返回结果、模型生成Token等。我在写交互式Agent时,就用它把工具调用的中间结果流式转发给前端。

async for event in graph.astream_events( {"messages": [("user", "分析这个仓库的Issue并生成报告")]}, config={"recursion_limit": 20} ): if event["event"] == "on_tool_start": print("调用工具:", event["name"], event["data"].get("input")) elif event["event"] == "on_tool_end": print("工具输出:", str(event["data"].get("output"))[:300])

状态管理方面,LangGraph每个节点都会收到一个共享的State对象。多Server调用的输出要作为下一步输入时,你可以在State里用字段名作为标识。比如让SQL查询工具把结果写到query_result字段,后续生成报告的节点从这个字段读取。千万注意工具返回的内容要转成字符串,否则有些模型的Context Window会直接爆掉。

4. 常见问题与排查技巧实录

4.1 握手失败:“拒绝访问 (os error 5)”

如果你在Windows上用stdio启动MCP Server,很可能看到类似error: 拒绝访问。 (os error 5) to work without the background server, rerun的报错。这个os error 5在Windows里就是权限不足的意思。排查步骤很简单:

第一,确认你给Server的可执行文件设置了正确的执行权限,尤其当Server是可执行脚本(如.exe或.cmd)时,Windows默认可能会拦截。第二,确认路径中没有特殊字符,最好用绝对路径。第三,检查工作目录是否正确,有些Server启动时依赖相对路径的文件,工作目录不对就会直接报权限错误。

如果是远程HTTP方式,那还要检查鉴权头是否带上,以及服务端是否把/mcp路径暴露在了代理之后。

4.2 工具列表为空或调用不到

这类问题八成出在capabilities声明上。有些Server虽然实现了list_tools回调,但忘了在初始化响应里声明toolscapability,客户端就会认为该Server不支持工具。调试时可以直接用mcp.__main__里的调试命令行来查看server的完整初始化响应。

还有一种情况是Server进程没有正常启动,但客户端没感知到。你会发现握手好像成功了,但拉工具列表时返回空。这种多半是Server端在启动过程中崩了,或者还有依赖没有加载。建议先把Server进程单独跑一遍,用curl或官方调试工具验证,再接入LangGraph。

4.3 流式输出中断与超时

长时间跑LangGraph任务时,Streamable HTTP很容易出现流式输出中断,尤其在请求超过路由器默认超时时。这时的表现是前面几步有正常返回,到了某个工具调用后整个图卡住不动。我的做法是把所有MCP工具的timeout参数调大,同时在LangGraph的配置里增加recursion_limit,避免因为递归次数过多而报错。

如果是自己实现MCP Server,记得给长任务工具增加进度通知机制,而不是让客户端一直干等。协议里虽然没有强制要求,但一个负责任的生产级Server应该主动上报进度。

4.4 MCP 调试三板斧:日志、抓包、最小复现

最后分享一套我一直在用的调试方法论。第一斧是开日志:Python SDK支持--log-level DEBUG,把握手报文和工具调用报文都打出来。第二斧是抓包看报文:对于HTTP传输的方式,直接抓HTTP流量,查看JSON-RPC消息是否符合规范。第三斧是最小复现:出了问题时不要直接拿整个LangGraph图来调,而是先写一个10行代码的Client,单独连目标MCP Server,看能不能握手、能不能调用工具。

这三个层层递进,能帮你快速定位问题在哪个Layer。我把常见问题整理成了速查表,方便你直接对照。

症状可能原因解决办法
初始化超时Server启动慢异步初始化,提高SDK超时
工具拉取不到capabilities未声明tools检查Server初始化响应
os error 5Windows权限不足检查执行权限和路径
工具名冲突多个Server同名工具启用命名空间前缀
流式输出中断网络代理超时调大timeout,使用Streamable HTTP
调用结果为空工具返回类型不规范检查工具返回值是否序列化正确

从我自己的实际体验来说,把MCP和LangGraph组合起来之后,最大的改变不是写代码量变少了,而是整个Agent系统的“接插”成本被压到了极低。以前每加一个工具,就要重新梳理一遍调用链和参数映射,现在只需要配一个MCP Server地址,再在LangGraph里决定把它挂到哪个节点就完事了。如果你还在纠结项目里工具调用越来越乱,不妨先梳理出“哪些能力可以拆成独立Server”,再按这个思路走一遍,跑通后你会回来感谢当初动手的自己。

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

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

立即咨询