第一次真正被 MCP 吸引,是看到同事把本地文件系统、数据库查询和代码仓库操作全部接进同一个 Agent,一个自然语言指令下去,工具被自动编排调用。当时我就想:这东西不就是 AI 时代的万能插座吗?但等我自己动手去对接 MCP,才发现"理解概念"和"跑通流程"之间隔着一整条协议链路。MCP(Model Context Protocol)这两年已经从概念普及期走到了工程落地期,但网上大部分资料还停留在"什么是 MCP"这个层面,真正讲清楚协议握手怎么走、能力怎么协商、多个 MCP Server 怎么在 LangGraph 里协同工作的内容少之又少。
这篇文章不打算再复述"USB-C 接口"那种比喻,而是把我从协议握手到 LangGraph 多 Server 调用这条完整链路踩过的坑、查过的文档、实验过的方案整理出来。内容包括 JSON-RPC 握手细节、三种核心原语的使用、单 Server 的完整调试方法、多个 MCP Server 接入 LangGraph 的工程实践,以及一份可以直接抄走的排查清单。适合两类人看:一是刚开始接触 MCP、想知道它底层到底怎么运转的读者;二是已经在用 LangGraph 做 Agent 编排、想接入多个 MCP Server 但被各种报错卡住的工程师。
1. MCP 到底解决什么问题,以及三个你必须记住的原语
1.1 没有 MCP 之前的"适配器地狱"
在 MCP 出现之前,接一个 AI 工具是件相当难受的事。假设你做了一款桌面 AI 应用,想让模型能读本地文件、查数据库、操作浏览器,你就得给每个工具单独写一套插件协议。今天我们接文件系统写一套,明天接数据库又写一套,后天换一个 AI 宿主程序,前面写的所有对接代码全部作废重来。这是典型的 M×N 适配器问题:M 个 AI 宿主、N 个工具,每个组合都要维护一套专属适配器,维护成本随着接入数量指数级上升。
MCP 的精髓就是把这层关系彻底压扁。它规定了 AI 宿主和工具之间的通用通信标准,工具只需要实现一次 MCP Server,任何支持 MCP 的宿主都能直接接入。用我经常给团队打的比方:以前是给每种家电都配一种专用插座,现在统一成一种标准插座,谁生产家电都得按这个规格来。这个标准化带来的收益非常直接——你在本地调试好的 MCP Server,拿到 Cursor、Claude Desktop、自己的 LangGraph Agent 里都能用,代码基本不需要改。
1.2 Host、Client、Server 三层角色
理解 MCP 架构,第一件事是分清三个角色,很多人在这里就绕晕了。Host 是运行 Agent 的宿主程序,比如你正在用的 IDE 插件、桌面客户端、或者一个自定义的 LangGraph 服务。Client 是 Host 内部的连接器,负责与 Server 建立会话、维护状态、收发消息。一个 Host 可以同时持有多个 Client,每个 Client 对应一个独立的 Server 连接。Server 则是真正提供能力的进程,它向外暴露工具、资源、提示词等能力,等待 Client 来发现和调用。
这三个角色之间的边界在实际工程里很容易模糊。最常见的误解是"客户端 = LangGraph 程序",其实 LangGraph 程序本身是 Host,它内部为每个 MCP Server 创建一个 Client 会话。职责上,Host 管的是业务流程和模型编排,Client 管的是协议层面的会话维护,Server 管的是工具的具体实现。把这三个角色分开想,后面看 LangGraph 的多 Server 集成代码就会觉得顺理成章:每个 Server 都是一个独立的 Client 会话,LangGraph 只是把多个会话的工具统一交给模型编排。
1.3 Tools、Resources、Prompts 三个原语
MCP 协议定义了三种能力原语,Server 可以按需宣告自己支持哪些。Tools 是模型可以主动调用的函数,比如读取文件、查询数据库、发送 HTTP 请求,每个工具带一个 JSON Schema 描述输入参数。Resources 是模型可以读取的只读数据,以 URI 形式定位,比如一个配置文件的内容、一段帮助文档、一份行业报告。Prompts 是服务端预先定义好的提示词模板,呼叫方可以传入参数实例化模板,然后喂给模型。
三者的区别我在团队内训时用过一个类比:Tools 是工具台上能按下去的按钮,模型按下去会有动作发生;Resources 是工具台旁边贴的说明书,模型需要时可以翻开看;Prompts 是老师傅留下的操作模板,照着填参数就能得到一套标准话术。实际项目中最常见的是 Tools,Resources 用于给模型提供上下文数据,Prompts 用的相对少,但做企业内部共享提示词时特别有用。这三个原语如果一开始就分清,后面看协议文档、看 SDK 代码都会顺畅很多。
2. 协议握手:一次完整的"你好"是怎么完成的
2.1 MCP 的消息壳子:JSON-RPC 2.0
MCP 的底层通信不是自定义协议,而是建立在 JSON-RPC 2.0 之上。所有消息都是 JSON 格式,分为三类:请求(request)、响应(response)、通知(notification)。请求和响应通过相同的 id 关联,通知则不需要响应。这个设计跟 HTTP 里请求-响应模型很像,但注意 MCP 是双向的——客户端可以发请求给服务端,服务端也可以主动发通知给客户端(比如工具列表发生变化时)。
在 stdio 传输模式下,每一条 JSON-RPC 消息独占一行,通过标准输入输出传递,消息之间用换行符分隔。这个细节平时不显眼,但排查问题时很重要:如果你在代码里把日志混进 stdout,就会破坏协议解析,导致客户端报错。我见过太多人在 Server 里加 print 调试,结果把整个握手搞挂的情况。规范姿势是把调试信息写到 stderr,stdout 这条通道只允许协议消息通行。
2.2 initialize 到 initialized 的标准动作
MCP 会话建立的第一个动作永远是握手,而且顺序完全固定,不能跳过。第一步,Client 发送initialize请求,带上自己支持的 protocolVersion、自身能力声明(capabilities)和客户端信息(clientInfo)。第二步,Server 返回initialize响应,回复自己实际采用的协议版本、本身的能力声明(如是否支持 tools、resources、prompts、logging)以及服务端信息。第三步,Client 收到响应后发送notifications/initialized通知,告诉 Server 握手阶段完成,可以进入正常业务通信。
这三步走完,会话才算正式建立。之后 Client 才能发送tools/list、resources/list、prompts/list等业务请求。如果 Client 跳过了握手直接发业务请求,Server 通常会用错误码拒绝,我在初期调试时经常踩这个坑。很多 SDK 把握手封装在session.initialize()里,看起来只是一行代码,但理解它的内部顺序对排查"为什么连接上了但调不到工具"这类问题极其关键。
2.3 能力协商:两端各自报"我会什么"
initialize里最容易被忽略的部分是能力协商。客户端和服务端都会声明自己支持的能力集合:客户端可能声明支持 sampling(让 Server 反过来请求模型推理)和 roots(向 Server 暴露文件系统根目录);服务端可能声明支持 tools、resources、prompts、logging。这些声明不是随便写的,之后双方的任何功能调用都必须以握手中宣告的能力为边界,没声明的能力对方不应该调用。
还有一个关键点是协议版本协商。Client 在 initialize 请求里带一个期望的 protocolVersion,Server 会从它支持的版本列表里选一个最合适的返回。如果双方没有共同版本,握手会失败。MCP 的协议版本演进很快,SDK 版本和协议版本是两回事,即使使用的 SDK 都很新,如果一方写死了旧的协议版本字符串,也可能导致兼容问题。我的习惯是让 SDK 管理版本号,不要手动指定,几大语言 SDK 在 initialize 时都会自动带上当前的协议版本。
2.4 stdio 与 Streamable HTTP:传输层的选择
握手流程在逻辑层是统一的,但物理传输层有两种选择。stdio 模式让 Client 通过标准输入输出与本地子进程通信,最常用于本地开发场景。它的优势是简单直接,不需要网络端口,权限边界清晰;缺点是进程生命周期需要 Client 管理,Server 挂了要负责拉起。Streamable HTTP 模式则让 Server 运行在远端,Client 通过 HTTP 请求访问,细分为无状态和有状态两种,通常需要鉴权,适合部署成 SaaS 服务或企业内部共享服务。
实际工程里两种模式经常混合使用。你的私有工具比如文件操作、数据库访问,用 stdio 跑在本地最稳;需要共享的公共服务比如统一的文档检索、GitHub 操作,用 Streamable HTTP 部署在服务器上。LangGraph 这两种传输方式都支持,在创建 Server 实例时配置不同的 transport 类型即可。远程 MCP Server 的鉴权方式(API Key、OAuth)各服务商实现不尽相同,但协议层是一样的,这也是标准化带来的好处之一。
3. 先跑通一个 Server:单 MCP 调用全流程
3.1 用 FastMCP 快速写一个本地文件 Server
在进入 LangGraph 之前,我强烈建议先徒手跑通一个单 Server 的完整调用过程。这不只是练手,更重要的是建立对协议流程的直觉——你会在 langgraph-mcp 封装背后看到的所有动作,在这里都是裸露的。我用 Python 的 FastMCP 库给一个文件访问 Server 做示例,它把协议细节封装得很干净,适合演示。
from fastmcp import FastMCP mcp = FastMCP("local-files") @mcp.tool() def read_file(path: str) -> str: """读取本地文本文件内容。""" with open(path, "r", encoding="utf-8") as f: return f.read() @mcp.tool() def list_directory(path: str) -> list[str]: """列出目录下所有条目。""" import os return os.listdir(path) @mcp.resource("file://{path}") def file_content(path: str) -> str: """通过 resource 方式读取文件,URI 形如 file:///etc/hostname""" with open(path, "r", encoding="utf-8") as f: return f.read() @mcp.prompt() def summarize(file_path: str) -> str: return f"请阅读 {file_path} 并输出结构化摘要。" if __name__ == "__main__": mcp.run() # 默认走 stdio 传输这个 Server 定义了两种工具、一种资源、一种提示词,足够覆盖三种原语的演示。注意mcp.run()默认启动 stdio 传输,这意味着它和被启动的父进程通过标准输入输出通信,启动它之前先确认父进程的 stdout 没有被日志污染。写完后可以直接在命令行用 MCP Inspector 工具测一下,把 Server 进程挂上去就能看到握手日志和工具列表,是调试 Server 最趁手的工具。
3.2 手写 Client 完成握手、列工具、调工具
有了 Server,下一步写一个最原始的 Client,亲手把握手过程走完。我经常给团队强调:用 SDK 封装是一回事,亲手写一遍对协议的理解是完全另一回事。下面这段代码用的是官方 Python SDK,每一步注释都对应前面讲的握手流程。
import asyncio from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client async def main(): params = StdioServerParameters( command="python", args=["server.py"], ) async with stdio_client(params) as (read, write): # 这一步内部完成了 initialize + notifications/initialized 握手 async with ClientSession(read, write) as session: init = await session.initialize() print("服务端信息:", init.serverInfo) print("协议版本:", init.protocolVersion) # 列出工具,确认 Server 宣告的能力 tools = await session.list_tools() for t in tools.tools: print("工具:", t.name, "->", t.description) # 调用 read_file 工具 result = await session.call_tool( "read_file", {"path": "meeting_notes.md"} ) for content in result.content: print("结果:", content.text) asyncio.run(main())这份代码的意义在于把整个链路摊开了:stdio_client负责启动子进程并建立管道,ClientSession负责协议层会话,session.initialize()对应握手,list_tools()对应能力发现,call_tool()对应业务调用。你看到每一个函数对应一个 JSON-RPC 消息类型,这对后面理解多 Server 场景下的并发管理非常有帮助。
3.3 Resources 和 Prompts 的调用方式
Tools 调通之后,Resources 和 Prompts 也值得亲自试一遍,因为它们在多 Server 场景下承担的责任完全不同。Resources 的调用流程是:先resources/list拿到资源列表,或直接按已知 URI 调resources/read获取内容。在上面的 Server 示例里,读取/etc/hostname的资源 URI 就是file:///etc/hostname,客户端可以这样调:
resources = await session.list_resources() for r in resources.resources: print("资源:", r.uri, "->", r.name) content = await session.read_resource("file:///etc/hostname") print(content.contents[0].text)Prompts 则简单一点,先prompts/list看服务端提供哪些模板,再用prompts/get传入参数拿到实例化后的消息列表。我在实践中的一个心得是:Resources 特别适合给模型提供"随时查阅但不必时刻占用上下文"的数据,而 Prompts 适合把团队积累的提示词工程经验固化到服务端,避免每条消息重复携带大量系统提示。单 Server 阶段把这些原语摸一遍,到多 Server 时就能根据实际需求判断该用哪种能力配合哪种 Server。
4. 为什么需要多 Server,以及 LangGraph 在其中扮演什么角色
4.1 多 Server 的典型动机与架构收益
单 Server 的局限很快会出现。你把文件操作、数据库查询、外部 API 请求全部塞进一个 Server,功能上没问题,但工程上全是问题。第一是职责边界模糊,文件工具的代码和数据库连接逻辑搅在一起,出故障时很难定位;第二是权限难以细分,一个 Server 拿的是同一个进程权限,给不了"文件只读、数据库只读"这种细粒度控制;第三是部署和复用困难,整包一个 Server 意味着必须整体升级。
多 Server 架构把这些问题拆开了。一个典型场景:文件 Server 负责读写本地工作目录,数据库 Server 负责连接 Postgres 查询订单数据,GitHub Server 负责仓库信息检索,每个 Server 独立部署、独立演进、独立控权。对外部团队来说,大家各自维护自己的 MCP Server,AI 宿主侧只需要配置一下接入多个 Server 即可。这有点像微服务拆分的思路,只不过拆的是"能力提供者"。多 Server 带来的故障隔离同样重要——数据库 Server 挂了不会影响文件 Server 的功能。
4.2 LangGraph 给你的是什么
LangGraph 在这里解决的核心问题是"编排"。多 Server 接进来之后,模型面对一个混合工具列表,什么时候调用哪个 Server 的哪个工具、多次工具调用之间的依赖关系如何处理、中间结果如何传递,这些就是编排问题。LangGraph 用图结构来表达 Agent 的执行逻辑:节点是处理单元,边是状态转移,状态对象在各个节点之间流动。MCP Server 的工具被注册到图里之后,模型生成的工具调用会被图自动执行,执行结果写回状态。
LangGraph 相比硬编码编排脚本的优势是可扩展和可观测。你想加一个新的 MCP Server,只需要注册进图,不需要改动其他节点的逻辑。每个节点的输入输出都显式定义在状态里,导致调试的时候可以切开看任意一步。比如 Agent 决定先调文件 Server 读取报告,再调数据库 Server 核对销售数据,你可以在状态里看到每一步的工具调用结果。状态驱动的好处是天然适合多 Server 协作——每个 Server 的结果成为后续模型推理的输入,形成一个完整闭环。
4.3 多 Server 协调的三个难点
多 Server 接进 LangGraph 并不是把多个addMcpServerToGraph叠加就完事,实际工程里有三个绕不开的难点。第一是工具名冲突。不同 Server 很可能都有名为search或read_file的工具,模型被同时暴露两个同名工具时会出现混淆,LangGraph 需要可靠的命名空间策略,比如在工具名前面加上 Server 前缀。第二是上下文膨胀。每个 Server 的工具都带着参数 Schema 和描述文本注入到模型的系统提示里,Server 一多,提示词体积急剧膨胀,既浪费 Token 也可能压低模型对核心指令的注意力。第三是调用策略设计。所有工具放在一个图节点里由模型自由调用,还是按类别分散到多个节点由路由逻辑决定?前者简单但大模型容易迷失,后者可控但需要额外设计路由条件。
这些难点没有银弹,但可以通过合理的架构选择来缓解。Server 数量控制在一定范围(通常 2 到 4 个),工具名在各自 Server 侧就做好全局唯一,必要时用 LangGraph 的条件边做粗粒度路由。这些经验是我在多个项目里试错试出来的,放在第 5 节的落地过程里会具体展示。
5. LangGraph 多 Server 调用落地实录
5.1 场景与工程结构
落地实录我选一个自己做过且比较典型的需求:一个数据分析 Agent,需要读本地报告文件,还要连 Postgres 查询销售数据,最终汇总成一份结论。这要求两个 MCP Server 协同工作——文件工具和数据库工具互补,正适合演示多 Server 调用。工程结构如下:
mcp-langgraph-demo/ ├── package.json ├── tsconfig.json ├── servers/ │ ├── file_server.py # 文件操作 MCP Server │ └── db_server.py # 数据库查询 MCP Server └── src/ ├── index.ts # 入口 └── agent.ts # LangGraph 图定义文件 Server 用第 3.1 节那段 FastMCP 代码的扩展版即可,关键是多加一个query_sales_summary工具或者直接复用 Postgres 官方参考 Server。日常开发里我建议数据库 Server 第一版先用@modelcontextprotocol/server-postgres这个参考实现,它能让你快速把 SQL 能力提供给 Agent,等跑通后再换自己定制的版本。
5.2 依赖与准备工作
TypeScript 侧的依赖主要是三块:LangGraph 核心库、langgraph-mcp 集成包、MCP SDK。直接用 npm 安装:
npm i @langchain/langgraph @langgraph/mcp @modelcontextprotocol/sdk注意langgraph-mcp这个包在过去一段时间 API 变化比较频繁,不同版本里createMcpServer的配置项和addMcpServerToGraph的选项签名可能不完全一致。如果照着示例跑不动,优先去查当前版本的官方文档而不是怀疑代码逻辑。Python 侧对应的依赖是langgraph、langgraph-mcp、mcp、fastmcp,本地跑数据库 Server 时还要确保目标数据库可达。
模型方面我用了 DeepSeek API,通过 OpenAI 兼容接口接入,因为整个链路里模型只需要支持 function calling 和流式输出,任何兼容接口的模型都可以。如果你本地有 Ollama 跑模型,把 baseURL 和 model 换掉即可,这块 LangGraph 的抽象做得不错,换模型基本不动其他代码。
5.3 创建多个 MCP Server 实例
核心代码从创建两个 Server 实例开始。createMcpServer这个函数接收名称和传输配置,返回一个 LangGraph 可识别的 Server 对象。文件 Server 用 stdio 拉起本地的 Python 进程,数据库 Server 用 npx 拉起 Postgres 参考实现。数据库连接串从环境变量读取,避免写死在代码里,这是我一直坚持的配置习惯。
import { createMcpServer } from "@langgraph/mcp"; const fileServer = createMcpServer("file-server", { transport: "stdio", command: process.platform === "win32" ? "python" : "python3", args: ["./servers/file_server.py"], }); const dbServer = createMcpServer("db-server", { transport: "stdio", command: "npx", args: [ "-y", "@modelcontextprotocol/server-postgres", process.env.DATABASE_URL ?? "", ], });这里有几个容易被新手踩中的点。第一,command在 Windows 上要注意是python还是python3,直接写错会进程启动失败;第二,npx首次执行需要网络下载包,慢或失败都会导致 Server 起不来,建议先手动在命令行跑一遍npx -y @modelcontextprotocol/server-postgres "连接串"验证可行性;第三,启动子进程的环境变量会继承父进程,数据库连接串放环境变量里正好可以透传进去。
5.4 把 Server 注入 LangGraph 图
有了 Server 实例,下一步就是注入图。addMcpServerToGraph这个工具函数做的事情看起来很简单,背后其实完成了一整套动作:启动子进程、完成 MCP 握手、列出所有工具、把这些工具绑定到模型的可用工具列表里。模型一旦决定调用其中某个工具,图会自动将对应的 MCP 请求发出去并取回结果写回状态。
import { StateGraph, Annotation } from "@langchain/langgraph"; import { addMcpServerToGraph } from "@langgraph/mcp"; const AgentState = Annotation.Root({ messages: Annotation({ reducer: (x, y) => x.concat(y), default: () => [], }), }); async function buildGraph() { let graph = new StateGraph(AgentState); graph = await addMcpServerToGraph(graph, fileServer, { stateKey: "messages", useToolNode: true, }); graph = await addMcpServerToGraph(graph, dbServer, { stateKey: "messages", useToolNode: true, }); return graph; }stateKey指定了工具调用结果写回状态里的哪个字段,messages是最常见的选项。useToolNode为 true 时,LangGraph 会自动把工具调用编排进一个专门执行工具的节点。两个 Server 注入的先后顺序会影响工具列表在上下文中的排序,但不影响调用逻辑。一个容易忽略的点是:注入操作是异步的,因为底层要跑握手和工具发现,所以buildGraph返回的是 Promise,后续构建图链时要注意这里的异步边界。
5.5 模型接入与图装配
图构建好之后,需要给它加上模型节点,并把手动绑定的工具列表(包括两个 MCP Server 的工具,以及其他本地工具)交给模型。DeepSeek 走 OpenAI 兼容接口,在 LangChain 生态里直接复用ChatOpenAI类即可。核心代码是拿到所有 MCP Server 注入的工具名,把它们组合成一个工具列表传给模型绑定。
import { ChatOpenAI } from "@langchain/openai"; const model = new ChatOpenAI({ model: "deepseek-chat", apiKey: process.env.DEEPSEEK_API_KEY, configuration: { baseURL: "https://api.deepseek.com/v1", }, }); // 从图里拿到注入 MCP 工具后的状态定义, // 把工具列表与模型绑定 const modelWithTools = model.bindTools([ fileServer.tools, // 实际 API 中通过 addMcpServerToGraph 封装 dbServer.tools, ]);实际编码时,addMcpServerToGraph返回的图对象里已经集成了工具绑定逻辑,所以你不必像上面这段伪代码一样手工bindTools。这里我只是想表达一层意思:多 Server 的工具和模型之间的铆点就在工具列表绑定的环节,理解这一点,你就能明白为什么 Server 数量膨胀会导致模型上下文变大。如果深入源码你会发现,addMcpServerToGraph在内部就是把 MCP 工具注册到模型的 tools 集合里。
完整装配后的执行流程大致是:Agent 接收用户问题 -> 模型判断需要查数据 -> 调用file-server的读取类工具或db-server的查询工具 -> 工具结果回到状态 -> 模型继续推理 -> 循环直至完成任务。这个过程里模型每次推理都会重新审视所有可用工具,两个 Server 的工具在其中按需被选中。
5.6 运行验证与流式输出
图编译完成后,用.stream()方法运行并打印每个步骤之间的状态变化。LangGraph 支持多种流式模式,"values"模式返回每一步之后的状态快照,"messages"模式返回模型和工具产生的信息块。对多 MCP Server 场景,我强烈建议开发阶段用"values"模式逐帧观察状态变化,你能清清楚楚看到哪个 Server 的工具被调用了、返回了什么。
const graphApp = await buildGraph(); for await (const event of await graphApp.stream( { messages: [ { role: "user", content: "读取 sales_report.md 并查询本周各区域销售额" }, ], }, { streamMode: "values" } )) { const lastMsg = event.messages.at(-1); console.log("---- 状态更新 ----"); if (lastMsg?.content) { console.log(String(lastMsg.content).slice(0, 300)); } }关于流式输出,多说一句实际经验。很多人纠结"流式输出内容到文件"的问题——模型生成太慢,希望边生成边把结果写进文件。最简单的落地方式就是把文件写入做成一个 MCP 工具(比如append_to_file),模型在生成过程中分多次调用该工具追加内容。LangGraph 在messages流式模式下,模型每次输出都会以块的形式推送给调用方,你把推送的数据实时转发给一个文件流即可,完全不需要特殊插件。
6. 常见问题与排查技巧实录
6.1 握手失败与协议版本不匹配
握手失败是最常见的第一道坎,报错通常出现在session.initialize()阶段。排查第一步看 Server 进程的 stderr 输出,很多 SDK 会把握手异常细节写到 stderr。第二步确认双方协议版本能对上,老 SDK 和新 SDK 混合使用时最容易出现版本协商失败,优先把语言 SDK 升级到最新版。还有一个容易被忽视的点:某些封闭环境里npx拉不到包导致 Server 进程直接退出,握手自然失败,这类问题要单独在命令行验证进程能否正常启动。
我整理过一个经验法则:握手失败,先分两半排查——进程层(进程活着吗、端口/管道通吗)和协议层(JSON-RPC 格式对吗、版本对吗)。先用mcp inspector或单独命令启动 Server 确认进程层没问题,再抓握手报文确认协议层没问题,基本能覆盖 90% 的握手故障。
6.2 工具名冲突与命名空间
多 Server 场景里"同名工具"问题非常典型。两个 Server 都暴露search工具时,模型可能选错。解决思路有三个层次:第一,在 Server 侧就保证工具名全局唯一,比如file_search、db_search;第二,利用 LangGraph 对工具名的自动改写机制,很多版本会在工具名前追加 Server 标识;第三,如果 SDK 没有自动加前缀,就自己在创建 Server 实例时指定工具名前缀参数。我的建议是架构层面直接采用"Server 名 + 工具名"的命名规范,一劳永逸。
判断洗重的办法也很简单,在模型调用前先打印完整的工具列表:
const tools = graphApp.getTools(); console.log(tools.map(t => t.name));看见重名再动手处理,不要猜。
6.3 stdio 进程启动异常
stdio 模式虽然简单,但异常类型不少。常见的有:command不对(Windows 上python与python3的差异)、args路径写错、工作目录不对导致相对路径失效、Server 进程需要特定环境变量但没配置。另外要特别留意 stdio 与日志的冲突,前面提过,Server 侧绝对不要把调试日志打到 stdout,否则会污染协议数据流。解决方法是统一把日志输出到 stderr 或文件,保持 stdout 纯净。
启动异常还有一个隐蔽来源:进程退出码。Server 启动即崩溃时,Client 往往只会收到一个 EOF 或空响应,没有任何有意义的报错。这时候在命令行手动执行一下启动命令,直接看进程输出,通常比在 Client 代码里加日志更快定位根因。
6.4 工具调用超时与上下文膨胀
工具调用超时是多 Server 场景的另一个高频问题。数据库查询慢、远程服务响应慢都会让工具执行时间超过默认超时阈值。解决办法是给工具调用设置合理的 timeout,或者在 Server 端做异步化处理。注意超时设置的层次——可以针对整个会话设置,也可以针对单个工具调用设置,粒度要选择好,否则一个慢工具拖死整个 Agent。
上下文膨胀更隐蔽。Server 数量增多后,工具描述文本吃掉的上下文越来越多,模型的注意力会被稀释。一个解决思路是把"低频的大帮助文档"做成 Resources 而非 Tools,让模型需要时才读取;另一个思路是给工具描述写更精炼的注释。实践下来,每个工具描述尽量控制在一两句话,包含足够的触发条件和参数说明即可,长篇示例只会浪费 Token 并降低工具调用准确率。
6.5 排查工具速查表
把这些经验归纳成一张表,贴到团队 Wiki 里几乎能覆盖日常 80% 的问题排查需求:
| 症状 | 可能原因 | 优先排查动作 |
|---|---|---|
| initialize 永远不返回 | Server 进程未启动 | 命令行手动启动验证 |
| 握手报 protocolVersion 错误 | SDK 版本过旧 | 升级 MCP SDK 与 langgraph-mcp |
| 工具列表为空 | Server 未声明任何工具 | 检查 Server 代码与tools/list返回 |
| 同名工具选错 | 缺少命名空间 | 按 Server 名前缀统一改名 |
| 工具调用无响应 | Server 端死锁或超时 | 查 Server 日志,调大 timeout |
| 模型不调用工具 | 描述不清晰或上下文中被淹没 | 精简描述,用 Resources 分担上下文 |
| stdout 数据错乱 | Server 打日志污染管道 | 日志全部改走 stderr |
| 远程 Server 401 | 鉴权配置缺失 | 检查 API Key / OAuth 配置 |
排查的基本功是分层次,进程问题先于协议问题,协议问题先于业务问题。绝大多数"玄学报错"都能在这张表里找到影子。
7. 一点个人体会
从协议握手到 LangGraph 多 Server 调用,这条路看起来技术点密集,但真正难的不是某个具体 API 的用法,而是理解 MCP 作为一个"标准接口"的设计意图。它把工具接入从"为每个宿主单独写适配器"变成了"写一次,到处用"。LangGraph 则是把这个标准化能力接进 Agent 编排体系的桥梁——它负责让模型在正确的时间点选择正确的工具。
我个人在实际操作中的体会是,多 Server 的架构决策一定要克制。不要一上来就接四五个 Server,先两个,把握手、注入、命名、排查这一套流程跑顺,再渐进式扩展。协议版本和 SDK API 都还在快速演进,今天写的代码过几个月可能就有新写法,保持模块化的结构,隔离变化,比追逐最新 API 更重要。最后再分享一个技巧:每次调试前先把 MCP Inspector 挂上,把所有 Server 的握手日志、工具列表、调用响应都过一遍,再回到 LangGraph 里跑 Agent,能把定位问题的速度至少提升一倍。