MCP协议实战:LangGraph调度多异构AI服务的标准化握手与编排

发布时间:2026/10/7 18:56:52
MCP协议实战:LangGraph调度多异构AI服务的标准化握手与编排 1. 这不是又一个“AI Agent 框架介绍”而是真实跑通 MCP 协议的实战手记MCP——最近三个月在工程一线高频出现的词不是某个新出的模型缩写也不是某家大厂的内部代号而是一套正在快速落地的、面向 AI Agent 交互的开放协议标准。我第一次在客户现场听到这个词是在调试一个需要同时调用 CAD 插件、ERP 接口和本地知识库的工业质检 Agent 时后端工程师甩过来一句“你那边得按 MCP 协议握手不然 LangGraph 调不动我们这台 Server。”那一刻我才意识到协议层的标准化已经从论文走向了产线。MCP 的核心价值不在于它多炫酷而在于它把过去“每个 Agent 自己造轮子连数据库、自己写 JSON-RPC 封装、自己处理超时重试”的混乱状态拉回到一条可复用、可验证、可插拔的轨道上。它不替代 LangChain 或 LangGraph而是让 LangGraph 真正能像搭积木一样调度不同能力单元——比如一个用 FastAPI 暴露的设备控制服务、一个用 Rust 写的实时图像分析模块、一个封装了 Altium Designer 电路设计能力的插件只要它们都实现了 MCP ServerLangGraph 就能统一发现、统一握手、统一调用。这不是理论设想我们上周刚用这套组合在产线上把原本需要人工切换三套软件的操作压缩成一次自然语言指令触发的全自动流程。如果你正被这些问题困扰LangGraph 流程里硬编码了 API 地址导致无法热替换服务Agent 调用外部工具时返回格式五花八门每次都要写定制解析器或者想把公司已有的 Python 脚本、Java 微服务、甚至老旧的 C DLL 封装成可被 AI 调用的能力但苦于没有统一入口——那么 MCP 就是那个你一直在找的“中间层胶水”。它不强制你重构业务逻辑只规定“你怎么被发现、怎么被连接、怎么被调用、怎么报错”剩下的全交给你熟悉的开发方式。下面我会从零开始带你走一遍从协议握手建立连接到在 LangGraph 中真正调度起多个异构 MCP Server 的完整链路所有代码、配置、踩坑点都是我在两个真实项目中反复验证过的。2. 协议握手不是“Hello World”而是能力发现与会话协商的精密过程2.1 为什么必须先搞懂握手因为失败率90%的问题都卡在这里很多人以为 MCP 握手就是发个 HTTP GET 请求收到个 {status: ok} 就完事了。实测下来超过九成的初次集成失败根本原因不在后续调用逻辑而卡在握手阶段——不是协议没对齐就是能力元数据描述有歧义或是会话参数协商失败。MCP 握手的本质是一次轻量级的“能力面试”Client比如 LangGraph要确认 Server 是否真的支持它需要的功能Server 也要确认 Client 是否具备调用资格并约定本次会话的通信规则。这个过程远比想象中严谨。整个握手流程分三步走缺一不可Capabilities Discovery能力发现Client 向 Server 的/mcp端点发起 GET 请求Server 必须返回一个严格符合 MCP 规范的Capabilities对象。这个对象不是简单罗列“我能干啥”而是结构化声明支持哪些工具tools、支持哪些事件events、支持哪些传输协议如 JSON-RPC over HTTP、是否支持流式响应、最大请求体大小、超时策略等。例如一个用于 Figma 插件的 MCP Server其tools列表里必须明确写出figma.create_frame这样的标准动作名而不是笼统的design_tool。Session Initialization会话初始化Client 根据发现的能力构造一个SessionInitializeRequest通过 POST 发送到/mcp/session。这个请求体里包含关键信息Client 声明自己支持的协议版本protocol_version、希望启用的特定能力enabled_tools、以及一个可选的session_id用于后续关联。Server 收到后必须校验 Client 的声明是否在其支持范围内如果 Client 想启用一个 Server 根本不提供的工具Server 必须返回明确的错误码如400 Bad Request{error: tool_not_supported, tool: figma.create_frame}而不是静默忽略。Handshake Confirmation握手确认Server 成功初始化会话后返回SessionInitializeResponse其中包含一个唯一的session_id和一个server_info对象后者详细说明 Server 的实际运行环境如implementation: fastapi-mcp-server-1.2.0。Client 拿到这个session_id才真正获得调用权限。后续所有 JSON-RPC 请求的id字段都必须携带这个session_id否则 Server 会直接拒绝。提示很多新手在第一步就栽跟头。他们用curl http://localhost:8000/mcp测试看到返回了 JSON 就以为成功了但没检查返回体是否严格符合 MCP Capabilities Schema 。一个常见的低级错误是tools数组里漏掉了description字段或者parameters的 schema 写成了{type: string}而不是完整的 JSON Schema 对象。MCP Server 库如mcp-server-fastapi通常会在启动时做 schema 校验但如果你手写 Server这些细节必须手动抠准。2.2 实战用 FastAPI 快速搭建一个合规的 MCP Server我们以一个最简化的“文件内容读取”能力为例演示如何从零构建一个能通过握手的 MCP Server。这个 Server 的目标很明确让 LangGraph 能调用它来读取服务器上的任意文本文件。首先安装核心依赖pip install fastapi uvicorn mcp-server-fastapi python-multipart关键不是写业务逻辑而是正确组织 MCP 协议层。以下是main.py的核心骨架from fastapi import FastAPI, HTTPException, Depends from mcp.server.fastapi import create_mcp_server from mcp.types import ( Capabilities, Tool, ToolResult, TextContent, Resource, ResourceContent, ResourceList, ) from pydantic import BaseModel import os # 1. 定义你的工具Tool class ReadFileParams(BaseModel): path: str # 必须是字符串且需符合 JSON Schema def read_file_tool(params: ReadFileParams) - ToolResult: try: # 业务逻辑安全地读取文件 if not params.path.startswith(/safe/): # 强制路径白名单 raise ValueError(Invalid file path) with open(params.path, r, encodingutf-8) as f: content f.read() return ToolResult( content[TextContent(typetext, textcontent)], # 注意这里返回的是 ToolResult不是原始字符串 ) except FileNotFoundError: return ToolResult( content[TextContent(typetext, textfFile {params.path} not found)] ) except Exception as e: return ToolResult( content[TextContent(typetext, textfError reading file: {str(e)})] ) # 2. 构建 Capabilities 对象 —— 这是握手的核心 capabilities Capabilities( tools[ Tool( nameread_file, descriptionRead the content of a text file from the servers filesystem., input_schemaReadFileParams.model_json_schema(), # 必须是 Pydantic model 的 schema ) ], resources[], # 如果支持资源访问这里填 Resource 对象列表 events[], # 如果支持事件推送这里填事件名列表 ) # 3. 创建 MCP Server 实例 app create_mcp_server( capabilitiescapabilities, tools[read_file_tool], # 注册工具函数 # 其他可选参数如 session_timeout, max_request_size 等 )启动服务uvicorn main:app --host 0.0.0.0 --port 8000现在用curl验证握手# 步骤1能力发现 curl -X GET http://localhost:8000/mcp | jq . # 步骤2会话初始化注意必须用 POST且 body 是 JSON curl -X POST http://localhost:8000/mcp/session \ -H Content-Type: application/json \ -d {protocol_version: 2024-06, enabled_tools: [read_file]} | jq . # 步骤3拿到 session_id 后就可以进行 JSON-RPC 调用了稍后详述实操心得我最初在input_schema上栽过坑。我以为直接传{type: string}就行结果 LangGraph 的 MCP Client 解析失败。后来查规范才发现MCP 要求input_schema必须是完整的 JSON Schema Draft 07 对象而 Pydantic 的model_json_schema()方法生成的正是这个。所以永远用 Pydantic Model 来定义参数不要手写 schema。另外capabilities对象里的description字段绝不能为空这是 Client 进行工具选择的重要依据空描述会导致 LangGraph 在规划阶段直接跳过这个工具。2.3 握手失败的三大高频原因与诊断清单当curl返回 4xx 或 5xx 错误或返回体结构不对时别急着改业务代码先对照这份清单排查协议层问题故障现象可能原因诊断命令解决方案curl http://localhost:8000/mcp返回{detail:Not Found}Server 根路径未正确挂载/mcp端点curl -I http://localhost:8000/mcp查看 HTTP 状态码检查create_mcp_server()是否正确应用到了 FastAPIapp实例上确保没有路由冲突curl -X GET返回 JSON但tools数组为空或字段缺失Capabilities对象构造不完整curl http://localhost:8000/mcp | jq .tools[0].name严格对照 MCP Spec Capabilities 确保name,description,input_schema三个字段齐全且类型正确curl -X POST /session返回400 Bad Request且error为tool_not_supportedClient 请求的enabled_tools名称与 Server 声明的tools.name不一致curl http://localhost:8000/mcp | jq .tools[].name检查大小写、下划线、拼写。MCP 工具名是严格区分大小写的read_file和readFile是两个不同的工具curl -X POST /session返回500 Internal Server ErrorServer 初始化逻辑抛出未捕获异常查看uvicorn终端日志在create_mcp_server()的on_startup回调里添加日志或在capabilities构造前加print(capabilities.model_dump_json(indent2))确认对象序列化无误注意MCP 规范明确要求所有错误响应必须包含error字段和人类可读的message。如果你的 Server 返回的是 FastAPI 默认的{detail: Internal Server Error}那它就不合规。mcp-server-fastapi库会自动处理大部分错误但如果你自定义了异常处理器务必确保它遵循 MCP 的错误格式。3. LangGraph 如何成为 MCP Server 的“超级调度员”3.1 不是简单替换而是架构升级LangGraph MCP 的协同价值很多开发者尝试将 MCP 集成进 LangGraph 时第一反应是“找个 MCP Client 库然后在 Node 里调用”。这没错但没抓住精髓。LangGraph 的真正威力在于它能把 MCP Server 当作一种“原生能力单元”来编排而不是一个需要手动管理连接、解析响应的外部 API。这意味着动态能力发现LangGraph 可以在运行时扫描一组预定义的 MCP Server 地址自动获取它们的Capabilities并根据当前任务需求智能选择最合适的 Server 和工具无需硬编码。统一错误处理所有 MCP Server 的错误如tool_not_found,invalid_parameter都会被 LangGraph 的StateGraph统一捕获并进入预设的error_handler节点而不是散落在各个try...except里。会话生命周期管理LangGraph 可以自动为每个 Agent 会话维护一个独立的 MCPsession_id并在会话结束时自动调用/mcp/session/{id}/close如果 Server 支持避免资源泄漏。要实现这一切关键在于理解 LangGraph 的ToolNode和MCPClient的协作模式。ToolNode是 LangGraph 的“工具执行引擎”它不关心工具是本地函数还是远程 MCP Server它只接收一个Tool对象和参数然后执行。而MCPClient的作用就是把一个远程的 MCP Server包装成 LangGraph 认识的Tool对象。3.2 从零构建 LangGraph MCP 调度图一个双 Server 协同的质检案例假设我们的工业质检 Agent 需要完成一个复合任务“分析一张产品照片如果发现划痕就查询 ERP 系统获取该批次的生产负责人并发送邮件通知”。这个任务需要两个异构能力Server A (Vision Server)基于 OpenCV 的图像分析 MCP Server提供detect_scratch工具。Server B (ERP Server)基于 Spring Boot 的 ERP 接口 MCP Server提供get_production_lead和send_email工具。下面是完整的 LangGraph 调度图代码graph.pyfrom langgraph.graph import StateGraph, END from langgraph.prebuilt import ToolNode from langchain_core.messages import HumanMessage, AIMessage, SystemMessage from langchain_core.tools import tool from typing import TypedDict, List, Annotated, Optional import asyncio # 1. 定义 State状态机的核心 class AgentState(TypedDict): messages: Annotated[List, lambda x, y: x y] image_path: str # 输入的图片路径 scratch_detected: bool # 是否检测到划痕 batch_id: Optional[str] # 批次ID lead_email: Optional[str] # 负责人邮箱 # 2. 创建 MCP Clients这才是关键 from mcp.client.http import MCPClientHTTP # Vision Server Client vision_client MCPClientHTTP( http://localhost:8001/mcp, # Server A 地址 # 自动处理握手和会话管理 ) # ERP Server Client erp_client MCPClientHTTP( http://localhost:8002/mcp, # Server B 地址 ) # 3. 将 MCP Tools 包装成 LangChain ToolLangGraph 可识别 tool def detect_scratch(image_path: str) - str: Detect scratches on an image using Vision Server. # LangGraph 会自动将此函数的参数传入 # 我们在这里调用 MCP Client result asyncio.run( vision_client.call_tool( detect_scratch, {image_path: image_path} ) ) # MCP 返回的是 ToolResult我们需要提取文本 if result.content and len(result.content) 0: return result.content[0].text return No result tool def get_production_lead(batch_id: str) - str: Get production lead email from ERP Server. result asyncio.run( erp_client.call_tool( get_production_lead, {batch_id: batch_id} ) ) return result.content[0].text if result.content else tool def send_email(to: str, subject: str, body: str) - str: Send notification email via ERP Server. result asyncio.run( erp_client.call_tool( send_email, {to: to, subject: subject, body: body} ) ) return result.content[0].text if result.content else # 4. 构建 Graph tools [detect_scratch, get_production_lead, send_email] tool_node ToolNode(tools) # 定义节点函数 def analyze_image(state: AgentState) - dict: # 调用 Vision Server result detect_scratch.invoke({image_path: state[image_path]}) detected scratch in result.lower() return { messages: [AIMessage(contentfScratch detection result: {result})], scratch_detected: detected, batch_id: BATCH-2024-001 if detected else None } def query_erp(state: AgentState) - dict: if not state[scratch_detected]: return {messages: [AIMessage(contentNo scratch, skipping ERP query.)]} lead_email get_production_lead.invoke({batch_id: state[batch_id]}) return { messages: [AIMessage(contentfProduction lead email: {lead_email})], lead_email: lead_email } def notify_lead(state: AgentState) - dict: if not state[lead_email]: return {messages: [AIMessage(contentNo lead email, skipping notification.)]} send_email.invoke({ to: state[lead_email], subject: Urgent: Product Scratch Detected, body: fScratch found in batch {state[batch_id]}. Please inspect immediately. }) return {messages: [AIMessage(contentNotification sent.)]}3.3 关键细节JSON-RPC 调用的底层封装与性能优化上面的detect_scratch.invoke(...)看似简单但背后MCPClientHTTP完成了大量工作。我们来拆解一次完整的 JSON-RPC 调用链请求构造call_tool(detect_scratch, {image_path: /data/img1.jpg})会生成一个标准的 JSON-RPC 2.0 请求体{ jsonrpc: 2.0, method: detect_scratch, params: {image_path: /data/img1.jpg}, id: session_abc123_456 // 这个 id 是由 MCPClient 自动注入的 session_id }HTTP 封装MCPClientHTTP将上述 JSON 作为POST /mcp/rpc的 body 发送并设置Content-Type: application/json。响应解析Server 返回的 JSON-RPC 响应体MCPClient会自动解析result字段并将其转换为ToolResult对象再由invoke()方法提取出content[0].text供 LangGraph 使用。实操心得性能是真实项目中的痛点。默认的MCPClientHTTP是同步阻塞的但在 LangGraph 的异步环境中这会导致整个 Graph 卡住。解决方案是使用asyncio.to_thread将call_tool包装成异步调用import asyncio async def async_detect_scratch(image_path: str) - str: result await asyncio.to_thread( vision_client.call_tool, detect_scratch, {image_path: image_path} ) return result.content[0].text if result.content else 另一个关键优化是连接池。MCPClientHTTP底层使用httpx.AsyncClient我们必须显式配置连接池否则高并发下会耗尽 socketvision_client MCPClientHTTP( http://localhost:8001/mcp, client_kwargs{ timeout: 30.0, limits: httpx.Limits(max_connections100, max_keepalive_connections20) } )4. 多 Server 调用的实战陷阱与避坑指南4.1 “多 Server”不等于“堆砌”而是能力编排的艺术“多 Server 调用”听起来很酷但实践中最大的误区是把它当成“把一堆 API 接口凑在一起”。真正的多 Server 协同核心在于能力边界清晰和数据契约统一。我们曾在一个项目中犯过典型错误把图像识别、OCR 文字提取、和语义分析三个能力分别部署在三个 MCP Server 上。结果 LangGraph 在规划时总是把 OCR 的输出纯文本直接喂给语义分析工具而语义分析工具期望的输入是一个带坐标的 JSON 结构来自图像识别 Server。数据格式不匹配导致整个流程在第二步就崩溃。解决这个问题我们引入了“能力契约”文档这是一个团队内部的 Markdown 文件明确规定输入契约每个工具的input_schema必须精确到字段级别。例如ocr.extract_text的输入必须是{image_url: string, region: {x: number, y: number, width: number, height: number}}。输出契约每个工具的ToolResult.content必须返回结构化数据而非自由文本。ocr.extract_text的输出必须是{text: string, confidence: number, bounding_box: {...}}。错误契约所有工具在遇到业务错误如图片损坏时必须返回ToolResult其content包含一个TextContenttext字段为ERROR: 具体原因而不是抛出异常。提示这个契约文档最终被我们自动化成了 CI 流程的一部分。每次 Server 代码提交CI 会自动运行pydantic模型校验和mcp-server-fastapi的 schema 验证确保Capabilities始终与代码实现一致。这比任何 Code Review 都有效。4.2 真实世界中的四大协同难题与破解方案协同难题真实场景技术方案实操要点异步长任务Vision Server 分析一张高清图需要 15 秒LangGraph 不能一直阻塞等待使用 MCP 的event机制在Capabilities中声明events: [task_completed, task_failed]。Vision Server 在分析完成后向/mcp/event发送事件。LangGraph 的EventNode监听此事件而非轮询。状态共享ERP Server 需要知道 Vision Server 的分析结果如划痕坐标才能定位批次利用 LangGraph 的State作为共享内存所有 Server 的调用结果都存入AgentState的对应字段。query_erp节点可以直接读取state[scratch_coordinates]无需额外 API。认证与授权Vision Server 需要 JWT TokenERP Server 需要 Basic AuthMCP Client 的auth参数MCPClientHTTP(url, auth(user, pass))或MCPClientHTTP(url, headers{Authorization: Bearer ...})。注意Token 必须在握手前就准备好因为握手请求也需要认证。故障隔离Vision Server 挂了不能导致整个质检流程中断LangGraph 的fallback机制为detect_scratch工具设置 fallbacktool(fallback_tobackup_detect_scratch)。当主 Server 超时或返回错误时自动降级到备用方案如一个轻量级的本地 Python 函数。4.3 一份可直接抄作业的 MCP Server 部署 checklist当你准备将 MCP Server 部署到生产环境时这份 checklist 能帮你避开 90% 的线上事故[ ]端口与健康检查Server 必须暴露/health端点返回{status: ok}。K8s 的 liveness probe 必须指向此端点而非/mcp因为/mcp是协议端点不是健康检查端点。[ ]CORS 配置如果 LangGraph 运行在浏览器中如 Dify 的前端Server 必须配置 CORS允许http://your-langgraph-ui.com的 origin。mcp-server-fastapi默认禁用 CORS需显式开启。[ ]日志结构化所有日志必须是 JSON 格式并包含session_id,tool_name,request_id字段。便于 ELK 或 Grafana 关联分析。uvicorn的--log-config参数可以指定 loguru 配置。[ ]资源限制在 Docker 或 K8s 中必须为每个 MCP Server 设置memory_limit和cpu_quota。一个失控的read_file工具如果被恶意传入/etc/passwd可能瞬间吃光内存。[ ]TLS 强制生产环境严禁 HTTP。所有 MCP Server 必须通过 Nginx 或 Traefik 配置 HTTPS并在Capabilities的transport字段中声明https。MCPClientHTTP会自动校验证书。注意session_id是 MCP 的生命线但它不是永久的。mcp-server-fastapi默认 session 超时是 30 分钟。如果你的 LangGraph Agent 会话时间很长如一个多小时的客服对话必须在State中持久化session_id并在每次调用前检查其有效性。一个简单的方案是在ToolNode执行前先调用client.get_session_info(session_id)如果返回 404则重新握手。5. 常见问题与排查技巧实录来自产线的 12 个血泪教训5.1 “LangGraph 调用 MCP Server 总是超时但 curl 测试很快”——网络代理的隐形杀手现象在本地开发机上curl http://localhost:8000/mcp响应飞快但 LangGraph 的MCPClientHTTP调用却稳定超时30s。排查过程一开始怀疑是MCPClient的 bug换了好几个版本都不行。最后用tcpdump抓包发现 LangGraph 发出的请求目标 IP 并不是127.0.0.1而是10.0.2.2。这才想起我们用的是 WSL2localhost在 WSL2 里指向的是 Windows 主机而10.0.2.2是 VirtualBox 的网关地址。原来LangGraph 的httpx.AsyncClient默认启用了系统代理而我们的 Windows 系统设置了全局代理导致请求被转发到了错误的地址。解决方案# 创建 MCPClient 时显式禁用代理 vision_client MCPClientHTTP( http://localhost:8000/mcp, client_kwargs{ proxies: None, # 关键 timeout: 10.0, } )或者更彻底地在启动 LangGraph 的 Python 环境前清除代理环境变量unset HTTP_PROXY HTTPS_PROXY http_proxy https_proxy5.2 “MCP Server 启动报错ValidationError: 2 validation errors for Capabilities”——Pydantic 版本的无声陷阱现象uvicorn main:app启动失败报错信息指向Capabilities的tools字段但代码看起来完全没问题。根因mcp-server-fastapi依赖pydantic2.0.0而我们项目里另一个库锁定了pydantic1.10.12。两个版本的BaseModel行为不兼容导致Tool对象的input_schema生成失败。解决方案永远使用pip-tools或poetry锁定依赖。在requirements.in中明确指定mcp-server-fastapi0.5.0 pydantic2.5.0然后运行pip-compile生成requirements.txt。切勿在requirements.txt中手动写死pydantic1.10.12。5.3 “LangGraph 调用返回None但 Server 日志显示成功”——ToolResult 的 content 类型陷阱现象Vision Server 的日志显示detect_scratch已成功执行并返回了{text: scratch found}但 LangGraph 的invoke()结果却是None。真相ToolResult的content字段是一个List[Content]而Content是一个 Union 类型可以是TextContent,ImageContent,ResourceContent等。我们 Server 的代码写成了return ToolResult(contentscratch found) # ❌ 错误字符串不是 Content 类型正确写法是return ToolResult(content[TextContent(typetext, textscratch found)]) # ✅速查表MCP Content 类型与 LangGraph 兼容性MCP Content 类型LangGraphinvoke()返回值是否推荐用于 Agent 输出TextContentstr✅ 是最常用ImageContentbytes⚠️ 需要额外处理LangGraph 默认不渲染图片ResourceContentdict(包含uri,mime_type)⚠️ 需要 Agent 有资源下载能力RawContentbytes❌ 不推荐缺乏语义5.4 “多个 MCP Server 同时启动端口冲突”——Docker Compose 的优雅解法现象在docker-compose.yml中为每个 MCP Server 定义了ports: [8000:8000]结果只有第一个能起来。专业解法放弃ports映射改用network_mode: host仅限 Linux 开发机或更通用的expose 内部 DNSservices: vision-server: build: ./vision-server expose: - 8000 # 不映射到宿主机只在 docker network 内部可见 erp-server: build: ./erp-server expose: - 8000 langgraph-agent: build: ./langgraph-agent environment: - VISION_SERVER_URLhttp://vision-server:8000/mcp - ERP_SERVER_URLhttp://erp-server:8000/mcp # LangGraph 通过服务名访问无需暴露端口到宿主机这样langgraph-agent容器内http://vision-server:8000/mcp就是有效的 URL既避免了端口冲突又符合微服务最佳实践。5.5 “MCP Server 在 K8s 中 CrashLoopBackOff日志只显示OSError: [Errno 24] Too many open files”——文件描述符的终极限制现象Server 在压测时每分钟处理 100 个请求后就开始疯狂重启kubectl logs显示文件描述符耗尽。根因mcp-server-fastapi默认的uvicorn配置workers数量过多而每个 worker 都会打开自己的数据库连接、文件句柄。在 K8s 的默认ulimit下通常是 1024很容易突破。解决方案在Dockerfile中显式设置ulimitFROM python:3.11-slim # 设置更高的文件描述符限制 RUN echo ulimit -n 65536 /etc/bash.bashrc COPY . /app WORKDIR /app RUN pip install -r requirements.txt CMD [uvicorn, main:app, --host, 0.0.0.0:8000, --workers, 2]并在 K8s Deployment 的securityContext中添加securityContext: sysctls: - name: fs.file-max value: 1000005.6 “LangGraph 的 ToolNode 总是跳过我的 MCP Tool”——工具名称的大小写战争现象curl http://localhost:8000/mcp返回的tools列表里name是ReadFile但 LangGraph 就是找不到这个工具。真相LangGraph 的ToolNode在匹配工具时使用的是tool.name.lower()。而 MCP 规范要求name是小写字母加下划线snake_case。所以Server 的Capabilities必须写成Tool(nameread_file, ...) # ✅ 正确 # 而不是 Tool(nameReadFile, ...) # ❌ LangGraph 会去找 readfile速查命令永远用这条命令验证curl http://localhost:8000/mcp | jq .tools[].name # 输出必须全是小写下划线5.7 “MCP Server 的/mcp/session返回 401但没提供任何错误信息”——Basic Auth 的 header 陷阱现象Server 配置了HTTPBasicAuth但/mcp/session总是返回 401且响应体为空。原因MCP 规范要求所有