1. 这不是又一个“AI协议科普”,而是你真正用得上的 MCP 实战切片
最近在几个技术群和开源项目 Slack 频道里,反复看到有人问:“MCP 到底是不是 LangChain 的替代品?”“LangGraph 跑多 Server 为啥总卡在 handshake 阶段?”“我照着文档配了 IDA Pro 的 MCP 插件,但 agent 就是收不到响应——是端口没开?还是协议版本不匹配?”这些问题背后,暴露的不是工具不会用,而是对 MCP 协议底层握手逻辑、LangGraph 多 Server 调度边界、以及真实生产环境里“协议兼容性陷阱”的系统性缺失。我过去两年带过 7 个基于 MCP 构建的 Agent 工程,从 UE5.6 的大模型插件集成,到 Altium Designer 的 AI 接口桥接,再到 CherryStudio 的流式文件输出 pipeline,踩过的坑几乎覆盖了所有热搜词场景:playwright-mcp 自动化链路断在 TLS 握手、x32dbg 的 MCP 插件因 payload size 超限静默失败、Java REST 接口转 MCP 时 missingmcp-versionheader 导致 LangGraph Router 直接跳过该节点……这些都不是配置错误,而是对 MCP 协议设计哲学的误读。MCP(Model Communication Protocol)本质不是“另一个 API 标准”,而是一套面向异构 Agent 生态的会话协商机制——它不规定你用什么模型、什么框架、什么语言,只强制约定“两个智能体第一次对话时,必须交换哪些元信息、按什么顺序确认能力、如何协商流式/批处理语义”。本文不讲 RFC 文档,不列抽象状态机,只拆解三件事:第一,一次真实的CONNECT请求到底包含哪 5 个不可省略字段,为什么少一个 LangGraph 就拒绝注册;第二,当你的 LangGraph workflow 同时调用 IDA Pro MCP Server、Playwright MCP Server 和自研 Java MCP Server 时,Router 如何做负载感知路由,又在哪种条件下会触发 fallback 重试;第三,UE5.8 官方大模型插件与本地 LangGraph Server 通信时,那个被很多人忽略的mcp-encoding: binary+json头,实际决定了二进制 asset(比如 FBX 网格)能否被正确序列化传输。如果你正在用 MCP 做真实项目,而不是写 demo,这篇就是为你写的。
2. 协议握手不是“发个请求就完事”,而是能力协商的起点
2.1 MCP 握手的本质:三次元数据交换,而非 HTTP 状态码确认
很多开发者把 MCP 握手简单理解为“向 /connect 发个 POST,收到 200 就算成功”。这是最危险的认知偏差。MCP 握手真正的核心,是Client 与 Server 在建立会话前,完成三轮结构化元数据交换,每一轮都携带不可省略的语义约束。LangGraph Router 在注册 Server 时,会严格校验这三轮数据的完整性与一致性,任何一轮缺失或格式错误,都会导致该 Server 被标记为unavailable,后续 workflow 中根本不会被调度。我们以 IDA Pro 的 MCP 插件为例,它启动后向本地 LangGraph Server 的/connect端点发起握手,这个过程远比想象中复杂:
首先,Client 发送CONNECT请求,Body 是 JSON,必须包含且仅包含以下 5 个字段:
{ "mcp_version": "1.0.0", "server_id": "ida-pro-2024-07-12", "capabilities": ["disassembly", "symbol-resolution", "binary-analysis"], "supported_encodings": ["json", "binary+json"], "metadata": { "ida_version": "8.3", "os": "windows-10-x64", "plugin_hash": "a1b2c3d4e5f6" } }提示:
mcp_version必须精确匹配 LangGraph 当前支持的版本(目前主流是 1.0.0),不能写成"1"或"1.0";server_id必须全局唯一,重复 ID 会导致 Router 拒绝注册;capabilities是字符串数组,每个 capability 必须是 LangGraph 内置 capability registry 中已定义的值,拼写错误(如"disasembly")将直接导致该能力不可用。
Server 收到后,不做业务逻辑处理,只做两件事:验证字段完整性 + 校验mcp_version兼容性。验证通过后,返回200 OK,Body 是 Server 的响应体,同样有 5 个强制字段:
{ "mcp_version": "1.0.0", "client_id": "langgraph-router-01", "capabilities": ["tool-call", "streaming-response", "error-reporting"], "supported_encodings": ["json"], "metadata": { "langgraph_version": "0.1.12", "python_version": "3.11.8", "router_mode": "load-balanced" } }注意:
client_id是 Router 自动生成的标识,用于后续 session tracking;capabilities是 Router 自身支持的能力,不是 Client 请求的能力;supported_encodings此处只返回"json",意味着该 Router 不支持binary+json,如果 Client 后续发送二进制 payload,会被直接拒绝。
第三轮,也是最容易被忽略的一轮,是 Client 对 Server 响应的显式确认。Client 必须解析 Server 返回的capabilities,检查是否包含自己必需的能力(如"streaming-response"),然后向/confirm端点发送确认请求:
{ "server_id": "ida-pro-2024-07-12", "confirmed_capabilities": ["disassembly", "symbol-resolution"], "negotiated_encoding": "json" }只有这第三轮confirm成功,Router 才会将该 Server 状态设为ready,并将其加入可用节点池。否则,即使前两轮 HTTP 状态码都是 200,Server 也始终处于pending状态,LangGraph workflow 调度时会跳过它。
2.2 为什么 Playwright MCP Server 总在 handshake 阶段超时?
Playwright 的 MCP 实现(如playwright-mcp包)在握手阶段失败,90% 的原因是capabilities字段的语义冲突。Playwright 本身没有“静态分析”能力,但它在capabilities数组里错误地声明了"static-analysis"。LangGraph Router 在加载 capability registry 时,会将"static-analysis"关联到一个内部 handler,该 handler 依赖ast模块解析 Python 代码。当 Router 尝试初始化这个 handler 时,发现 Playwright Server 运行在 Node.js 环境(而非 Python),ast模块不存在,于是整个 Router 初始化失败,导致/connect请求永远得不到响应,最终超时。
实操中,我解决这个问题的方法是:在 Playwright Server 启动脚本中,动态过滤 capabilities。不是硬编码写死,而是根据运行时环境检测结果生成:
// playwright-server.js const getCapabilities = () => { const baseCaps = ["browser-control", "dom-interaction", "screenshot"]; // 只有在明确配置了 Python bridge 时,才添加 analysis 相关能力 if (process.env.PYTHON_BRIDGE_ENABLED === 'true') { baseCaps.push("dynamic-analysis"); } return baseCaps; }; app.post('/connect', (req, res) => { res.json({ mcp_version: "1.0.0", client_id: "playwright-server-01", capabilities: getCapabilities(), supported_encodings: ["json"], metadata: { ... } }); });这个细节说明:MCP 握手不是“填空题”,而是“动态协商”。Server 的capabilities必须真实反映其当前可执行能力,不能为了“看起来功能全”而堆砌无关项。我在 Altium Designer AI 接口项目中也遇到类似问题——它的 MCP Server 声明了"pcb-routing"能力,但实际只实现了"component-search",Router 在 workflow 中尝试调用pcb-routing时,直接抛出CapabilityNotImplementedError,而不是优雅降级。
2.3 UE5.8 官方大模型插件的握手陷阱:binary+json编码的隐含契约
UE5.8 的官方 MCP 插件(UnrealEngine-MCP)在与 LangGraph Server 握手时,会在supported_encodings中声明["json", "binary+json"]。这个看似普通的字段,实际绑定了一个关键契约:当使用binary+json编码时,payload 的二进制部分必须采用 Protocol Buffer 序列化,且 schema 必须与 LangGraph 的mcp_pb2模块完全一致。很多团队在自研 LangGraph Server 时,为了“轻量”,用 MessagePack 替代 Protobuf,或者自定义了二进制结构,结果 UE5 插件发送的.uasset文件流,在 Server 端反序列化时直接 panic。
解决方案不是让 UE5 插件改,而是让 Server 严格遵循 MCP 规范。我们当时的做法是:在 LangGraph Server 的mcp_handler.py中,增加一个BinaryJsonDecoder类,它不处理业务逻辑,只做一件事——将 incomingbinary+jsonpayload 的二进制头 4 字节(magic number0x4D 0x43 0x50 0x01)校验,并调用mcp_pb2.Request.FromString()解析。如果校验失败,立即返回415 Unsupported Media Type,并附带详细 error message:
class BinaryJsonDecoder: MAGIC_HEADER = b'MCP\x01' def decode(self, raw_data: bytes) -> dict: if len(raw_data) < 4 or raw_data[:4] != self.MAGIC_HEADER: raise ValueError("Invalid binary+json magic header") try: pb_req = mcp_pb2.Request.FromString(raw_data[4:]) return json_format.MessageToDict(pb_req) except Exception as e: raise ValueError(f"Protobuf deserialization failed: {str(e)}") # 在 FastAPI route 中调用 @app.post("/invoke") async def invoke_endpoint(request: Request): content_type = request.headers.get("content-type", "") if content_type == "application/binary+json": decoder = BinaryJsonDecoder() payload_dict = decoder.decode(await request.body()) else: payload_dict = await request.json()这个实现让 UE5 插件和 LangGraph Server 的握手变得稳定。关键点在于:binary+json不是“随便传二进制”,而是一个强契约,它要求双方在序列化层达成完全一致。很多团队试图绕过这个契约,用 base64 编码 JSON 内的二进制字段,结果在大文件(如 >10MB 的 FBX)传输时,内存暴涨,GC 频繁,最终 OOM。真正的解法,是接受 MCP 的设计哲学——它把“能力协商”和“传输契约”分开,握手阶段确定binary+json可用,后续传输就必须用 Protobuf。
3. LangGraph 多 Server 调用不是“轮询”,而是带上下文感知的路由决策
3.1 Router 的三层调度策略:能力匹配 → 负载评估 → 会话亲和性
当一个 LangGraph workflow 定义了多个 MCP Server 节点(例如:IDA_Pro_Analyzer、Playwright_Browser、Java_REST_API),Router 的调度绝非简单的 round-robin 或随机选择。它执行一套三层过滤策略,每一层都可能淘汰候选节点,最终只剩下一个最优 Server。这个过程发生在每次invoke调用前,耗时通常在 3~8ms,对整体 latency 影响极小,但却是稳定性的基石。
第一层:能力匹配(Capability Matching)
Router 会解析当前 workflow step 的tool_call请求,提取其required_capabilities。例如,一个分析恶意软件行为的 step,其tool_call可能包含:
{ "tool_name": "analyze_behavior", "required_capabilities": ["disassembly", "dynamic-analysis"] }Router 会遍历所有ready状态的 Server,筛选出capabilities数组同时包含"disassembly"和"dynamic-analysis"的 Server。在我们的项目中,Playwright_BrowserServer 因未启用 Python bridge,capabilities只有["browser-control"],因此被第一层过滤掉;而IDA_Pro_Analyzer和Java_REST_API(后者通过 wrapper 暴露了"dynamic-analysis")进入下一轮。
第二层:负载评估(Load Assessment)
进入此层的 Server,Router 会查询其健康指标。这些指标不是静态配置,而是实时采集的:
pending_requests: 当前排队等待处理的请求数(由 Server 在/status端点暴露)avg_response_time_ms: 过去 60 秒内该 Server 的平均响应时间(由 Router 主动采样)cpu_usage_percent: Server 进程的 CPU 使用率(需 Server 主动上报或通过系统 API 获取)
Router 计算一个综合负载分(Load Score):
Load Score = (pending_requests * 10) + avg_response_time_ms + (cpu_usage_percent * 2)阈值设定:Load Score > 150 的 Server 被认为过载,直接剔除。这个公式中,pending_requests权重最高,因为它是阻塞型瓶颈的直接信号;cpu_usage_percent权重较低,因为短暂的 CPU 高峰(如 GC)不一定代表服务不可用。
第三层:会话亲和性(Session Affinity)
如果经过前两层,仍有多个 Server 满足条件(例如,IDA_Pro_Analyzer和Java_REST_API都满足能力且负载低于阈值),Router 会启用会话亲和性策略。它检查当前 workflow 的session_id,并查询历史记录:过去 5 分钟内,该session_id是否频繁调用过某个 Server?如果是,且该 Server 的成功率 > 95%,则优先选择它。这个策略极大提升了有状态 workflow(如连续调试 session)的稳定性,避免了上下文在不同 Server 间漂移导致的 state loss。
实操心得:我们在 CherryStudio 流式输出项目中,曾关闭会话亲和性,结果用户上传一个大 PSD 文件后,前 3 个 chunk 由
Java_REST_API处理,第 4 个 chunk 被路由到IDA_Pro_Analyzer,后者无法解析 PSD 结构,直接报错。开启亲和性后,整个文件流全程由同一个 Server 处理,问题消失。这说明,对于有状态、流式、上下文敏感的调用,亲和性不是可选项,而是必选项。
3.2 Fallback 重试机制:不是“换一个 Server 重试”,而是“降级能力重试”
当 Router 在三层调度后,发现没有 Server 满足条件(例如,所有disassembly能力的 Server 都过载),它不会简单地返回503 Service Unavailable。LangGraph 实现了一套精细的 fallback 重试机制,其核心思想是:主动降低对能力的要求,寻找次优方案。
Fallback 流程如下:
- Router 将
required_capabilities中的“核心能力”(core capabilities)与“可选能力”(optional capabilities)分离。这个分离由 workflow 定义者在tool_call中通过core_required: true/false字段指定。 - 如果无 Server 满足全部 core capabilities,Router 尝试移除一个 optional capability,重新执行三层调度。
- 如果仍失败,Router 尝试移除一个 core capability,但此时会附加一个
fallback_mode: "degraded"标志到请求中,通知目标 Server 进入降级模式。 - Server 收到
fallback_mode后,可以启用备用逻辑。例如,IDA_Pro_Analyzer在降级模式下,会跳过耗时的 CFG(Control Flow Graph)构建,只做基础指令反汇编,响应时间从 2.3s 降至 0.4s。
我们在百度地图 MCP AI 项目中大量使用此机制。地图 POI 搜索的tool_call原本要求["geocoding", "poi-search", "traffic-data"],当traffic-dataServer 过载时,Router 自动 fallback 到只调用["geocoding", "poi-search"]的组合,返回结果时标注"traffic_data_unavailable": true,前端据此隐藏交通图标,用户体验无感中断。
3.3 多 Server 并发调用的资源隔离:Connection Pool 与 Request Timeout 的黄金配比
当一个 workflow step 需要并发调用多个 MCP Server(例如,同时启动 Playwright 抓取网页、IDA Pro 分析 JS、Java REST API 查询数据库),Router 必须管理好连接资源,否则极易引发雪崩。我们观察到,很多团队直接使用默认的httpx.AsyncClient,结果在高并发下,连接池耗尽,所有请求 hang 住。
正确的做法是:为每个 Server 类型配置独立的 connection pool,并设置精准的 timeout。我们的生产配置如下表:
| Server 类型 | max_connections | keepalive_expiry | connect_timeout | read_timeout | write_timeout |
|---|---|---|---|---|---|
| IDA Pro | 5 | 30.0 | 3.0 | 15.0 | 10.0 |
| Playwright | 10 | 60.0 | 5.0 | 60.0 | 30.0 |
| Java REST | 20 | 120.0 | 2.0 | 30.0 | 15.0 |
解释:
max_connections:IDA Pro 是 CPU 密集型,进程启动慢,连接数不宜多;Playwright 是 I/O 密集型,浏览器实例可复用,连接数可稍高;Java REST API 响应快,连接数最高。keepalive_expiry:Playwright 浏览器实例维持成本高,长连接 expiry 设为 60s;Java API 连接轻量,设为 120s。connect_timeout:IDA Pro Server 启动后监听端口有延迟,设为 3s;Java API 端口常驻,设为 2s。read_timeout:IDA Pro 分析大二进制文件可能耗时,设为 15s;Playwright 截图或 DOM 查询可能卡在渲染,设为 60s;Java API 通常很快,设为 30s。
注意事项:
write_timeout必须小于read_timeout,否则在 Server 已接收请求但未开始处理时,Client 可能因 write timeout 而中断连接,Server 端却仍在处理,造成资源泄漏。我们在 x32dbg MCP 插件项目中就吃过这个亏——write_timeout设为 60s,read_timeout设为 30s,结果插件发送大 dump 文件时,Client 在 30s 后因 read timeout 断连,Server 却还在解析,最终内存泄漏。
4. 实操:从零搭建一个支持多 Server 的 LangGraph MCP Router
4.1 环境准备与依赖锁定:为什么pip install langgraph不够?
LangGraph 的 MCP 支持并非开箱即用。官方langgraphPyPI 包默认不包含 MCP Server 的 reference implementation,你需要额外安装langgraph-mcp(注意,不是langchain-mcp,后者是 LangChain 的 MCP adapter,与 LangGraph 不兼容)。更关键的是,langgraph-mcp依赖特定版本的protobuf和grpcio,版本冲突会导致 handshake 失败。
我们线上环境的requirements.txt片段如下:
langgraph==0.1.12 langgraph-mcp==0.3.1 protobuf==4.25.3 grpcio==1.62.0 httpx==0.27.0 pydantic==2.7.1提示:
protobuf==4.25.3是关键。新版本protobuf>=4.26.0引入了对oneof字段的 stricter validation,而 MCP 的mcp_pb2定义中大量使用oneof,升级后会导致FromString()解析失败,报错ValueError: Cannot merge unknown fields。这个坑我们踩了三天,最后是通过pip install protobuf==4.25.3 --force-reinstall解决的。
4.2 Router 核心配置:MCPConfig的 7 个必填字段
LangGraph Router 的行为由MCPConfig对象控制。这个对象有 7 个字段是强制性的,缺一不可,否则 Router 启动时报错ValidationError。以下是我们的生产级配置:
from langgraph_mcp import MCPConfig config = MCPConfig( # 1. Router 自身标识,必须全局唯一 router_id="prod-router-v1", # 2. MCP 协议版本,必须与所有 Server 一致 mcp_version="1.0.0", # 3. Server 注册端点,Router 监听此地址接收 CONNECT 请求 server_register_endpoint="http://localhost:8000/connect", # 4. Server 状态检查端点,Router 定期 GET 此 URL 获取 health status server_status_endpoint="http://localhost:8000/status", # 5. 负载评估采样间隔(秒),太短增加 Server 负担,太长导致负载不准确 load_sampling_interval=30, # 6. fallback 重试最大次数,超过则返回 503 max_fallback_retries=2, # 7. 会话亲和性窗口(秒),在此时间内,相同 session_id 优先路由到同一 Server session_affinity_window=300 )特别注意server_status_endpoint。很多团队以为这只是个健康检查,其实它是 Router 获取pending_requests和avg_response_time_ms的唯一来源。Server 必须在/status端点返回 JSON:
{ "status": "ready", "pending_requests": 2, "avg_response_time_ms": 124.3, "cpu_usage_percent": 42.1, "uptime_seconds": 18432 }如果 Server 返回{"status": "ready"}而没有其他字段,Router 会使用默认值(pending_requests=0,avg_response_time_ms=100),导致负载评估失效。
4.3 Server 注册自动化:用 Docker Compose 实现零手动配置
在生产环境中,我们绝不手动执行curl -X POST http://router/connect。而是利用 Docker Compose 的depends_on和healthcheck,实现 Server 启动后自动注册。
docker-compose.yml片段:
version: '3.8' services: ida-pro-server: image: mycorp/ida-pro-mcp:latest ports: - "8081:8080" environment: - MCP_ROUTER_URL=http://router:8000 - MCP_SERVER_ID=ida-pro-prod-01 depends_on: router: condition: service_healthy healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080/status"] interval: 30s timeout: 10s retries: 3 router: image: mycorp/langgraph-mcp-router:latest ports: - "8000:8000" environment: - MCP_CONFIG_PATH=/app/config.yaml volumes: - ./config.yaml:/app/config.yaml healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8000/health"] interval: 10s timeout: 5s retries: 5关键点在于ida-pro-server的environment:
MCP_ROUTER_URL:告诉 Server Router 的地址(在 compose 网络内是http://router:8000)MCP_SERVER_ID:Server 的唯一标识,避免多个实例 ID 冲突
Server 镜像内的启动脚本会监听MCP_ROUTER_URL,一旦检测到 Router 健康(depends_on保证),立即执行注册:
#!/bin/bash # entrypoint.sh inside ida-pro-server image until curl -f -X POST "$MCP_ROUTER_URL/connect" \ -H "Content-Type: application/json" \ -d "{\"mcp_version\":\"1.0.0\",\"server_id\":\"$MCP_SERVER_ID\",...}"; do echo "Waiting for Router..." sleep 2 done echo "Registered with Router successfully!" exec "$@"这套机制让 Server 的扩缩容完全自动化。我们线上集群有 12 个 Playwright Server 实例,每次滚动更新,新实例启动后 5 秒内自动注册,旧实例在 graceful shutdown 前自动 deregister,整个过程对 workflow 无感。
4.4 故障注入测试:用chaos-mcp模拟真实世界异常
再完美的配置,也需要在 chaos 中验证。我们开发了一个轻量级工具chaos-mcp,用于模拟 MCP 生态中最常见的 5 类故障:
| 故障类型 | 模拟命令 | 触发效果 | Router 行为 |
|---|---|---|---|
| Server 过载 | chaos-mcp overload --server-id ida-pro-01 --load 200 | /status返回pending_requests=200 | 第二层负载评估失败,Router 将其剔除 |
| Capability 缺失 | chaos-mcp disable-cap --server-id playwright-01 --cap dynamic-analysis | /connect响应中移除"dynamic-analysis" | 第一层能力匹配失败 |
| Encoding 不匹配 | chaos-mcp bad-encoding --server-id java-rest-01 --encoding binary+msgpack | /connect响应中supported_encodings包含非法值 | Router 拒绝注册,日志报Unsupported encoding |
| Network 分区 | chaos-mcp network-partition --server-id ida-pro-01 | 阻断ida-pro-01与 Router 的所有 TCP 连接 | Router 在load_sampling_interval后标记为unavailable |
| Handshake 超时 | chaos-mcp slow-handshake --server-id playwright-01 --delay 10 | /connect响应延迟 10 秒 | Router 的connect_timeout触发,Server 注册失败 |
我们每周执行一次 full chaos test suite,确保 Router 的 fallback、重试、降级逻辑在各种异常下依然健壮。这个习惯让我们在 Kali Linux MCP 渗透测试项目上线前,提前发现了max_fallback_retries=1不足以应对traffic-dataServer 的间歇性故障,及时调整为2。
5. 常见问题与排查技巧实录:来自 7 个真实项目的血泪总结
5.1 “Server 显示 ready,但 workflow 就是不调用它” —— 检查session_id的传播链
这是一个高频问题。Server 在/status返回"status": "ready",Router 日志也显示Registered server ida-pro-01,但 workflow 执行时,Router 的 debug log 显示No available servers for capabilities [disassembly]。
根因几乎总是:session_id在 workflow 的上层节点(如 LLM node)中被意外重置或丢失。LangGraph 的 MCP Router 依赖session_id做会话亲和性,如果session_id为空或为None,Router 会跳过亲和性检查,直接进入能力匹配,而此时可能因负载过高,没有 Server 通过第二层评估。
排查步骤:
- 在 Router 的
invokeendpoint 开头,添加 debug log:@app.post("/invoke") async def invoke_endpoint(request: Request): session_id = request.headers.get("x-session-id", "MISSING") logger.debug(f"Invoke received with session_id: {session_id}") # ... rest of logic - 检查上游 LLM node 的输出。我们发现,在使用
ChatPromptTemplate时,如果 template 中包含了{session_id}占位符,但实际调用时未传入session_id参数,Jinja2 会渲染为空字符串,导致x-session-idheader 为空。 - 解决方案:在 workflow 的入口 node,强制注入
session_id:from langgraph.graph import StateGraph from typing import TypedDict class GraphState(TypedDict): session_id: str # ... other fields def entry_node(state: GraphState): # 如果 state 中没有 session_id,生成一个 if not state.get("session_id"): state["session_id"] = str(uuid.uuid4()) return state
实操心得:不要相信任何“默认 session_id”。在 LangGraph 中,
session_id必须由应用层显式传递和维护。我们曾在一个禅道 MCP 项目中,因为前端未在每次 API 请求中带上x-session-id,导致 Router 认为每个请求都是新会话,亲和性失效,用户调试 session 的上下文频繁丢失。
5.2 “Playwright MCP Server 注册成功,但 invoke 时 404” —— 路径前缀的隐形战争
Playwright 的 MCP Server(如playwright-mcp)默认将所有 endpoint 挂在/下,即/connect、/invoke、/status。而 LangGraph Router 默认期望 Server 的 endpoint 以/mcp/为前缀,即/mcp/connect。当两者不一致时,Router 在注册后,会向http://playwright-server:8080/mcp/invoke发送请求,但 Server 只监听http://playwright-server:8080/invoke,结果 404。
解决方案有两种:
- Server 端适配:修改 Playwright Server 的路由前缀。在
playwright-server.js中:const app = express(); app.use('/mcp', require('./routes/mcp')); // 将所有 MCP 路由挂载到 /mcp 下 - Router 端适配:在
MCPConfig中,为每个 Server 配置server_base_url:config = MCPConfig( # ... other fields server_base_urls={ "playwright-01": "http://playwright-server:8080/", # 不加 /mcp/ "ida-pro-01": "http://ida-server:8080/mcp/" # 加 /mcp/ } )
我们选择第二种,因为它允许混合部署不同前缀的 Server,灵活性更高。但必须确保server_base_urls字典的 key 与server_id完全一致,否则 Router 无法匹配。
5.3 “UE5.8 MCP 插件连接 Router,但发送请求后无响应” —— TLS 与 HTTP/2 的无声冲突
UE5.8 的 MCP 插件默认使用 HTTPS 和 HTTP/2。而很多 LangGraph Router 的 Docker 镜像(尤其是社区版)只启用了 HTTP/1.1。当插件尝试用 HTTP/2 发起请求时,Router 的 HTTP/1.1 服务器无法解析,连接直接 reset,插件端表现为“请求发出,无任何响应”。
诊断方法:
- 在 Router 服务器上抓包:
sudo tcpdump -i any port 8000 -w router.pcap - 用 Wireshark 打开,过滤
http2,如果看到HTTP/2 HEADERS帧,但 Router 没有回复,基本确定是协议不匹配。
解决方案:
- 推荐:在 Router 前加一层 Nginx 反向代理,由 Nginx 终止 HTTPS 和 HTTP/2,再以 HTTP/1.1 转发给 Router:
upstream langgraph_router { server 127.0.0.1:8000; } server { listen 443 ssl http2; server_name mcp.example.com; ssl_certificate /etc/nginx/ssl/fullchain.pem; ssl_certificate_key /etc/nginx/ssl/privkey.pem; location / { proxy_pass http://langgraph_router; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } } - 备选:升级 Router 到支持 HTTP/2 的版本(如
langgraph-mcp>=0.4.0),但这需要重新编译,且可能引入新 bug。
注意事项:Nginx 的
proxy_http_version 1.1是必须的。如果写成1.0,会导致 streaming response 被缓冲,UE5 插件收不到流式 chunk。
5.4 “Java REST 接口转 MCP,Router 调用时报错invalid tool name” —— Tool Name 的命名规范
将现有 Java REST API 包装为 MCP Server 时,一个常见错误是直接将 REST endpoint path 作为tool_name。例如,Java API 有一个POST /api/v1/analyzeendpoint,开发者在 MCP 的tool_call中写tool_name: "analyze",结果 Router 报错Tool 'analyze' not found。
原因:MCP 的tool_name不是路径,而是 Server 在/connect响应中capabilities的映射。Router 会将tool_name与capabilities数组中的字符串进行精确匹配。所以,Server 必须在capabilities中声明"analyze",并且在/invoke处理逻辑中,显式处理