MCP协议与LangGraph多Server工具调用:从握手到实际部署的完整指南 从协议握手到多Server调度这条路我踩了不少坑。最近在做一组工具调用的重构需要同时接入知识库检索、数据库查询、还有几个外部服务MCP 天然是最合适的标准接口LangGraph 负责把工具编排进 Agent 循环里。两者一组合理论上很丝滑实际跑起来却有不少细节需要抠——协议版本怎么协商、stdio 子进程怎么管理、多个 Server 的工具命名怎么防冲突、异步环境怎么避免 session 失效……这篇文章就从 MCP 的协议握手讲起一直讲到如何在 LangGraph 里同时挂多个 MCP Server 并完成真实调用把我自己的配置过程、代码实现和踩过的坑都摊开来说。适合已经写过一些 LLM Agent、但对 MCP 内部机制还比较陌生的朋友哪怕你之前完全没接触过 MCP顺着这篇文章的节奏走也能跑通一个多 Server 的完整示例。1. 整体设计与思路拆解1.1 MCP 到底在解决什么问题MCPModel Context Protocol本质上是一个开放协议用来统一 LLM 应用与外部工具、数据源之间的通信。你可以把它理解成工具界的 USB 接口标准在没有 USB 之前每个设备都有自己的接口鼠标、键盘、打印机各连各的适配器一大堆在 MCP 出现之前每个 AI 工具都有自己的 API 风格Agent 代码里充斥着各种 SDK 和 if-else 分支接一个新工具就得写一套新逻辑。有了 MCP 之后工具提供方只要实现一个 MCP Server暴露 tools、resources、prompts 三类能力任何支持 MCP 的 Client比如 LangGraph 里的适配层都能以统一方式发现和调用这些能力。实际开发中我们使用最频繁的就是 tools模型产生调用意图Client 把意图翻译成 JSON-RPC 请求发给 ServerServer 执行完把结构化结果返回整个过程与具体业务 API 的形状无关与语言、平台也基本无关。我在重构中最先感受到的好处是解耦。工具侧不用再关心 Agent 是用什么模型、走什么编排框架Agent 侧也不用关心工具是不是跨语言实现的。一个用 Node 写的服务一个用 Python 写的脚本只要它们实现 MCP 标准LangGraph 这边就能一视同仁地调用。1.2 为什么选 LangGraph 而不是自己写循环MCP 解决的是“工具怎么暴露和发现”的问题LangGraph 解决的是“工具调用怎么被编排和调度”的问题。如果只是接一个工具手写一个 for 循环也能跑让模型输出 tool_calls执行工具把结果塞回消息列表再让模型继续。但这个循环一旦涉及多轮对话、多个工具、失败重试、并发控制手写代码很快就变成一团乱麻。LangGraph 的核心抽象是状态图Agent 的执行过程被拆成节点和边所有消息状态保存在一个显式的 State 里。create_react_agent 这个预置构建器已经帮我们实现了标准的 ReAct 循环——模型节点负责推理和生成调用意图工具节点负责执行条件边决定是继续调用工具还是把最终结果返回给用户。你要做的主要事情就是准备 tools并把 tools 绑定到模型上。选 LangGraph 还有两个实际原因。第一可观测性好。图中每个节点的输入输出都能单独打印和调试一旦某个 MCP 工具返回了异常结果可以很清楚地看到是模型选错了工具还是工具执行阶段出了问题。第二后续扩展能力强。初期一个 Agent 挂多个 Server 够用但如果以后工具数量暴涨、需要拆分专业子 AgentLangGraph 的节点和子图模型能平滑过渡不需要推翻重写。1.3 多 Server 场景下的架构布局多 Server 接入时最关键的架构决策是所有工具塞进同一个 Agent还是拆成多个子 Agent。我一开始的做法是后者想着“知识库工具归知识库 Agent数据库工具归数据库 Agent”结果发现过度设计。对于工具数量不超过几十个的场景单 Agent 多 Server 的工具聚合是最简单可靠的方案。模型只要能看到全部工具和清晰的描述就有能力在它们之间做选择。真正需要拆子 Agent 的场景往往伴随着权限隔离、专用上下文、或者单个模型上下文窗口实在装不下所有工具描述这时候再引入 supervisor 结构也不迟。所以我的建议是第一个版本先用一个 Agent 聚合所有 MCP Server 的工具跑通了再考虑是否需要拆分。架构上就三层LangGraph Agent 作为调度核心MCP Client 作为协议适配层各 Server 作为实际执行单元。数据流向是用户问题进入 Agent模型决定调用哪个工具Client 将请求路由到对应 Server结果回到 Agent 继续推理。1.4 开工前的几个关键选型动手之前有四个选型点值得花十分钟想清楚省得后期返工。第一Server 的传输方式。本地自建或者在同一台机器上跑的脚本优先用 stdio需要跨机器、或者客户端多个实例共享同一个 Server用 HTTP。第二Server 实现语言。MCP 官方对语言没有限制Python 和 TypeScript 生态最成熟。如果只是封装现有 HTTP API用 Python 的 FastMCP 最快如果需要与现有 Node 服务共享代码用 TypeScript SDK 更顺。第三模型能力。工具调用依赖模型的 function calling 能力选模型时要确认它支持结构化工具输出否则工具参数经常会出现格式错误。第四版本锁定。MCP 协议本身、mcp 包、LangGraph 包、adapter 包的版本差异都可能造成兼容性问题项目一开始就要把依赖版本固定下来。2. 核心细节解析与实操要点2.1 协议握手到底是怎么完成的很多人用 MCP 是直接用封装好的 Client对握手过程并不了解但遇到问题时恰恰需要从这里入手排查。MCP 连接建立后Client 与 Server 之间会按如下顺序完成交互首先是 Client 发送 initialize 请求里面带着客户端支持的协议版本列表、客户端信息、以及它支持的能力比如是否支持工具调用。Server 收到后返回自己的协议版本、Server 信息和能力声明同时选定一个双方都能接受的协议版本。这个阶段本质上是“能力协商”双方亮底牌确认大家用哪个版本的协议、都能干什么。然后 Client 要发送 notifications/initialized 通知告诉 Server 初始化阶段已经完成可以进入正式工作状态。注意这是一个通知不需要返回响应。但实际开发中很多人漏掉这个步骤如果用底层 SDK 手搓 Client不发送这个通知会导致 Server 一直认为自己还在初始化中后续工具调用异常。完成初始化后Client 才会调用 tools/list 获取工具列表然后用 tools/call 执行具体工具。整个过程就像两个人见面先互相确认身份和共同语言再交换名片最后才开始谈正事。我强烈建议你在遇到连接问题时先抓一遍握手日志看看是版本协商失败、能力声明不匹配还是 initialized 通知没有送达大部分连接问题在这个层面就能定位。2.2 传输方式怎么选stdio 还是 HTTPMCP 支持两种主流传输方式选型直接影响部署形态和排查思路。stdio 模式下Client 直接拉起 Server 子进程通过标准输入输出传输 JSON-RPC 消息。它的优势是简单、本地隔离好、不需要额外端口适合命令行工具和本地脚本。一个容易被忽略的坑是Server 进程的输出通道必须保持纯净日志打到 stderr千万别打到 stdout否则会污染协议流造成消息解析失败。HTTP 模式下Server 是常驻服务Client 通过 HTTP 请求访问。适合跨机器部署、多个客户端共享同一套工具像团队里统一提供一个数据查询 MCP 服务所有人都通过 HTTP 调它。带来的问题是网络连接管理、超时、鉴权都比本地模式复杂。两种方式没有绝对优劣。我现在的策略很简单开发调试阶段全部用 stdio配合本地脚本快速验证需要布置到服务器共享能力时改用 HTTP。切换 Transport 对 LangGraph 层的代码几乎是透明的只需要改配置这点很舒服。2.3 多 Server 下的工具命名与冲突管理多个 Server 加载到同一个 Agent 之后工具名就是全局命名空间冲突问题随之而来。最常见的场景是两个 Server 都提供一个名为 search 的工具一个搜知识库一个搜数据库模型调用时直接懵了。Adapter 层在聚合工具时常会自动加上 Server 名前缀但我更习惯手动控制。做法是约定一套命名规范每个 Server 内部工具名保持简洁比如 get_news对外暴露时用“Server 代号 下划线 原始工具名”比如 kb_get_news、db_get_news。这套规范在业务侧执行而不是依赖 adapter 自动处理好处是工具名稳定、可读、不随实现细节变化。如果真出现了同名冲突可以在工具加载之后、绑定模型之前用 LangChain 工具包装器重命名。需要注意的是改名之后工具描述里最好也同步说明“本工具属于某某领域”帮助模型区分同名前缀的工具。3. 实操过程与核心环节实现3.1 环境准备与依赖清单我使用的环境是 Python 3.11 uv 管理依赖。核心包如下uv add mcp langgraph langchain-core langchain-mcp-adapters langchain-openaimcp协议 SDK包含 Server 和 Client 的实现FastMCP 就在这里langgraphAgent 编排框架提供 StateGraph、create_react_agentlangchain-mcp-adaptersMCP 工具与 LangChain 工具的桥接层核心工具类包括 MultiServerMCPClient、load_mcp_toolslangchain-coreLangChain 基础类型langchain-openai模型接入只要你的模型服务兼容 OpenAI 接口格式就能用项目结构建议分成三层project/ ├── servers/ # 存放各 MCP Server 实现 │ ├── math_server.py │ ├── weather_server.py │ └── docs_server.py ├── agent/ # LangGraph Agent 代码 │ └── main.py └── pyproject.toml注意将 langchain-mcp-adapters 和 langgraph 的版本锁死因为这两个包迭代非常快接口变动不小。我用的版本组合是 langgraph 0.2.x langchain-mcp-adapters 0.2.x如果你在复现时遇到接口不同优先查所选版本的官方示例。3.2 从 Server 配置到 LangGraph 接入这里是最核心的部分我用一个 MultiServerMCPClient 同时挂载两个 Server一个走 stdio一个走 HTTP。先准备两个简单的 MCP Server。math_server.py 是一个 stdio 模式的本地脚本from mcp.server.fastmcp import FastMCP mcp FastMCP(math) mcp.tool() def add(a: int, b: int) - int: 计算两个整数的和 return a b mcp.tool() def multiply(a: int, b: int) - int: 计算两个整数的积 return a * b if __name__ __main__: mcp.run(transportstdio)weather_server.py 是一个 HTTP 模式的远程服务用 FastMCP 启动from mcp.server.fastmcp import FastMCP mcp FastMCP(weather) mcp.tool() def get_weather(city: str) - str: 获取指定城市的当前天气概况支持中文城市名 return f{city}多云21℃~26℃东南风2级 if __name__ __main__: mcp.run(transporthttp, host127.0.0.1, port8000)然后写 Agent 入口 main.py。先初始化模型再创建多 Server 配置并加载工具import asyncio from langchain_openai import ChatOpenAI from langchain_mcp_adapters.client import MultiServerMCPClient from langgraph.prebuilt import create_react_agent model ChatOpenAI( modelyour-tool-call-model, base_urlhttps://your-compatible-endpoint.example.com, api_keyyour-token, ) MCP_SERVERS { math: { transport: stdio, command: uv, args: [run, python, servers/math_server.py], env: {PYTHONPATH: ./}, }, weather: { transport: http, url: http://127.0.0.1:8000/mcp, }, } async def run(): async with MultiServerMCPClient(MCP_SERVERS) as client: tools await client.get_tools() agent create_react_agent(model, tools) result await agent.ainvoke({ messages: [(user, 我的城市下雨了吗顺便帮我计算晴天数和下雨天数的比例如果有晴天数据的话)] }) for message in result[messages]: if hasattr(message, content) and message.content: print(f\n[{message.type}] {message.content}) if __name__ __main__: asyncio.run(run())这里面的 get_tools 会把两个 Server 的工具一起收集回来返回的是一个 LangChain Tool 列表可以直接传给 create_react_agent。我刚接触时最不习惯的地方是 agent 的输入 format 里没有 tools 参数而是通过 model.bind_tools 隐式绑定所以如果 Tools 没有成功附着到模型上Agent 会一直“看不见”工具这个问题后面排查章节细说。3.3 一个包含查询与计算的真实串联场景只验证工具能被调用还不够我想演示一下多 Server 协作时模型自主编排的效果。注册了 math 和 weather 两个 Server 后我构造了一个提问“某城市今天的天气怎么样如果气温在 20 度以下帮我算一下未来三天气温的整数平均值。”模型看到的问题同时涉及天气查询和数学计算它会先调用 weather 工具获取天气再根据返回结果决定是否调用 math 工具做均值计算。实际执行时LangGraph 的 Agent 循环会自动完成多次模型-工具交替不需要手工串接。我跑了一次调用消息轨迹大致是模型生成 tool_call指向 weather:get_weather工具节点调用 weather Server返回天气信息模型读取结果后判断需要计算均值生成 tool_call指向 math:add工具节点调用 math 内部逻辑模型组装最终文字回答这段轨迹说明模型有能力在多个 Server 之间自由选择工具前提是 Server 名和工具名足够清晰。另外如果某个 Server 暂时不可用Agent 不会死机而是会把这个失败结果当作上下文继续推理可能在回答里直接说明工具不可用。这个容错特性在多 Server 场景下很重要。3.4 更进一步手动构建 Agent 循环create_react_agent 很方便但如果你想控制重试逻辑、并行调用多个工具或者记录每一步耗时手动构建 LangGraph 图也很简单。基本结构是 State 里维护消息列表节点分为 call_model 和 execute_tools条件边判断是否继续。from typing import Annotated, TypedDict from langgraph.graph import StateGraph, END from langgraph.graph.message import add_messages class AgentState(TypedDict): messages: Annotated[list, add_messages] async def call_model(state: AgentState, config): response await model_with_tools.ainvoke(state[messages], config) return {messages: [response]} async def execute_tools(state: AgentState): messages state[messages] last_message messages[-1] outputs [] for tool_call in last_message.tool_calls: tool {t.name: t for t in tools}[tool_call[name]] tool_result await tool.ainvoke(tool_call[args]) outputs.append(ToolMessage(tool_result, tool_call_idtool_call[id])) return {messages: outputs} def should_continue(state: AgentState) - str: last state[messages][-1] return continue if last.tool_calls else end graph_builder StateGraph(AgentState) graph_builder.add_node(model, call_model) graph_builder.add_node(tools, execute_tools) graph_builder.set_entry_point(model) graph_builder.add_conditional_edges(model, should_continue, {continue: tools, end: END}) graph_builder.add_edge(tools, model) app graph_builder.compile()这段代码中点名了一个容易被忽略的机制langgraph 的 state 更新依赖 add_messages 把新消息追加到列表而不是覆盖。第一次手写图时我忘记了这个 reducer导致每次节点更新都清空之前的历史记录Agent 完全失忆。理解这个 mechanism 比会调用 create_react_agent 更重要。4. 常见问题与排查技巧实录4.1 握手失败、初始化超时现象Agent 启动后卡住或者报 “Initialization timeout” / “Connection closed” 之类错误。优先检查 Server 进程是否能独立启动。在命令行直接运行 Server 脚本如果可以跑起来说明脚本本身没毛病。接着检查 stdout 有没有被日志污染——这是 stdio 模式最经典的坑有些日志库默认打到 stdout直接把 JSON-RPC 消息流搅乱。解决方法是把日志统一配置到 stderrimport sys print(some debug log, filesys.stderr)如果走 HTTP transportcurl 一下 Server 地址确认端口在监听再检查 client 配置的 url 是否写成了 HTTP 服务首页地址MCP 的 HTTP 入口通常是 /mcp 这样的独立路径不是服务根路径。我排查时惯用方法是在 mcp client 那边手动建立一次连接不做任何工具调用只看握手是否成功。这一步能干净地区分问题在 Server 还是 Agent 层。4.2 工具列表为空现象Server 连接成功Agent 也能跑但模型永远不调用工具猜测是工具根本没加载进来。先在代码里打印 tools 列表看数量是否对tools await client.get_tools() print([t.name for t in tools])如果列表为空返回去检查 Server 装饰器有没有生效特别是 FastMCP 实例中是否用 mcp.tool() 注册过函数。另一个隐蔽点是代码块里用if __name__ __main__:包裹了 mcp.run但在 import 时工具已经注册好了与 Main 入口无关不会造成工具缺失。如果工具数量正常但模型不调用十有八九是 bind_tools 没有生效。检查 create_react_agent 传入 tools 之后模型对象是否真的绑定了这些工具最简单的验证方法是打印 model.bind_tools(tools) 之后的工具描述数量。4.3 多个 Server 工具冲突与串台现象工具能加载但模型调用起来张冠李戴比如想调用知识库检索实际跑的是另一个 Server 的同类工具。这种问题最普遍的原因就是工具名重叠。调试步骤分两层先打印工具全名列表确认 adapter 是否自动加了前缀如果没加你需要在工具加载后统一重命名然后再绑定模型。我这边采用的规范是每个工具名前面带 Server 代号比如 docs_get_article、sql_query、math_add一眼能看出工具归属。还有一个容易被忽略的点工具描述的歧义也会导致模型选错。名字不冲突但描述太相似模型依然会蒙。我的做法是在描述开头加一句“属于 X 类能力适用场景是……”人为拉大工具之间的语义距离。4.4 连接生命周期与 Session 失效现象第一次调用工具成功第二次调用时报 “session closed” 或 “client already exited”。原因基本都是生命周期管理的问题。MultiServerMCPClient 是异步上下文管理器async with 块退出时所有 session 都会被关闭Agent 也就失去了调用工具的能力。如果你在 FastAPI 或脚本服务里把 client 创建和 agent 调用拆到了不同函数一定要保证整个调用链都处在一个存活的上下文内。长驻服务场景下更稳的方案是在应用初始化阶段创建 client并把 session 生命周期拉长到进程级别而不是每次请求都新建。不过这样一来要管理并发访问 session 的问题多个请求同时调 MCP 工具时Session 内部有读写状态不能简单共享。我的一个折中是给每个会话创建独立 client虽然开销大一点但在没有官方并发指导之前最稳妥。4.5 stdio 子进程变为僵尸现象程序退出后本地 MCP Server 脚本的进程还残留着下次启动会报端口占用或者资源耗尽。stdio transport 由 Client 进程拉起 Server 子进程Client 异常退出时子进程可能收不到清理信号。解决方式有两个一是代码层面确保退出时关闭 client 上下文比如 with 块正常退出二是在 Server 脚本里给自己加个简单的心跳机制一段时间没消息就自动退出。后者在开发调试时尤其有用我在 math_server.py 里加过一个基于最后活动时间的超时退出逻辑有效避免了进程堆积。这类问题实现级解决容易但如果 Agent 进程被 kill -9子进程依然没人管运维界面上还是需要定期巡检清理。工具数量多起来之后建议给每个 Server 配置独立的进程管理这样既能自动重启也能看到输出日志。4.6 并发调用与性能问题现象多个工具同时可用时所有调用还是串行执行整体耗时很长。根本原因是 LangGraph 的 ReAct 循环天然是“推理一步 - 执行一批工具 - 再推理一步”的结构。模型一次生成多个 tool_call 的时候工具节点是可以并行执行的但默认实现往往逐个 await。优化手段有两个层面一是在手动图的工具节点里把独立的 tool_call 用 asyncio.gather 并发执行二是在模型层通过 prompt 或工具描述引导它一次性输出多个可并行 tool_call比如“同时获取 A 与 B 的数据后再一起分析”。另外需要注意 Server 端是否支持并发。HTTP 模式的 Server 通常是并发的stdio 模式如果 Server 主循环是单线程的并发调用会被阻塞。我测试过程中曾遇到两个工具都通过 stdio Server 提供并行后反而出错排查出是 Server 内部桥接的上游 API 只允许单连接。这种瓶颈定位起来很费时间建议从一开始就为共享 Server 预留 HTTP 模式。最后再分享两个小技巧排查 MCP 问题时一定要看你用的底层包能不能开 debug 日志。mcp 包里可以通过环境变量打开协议级别的日志输出那一刻你能看到 initialize 请求、tools/list 响应、tool call 全过程比靠猜快得多。另一个技巧是给每个工具的参数 schema 加严格的类型和描述语言模型对模糊 schema 的容忍度很低一个没有类型标注的参数经常让模型编造出错误格式的参数值。还有一点补充一下多 Server 聚合后不要一门心思只想着把工具全塞给一个 Agent。等你真的积累到几十个工具光是工具描述就会吃掉相当大的模型上下文。我的实践体会是先在 LangGraph 里把工具按照领域分装成几个子 Agent再让主 Agent 做路由这种结构跑起来反而更省心单 Agent 聚合适合起步但远期一定绕不开分层编排。希望这篇文章能帮你少走一些我走过的弯路如果有其他有意思的踩坑经验欢迎按着这套流程继续深挖。