Envoy MCP JSON REST Bridge:为 headers-only 上游响应合成 JSON-RPC 响应体的修复解析
本篇文章聚焦 Envoy mcp_json_rest_bridge 过滤器(HTTP Filter)的一个关键 Bug 修复:当 REST 上游返回无响应体的 headers-only 响应(如 HTTP 204 No Content)时,过滤器会主动合成合法的 JSON-RPC 响应,避免 MCP 客户端(MCP SDK)因等待响应体而超时或抛出异常。读完本文,你将理解该 Bug 的成因、过滤器在响应编码链路上的合成逻辑与状态码改写规则,以及对应的单元测试与端到端集成测试如何验证这一行为。
问题背景:headers-only 响应如何击穿 MCP 传输层
Model Context Protocol(MCP)基于 JSON-RPC 2.0 通信,客户端发出 tools/call、tools/list 等请求后,会严格按照 JSON-RPC 响应结构(jsonrpc、id、result/error 字段)解析返回值。而 mcp_json_rest_bridge 过滤器的作用,是把 MCP JSON-RPC 请求转码为普通 HTTP REST 请求转发给上游后端,再把上游的 JSON REST 响应映射回 JSON-RPC 格式。
问题出在:并非所有 REST 后端都会返回响应体。例如:
- 删除资源、触发任务、更新状态等操作,后端常返回
HTTP 204 No Content; - 部分网关或中间层也会对 GET 类操作返回空 body。
当这类 headers-only 响应(end_stream 为 true 且无任何数据帧)被原样透传给 MCP 客户端时,客户端拿不到 JSON-RPC 响应体,会一直等待导致超时,或直接因解析失败抛出异常。这正是本 changelog 记录的核心缺陷:
mcp_json_rest_bridge: Fixed a bug where headers-only upstream responses (e.g., HTTP 204 No Content) were passed through to MCP clients without a JSON-RPC response body, causing MCP SDK timeouts or exceptions.
修复方案是让过滤器在响应编码阶段主动合成一个合法的 JSON-RPC 响应:对 tools/call 请求合成空 ToolResult,对 tools/list 请求合成服务器错误。
修复实现:encodeHeaders 中的合成逻辑
该修复位于过滤器核心实现 mcp_json_rest_bridge_filter.cc 的 McpJsonRestBridgeFilter::encodeHeaders 方法中(约第 621-654 行)。该方法在响应头到达过滤器时被调用,end_stream 参数为 true 意味着上游已发送全部内容(即 headers-only,没有 body)。
对 tools/call 合成空 ToolResult
当 mcp_operation_ 为 ToolsCall 且响应为 headers-only 时,过滤器调用 translateJsonRestResponseToJsonRpc(同文件第 77-90 行)合成响应:
if (mcp_operation_ == McpOperation::ToolsCall) {
synthetic = translateJsonRestResponseToJsonRpc("", *session_id_, is_error).dump();
}
其中 is_error = response_code >= 400(400 及以上视为错误)。空 ToolResult 的实际 JSON 结构如下:
{
"jsonrpc": "2.0",
"id": 7,
"result": {
"content": [{"type": "text", "text": ""}],
"isError": false
}
}
translateJsonRestResponseToJsonRpc 的实现把上游 body 文本包装进 result.content[0].text,并携带 isError 标记(mcp_json_rest_bridge_filter.cc 第 77-90 行);此处传入空字符串,即表示“操作成功执行但没有返回内容”,符合 MCP 规范对工具调用结果的定义。
对 tools/list 合成服务器错误
当 mcp_operation_ 为 ToolsList 且响应为 headers-only 时,说明后端没能返回工具列表,过滤器直接合成一个 JSON-RPC 错误响应:
} else if (mcp_operation_ == McpOperation::ToolsList) {
// headers-only means no tools list is available; return a server error.
json ret = {
{McpConstants::JSONRPC_FIELD, McpConstants::JSONRPC_VERSION},
{McpConstants::ID_FIELD, *session_id_},
{McpConstants::ERROR_FIELD, generateErrorJsonResponse(-32000, "Server error")},
};
synthetic = ret.dump();
}
generateErrorJsonResponse(同文件第 115-120 行)生成 {"code": -32000, "message": "Server error"},其中 -32000 是 MCP 规范定义的“服务器端错误”保留错误码。合成后的完整响应为:
{"jsonrpc":"2.0","id":9,"error":{"code":-32000,"message":"Server error"}}
统一的状态码改写与响应头处理
无论合成哪种响应,过滤器都会执行统一的收尾逻辑(第 637-653 行):
- 状态码改写为 200:除
401 Unauthorized与403 Forbidden外,其余非 200 状态码一律改写为200 OK。原因是 MCP 客户端在传输层对非 200 响应会直接失败,而401/403按 MCP 授权规范必须保留,以驱动 OAuth 握手与 step-up scope 流程(源码注释引用了 MCP 2025-11-25 规范的 authorization error handling 章节)。 - 关于 400 的处理:即使 MCP 授权规范中也提到 HTTP 400,源码注释明确说明其假设——授权检查(如 OAuth token 校验)发生在本过滤器之前的过滤器链上,因此来自 REST 后端的 400 一律视为 API 错误,被转成标准 JSON-RPC 错误而非透传。
- 设置响应头:
Content-Type强制为application/json,并写入合成 body 的精确Content-Length,保证 MCP 客户端按长度读取完整 body。 - 注入合成 body:通过
encoder_callbacks_->addEncodedData(body_buffer, false)把合成的 JSON 字节注入编码数据流(第 651-652 行)。
测试验证:单元测试与集成测试的双重覆盖
该修复在仓库中有完整的测试支撑,分为两个层级。
过滤器单元测试
单元测试位于 mcp_json_rest_bridge_filter_test.cc,覆盖三类场景:
| 测试用例 | 上游响应 | 期望输出 |
|---|---|---|
ToolCallHeadersOnly204EmitsSyntheticSuccessResult(L2213) |
204,tools/call |
{"id":7,"jsonrpc":"2.0","result":{"content":[{"text":"","type":"text"}],"isError":false}},状态改写为 200 |
ToolCallHeadersOnly5xxEmitsSyntheticErrorResult(L2239) |
503,tools/call |
同上但 isError:true,状态改写为 200 |
ToolsListHeadersOnly204EmitsSyntheticServerError(L2265) |
204,tools/list |
{"error":{"code":-32000,"message":"Server error"},"id":9,"jsonrpc":"2.0"},状态改写为 200 |
以第一个用例为例,测试先构造 tools/call 请求并断言 decodeHeaders 返回 StopIteration,随后以 :status 204 构造响应头并调用 encodeHeaders(response_headers_, true),最终断言:
- 编码数据与期望的 JSON-RPC 结构完全一致(
addEncodedData被调用一次); Content-Type为application/json;Content-Length等于合成 body 的精确长度;- 响应状态码为
200。
第三个用例(L2265)则验证 tools/list 场景不会透传 204 空响应,而是合成带 -32000 错误码的 JSON-RPC 错误对象。
端到端集成测试
集成测试位于 mcp_json_rest_bridge_integration_test.cc,基于 HttpIntegrationTest 启动真实 Envoy 与上游模拟服务器,验证从下游 MCP 客户端到上游 REST 后端再到客户端的完整链路:
ToolsCallHeadersOnly204SyntheticSuccessResult(L2146):配置工具create_api_key(POST /v1/{parent=projects/*}/keys,body 为key),上游收到转码后的POST /v1/projects/foo/keys请求后,用encodeHeaders(headers, true)发送204headers-only 响应;下游最终收到200+application/json+ 精确Content-Length,body 为{"jsonrpc":"2.0","id":7,"result":{"content":[{"type":"text","text":""}],"isError":false}}。ToolsCallHeadersOnly5xxSyntheticErrorResult(L2212):上游返回503,合成结果isError:true,状态码同样被改写为200。ToolsListHeadersOnly204SyntheticServerError(L2342):配置tool_list_http_rule: get: "/v1/tools",上游确认收到转码后的GET /v1/tools请求并返回204,下游收到{"jsonrpc":"2.0","id":9,"error":{"code":-32000,"message":"Server error"}}。
集成测试同时验证了转码的正确性(路径、方法、Content-Length、Content-Type)与合成响应的完备性,确保该修复在真实 HTTP/2 连接上成立。
触发合成的完整链路与相关配置
理解修复位置后,再看整个过滤器的工作链路,可以更清楚合成逻辑发生在哪一步。过滤器定义在 mcp_json_rest_bridge.proto(扩展名 envoy.filters.http.mcp_json_rest_bridge,注册于 well_known_names.h),其核心流程为:
decodeHeaders:仅接受POST方法(其他方法直接返回405 Method Not Allowed),并根据 host/path 是否命中配置的 endpoint 决定是否进入 MCP 处理(mcp_json_rest_bridge_filter.cc第 476-526 行);decodeData:缓冲并解析请求体 JSON,按方法分派到handleMcpMethod——tools/call通过 http_request_builder.cc 依据HttpRule构建 REST 请求(路径模板、query 参数、body 映射),tools/list则按tool_list_http_rule转码为 GET 请求(第 865-992 行);encodeHeaders:此处即本次修复的合成逻辑——非 streaming 模式下若end_stream为 true,说明上游没有 body,立即合成 JSON-RPC 响应(第 621-654 行);encodeData/encodeTrailers:对存在 body 的正常响应做 JSON-RPC 转码(encodeJsonRpcData,第 994-1104 行)。
此外需注意,合成逻辑只针对非流式(text_content_streaming_enabled 未开启)路径;流式模式下 encodeHeaders 走的是另一条分支,预构建 JSON-RPC 前缀/后缀并剥离 Content-Length(第 602-619 行),不在此次修复范围内。
运维建议与适用前提
- 若你的 REST 后端存在“成功但无返回体”的接口(典型如
204 No Content、201 Created带空 body),升级到包含此修复的 Envoy 版本后,MCP 客户端将不再因等待 body 而超时,tools/call会得到isError:false的空结果,tools/list会得到明确的-32000服务器错误。 - 若某工具的操作语义是“必须返回内容才算成功”,可结合 proto 配置中的
max_response_body_size、HttpRule映射以及过滤器动态元数据(request_storage_mode: DYNAMIC_METADATA,可记录bridge_status、method、params、backend_response_code等字段,见 mcp_json_rest_bridge_filter.cc 第 1242-1296 行)来观测此类合成响应,判断后端行为是否需要调整。 - 状态码改写规则(除 401/403 外一律 200)是 MCP 传输层的通用约束,运维时应确保授权类检查放在本过滤器之前的过滤器链中,以配合 400/401/403 语义的正确分流。
- 若需自定义
tools/list的返回行为(而非在 204 时返回服务器错误),可配置tool_list_local让过滤器本地生成工具列表,或配置tool_list_http_rule将请求转码给后端——详见 mcp_json_rest_bridge.proto 中ServerToolConfig的说明。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust4.21 K637- DDeepSeek-V4.1-FlashDeepSeek-V4.1-Flash 是一个多模态混合专家(MoE)模型,拥有 5520 亿骨干参数,并支持最多一百万 token 的上下文长度。该模型原生支持图像和文本输入,并以自回归方式生成文本Python270
cherry-studio🍒 Cherry Studio 是一款支持多个 LLM 提供商的桌面客户端TypeScript2 K146
hello-agents📚 《从零开始构建智能体》——从零开始的智能体原理与实践教程Python46066
new-apiAI模型聚合管理中转分发系统,一个应用管理您的所有AI模型,支持将多种大模型转为统一格式调用,支持OpenAI、Claude、Gemini等格式,可供个人或者企业内部管理与分发渠道使用。🍥 A Unified AI Model Management & Distribution System. Aggregate all your LLMs into one app and access them via an OpenAI-compatible API, with native support for Claude (Messages) and Gemini formats.Go20143
JeecgBoot🔥企业级低代码平台集成了AI应用平台,帮助企业快速实现低代码开发和构建AI应用!前后端分离架构 SpringBoot,SpringCloud、Mybatis,Ant Design4、 Vue3.0、TS+vite!强大的代码生成器让前后端代码一键生成,无需写任何代码! 引领AI低代码开发模式: AI生成->OnlineCoding-> 代码生成-> 手工MERGE,显著的提高效率,又不失灵活~Java34051