Skip to content

Undici HTTP/2 下,独立 SSE 流占用调度槽位导致 Streamable HTTP MCP 启动超时 #1942

Description

@ntr5uki

What version of Kimi Code is running?

0.27.0

Which open platform/subscription were you using?

Kimi Code (OAuth)

Which model were you using?

No response

What platform is your computer?

Linux 7.1.3-2-cachyos x86_64 unknown

What issue are you seeing?

当 Kimi Code 连接一个满足以下条件的 Streamable HTTP MCP 服务器时,MCP 初始化必然超时:

  • HTTPS 连接通过 ALPN 协商为 HTTP/2;
  • 服务器支持独立的 GET SSE 流;
  • GET SSE 流在连接期间保持打开。

Kimi Code 最终报告:

ERROR mcp server unavailable server=FNS transport=http status=failed reason="Timed out after 30000ms"

服务器本身工作正常。以下客户端或实现连接同一端点时,完整握手均可在约 0.4 秒内完成:

initialize
→ notifications/initialized
→ tools/list

已验证可正常工作的客户端包括:

  • curl
  • node:http
  • node:http2
  • Codex CLI / Rust rmcp

What steps can reproduce the bug?

Kimi Code 的 HTTP MCP transport 使用官方 TypeScript MCP SDK的 StreamableHTTPClientTransport,默认调用 Node.js 全局 fetch。

初始化流程如下:

  1. POST /api/mcp:initialize 成功;
  2. POST /api/mcp:notifications/initialized 返回 202;
  3. 客户端打开独立的 GET /api/mcp SSE 流;
  4. 客户端发送 POST /api/mcp:tools/list;
  5. tools/list 请求一直等待,最终触发 30 秒启动超时。

该行为仅在连接实际协商为 HTTP/2 且独立 SSE 流保持打开时出现。

定位结果

通过 Undici 的 diagnostics_channel 观察到:

undici:request:create

会正常触发,说明 tools/list 请求已经创建。

但是后续:

undici:client:sendHeaders

始终没有触发,说明请求没有真正写入 HTTP/2 连接,服务端也没有收到对应的 HEADERS 帧。

此时已经存在一个长期保持打开的 GET SSE 流。后续 POST 请求被创建,但没有获得派发机会,最终一直等待至 Kimi Code 的 startupTimeoutMs 超时。

该现象与 Undici 的 HTTP/2并发派发行为高度相关,也与已有的 Undici #4143 (nodejs/undici#4143) 类似:HTTP/2 连接的有效并发派发能力可能受客户端配置或默认值限制,导致长驻 SSE 流占用唯一调度槽位。

对比实验

禁用 HTTP/2

new Agent({
allowH2: false,
});

结果:

  • 使用 HTTP/1.1;
  • GET SSE 保持打开;
  • tools/list 仍可正常派发;
  • 约 0.4 秒返回 200;
  • Kimi Code 成功加载全部 26 个 MCP 工具。

启用 HTTP/2

new Agent({
allowH2: true,
});

结果:

  • 通过 ALPN 协商 HTTP/2;
  • GET SSE 保持打开后,后续 POST 不再派发;
  • undici:request:create 触发;
  • undici:client:sendHeaders 不触发;
  • Kimi Code 在约 30 秒后启动超时。

排除服务端问题

使用原生 node:http2 客户端,在同一个 HTTP/2 session 上并发执行:

GET SSE
POST tools/list

两条 stream 均正常工作。

服务端发送的 HTTP/2 设置包括:

SETTINGS_MAX_CONCURRENT_STREAMS = 250

因此服务端允许多个并发 HTTP/2 stream,问题发生在 Undici 客户端派发请求之前。

最小复现

该问题可以通过一个与 MCP 服务器实现无关的最小示例复现:

  • 本地 node:http2 服务器;
  • 一个保持打开的 SSE GET 端点;
  • 同源 POST 端点;
  • Undici fetch 客户端;
  • 约 50 行代码。

在 GET SSE 流保持打开期间,Undici 创建 POST 请求但不发送 HEADERS。

最小复现脚本可以随 Issue 一并提供。

预期行为

HTTP/2 允许在同一连接中并发多个 stream。

当独立 GET SSE 流保持打开时,Undici 仍应正常派发后续的 POST 请求:

HTTP/2 connection
├── GET /api/mcp → 长驻 SSE stream
└── POST /api/mcp → tools/list

长驻 SSE stream 不应阻止其他 HTTP/2 stream 创建和发送。

实际行为

HTTP/2 connection
├── GET /api/mcp → 长驻 SSE stream
└── POST /api/mcp → 请求已创建,但 HEADERS 从未发送

Kimi Code 随后因 tools/list 无法完成而触发启动超时。

建议修复

作为兼容性修复,建议为 MCP HTTP transport 使用独立的 Undici dispatcher,并显式禁用 HTTP/2:

import {
Agent,
fetch as undiciFetch,
} from "undici";

const mcpDispatcher = new Agent({
allowH2: false,
});

const mcpFetch = (input, init) =>
undiciFetch(input, {
...init,
dispatcher: mcpDispatcher,
});

const transport = new StreamableHTTPClientTransport(url, {
fetch: mcpFetch,
});

关闭 MCP transport 时,还应关闭对应 dispatcher,避免连接资源泄漏:

await mcpDispatcher.close();

如果还需要支持旧版 SSEClientTransport,建议让它使用相同的自定义 fetch。

禁用 HTTP/2 会失去多路复用并使用额外的 HTTP/1.1 连接,但对于目前 MCP 的请求规模,这是一个可接受且已验证有效的兼容性方案。

另一种可能的修复方向,是确保 Undici 的 HTTP/2 dispatcher 允许至少两个并发 stream。不过该配置在不同 Undici 版本中的行为存在差异,禁用 H2 是目前验证最充分的绕行方案。

用户侧临时绕行

临时使用一个不发布 HTTP/2 ALPN、仅提供 HTTP/1.1 的 HTTPS 入口。

只有在 MCP 后端位于 localhost 或完全可信的内部网络时,才建议直接使用纯 HTTP 地址。远程 MCP 通常携带认证信息,不应通过不可信网络明文传输。

使用 HTTP/1.1 入口后已验证:

  • Kimi Code 会话日志中不再出现 MCP 启动错误;
  • FNS 的 26 个工具全部正常加载。

What is the expected behavior?

No response

Additional information

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions