Skip to content

[Bug] HTTP 型 MCP 的工具调用被连接超时(10s)截断,慢工具必然失败 / HTTP MCP tool calls are cut off by the connect timeout (10s) #1007

Description

@lowlandsheperd

What happened? / 问题描述

HTTP(streamable HTTP)类型的 MCP 服务器,当单个工具调用耗时超过 10 秒时,调用必定被中断,报 This operation was aborted(AbortError,errorCode: 20),而不是预期的 MCP_CALL_TIMEOUT_MS(100s)。

问题不在于「缺少可配置的超时」,而在于连接超时被误用为每次请求的中断上限——即代码没有按其自身设计运行:

apps/desktop/electron/main/plugin-mcp.ts

  • :10 MCP_CONNECT_TIMEOUT_MS = 10_000
  • :12 MCP_CALL_TIMEOUT_MS = 100_000
  • :497-498 handshake() 里取的是连接超时,并把它传进 createTransport():
    const timeoutMs = this.opts.connectTimeoutMs ?? MCP_CONNECT_TIMEOUT_MS;  // 10s
    const transport = this.createTransport(timeoutMs);
  • :567 createTransport() 再把它交给 HTTP 传输层作为 timeoutMs
  • :304 HTTP 传输层用它给每一次 send() 挂 abort 定时器:
    const timer = setTimeout(() => controller.abort(), options.timeoutMs);

因此 tools/call 的 HTTP 请求在 10 秒被 fetch abort。而 callTool() 在 :475 传入的 callTimeoutMs ?? MCP_CALL_TIMEOUT_MS(100s)只挂在 JSON-RPC 层的 Promise 上(:649-652),永远来不及触发——fetch 先抛 AbortError,request() 在 :654-660 把它 reject 出去,错误类型也随之退化成 abort 而非 TIMEOUT。

stdio 型 MCP 不受影响,可作对照证据:createStdioTransport(:129,由 :551-561 调用)不接收 timeoutMs,也没有 abort 定时器,其 tools/call 只受 JSON-RPC 层 100s 约束——这正是设计意图。HTTP 是唯一偏离的路径。

附带一个同类问题:tools/list 每页也被 10s 卡住(:511 传入 timeoutMs,:605-609 用 Math.min(timeoutMs, remaining)),虽然 :589 的整体 discovery deadline 是 30s。

影响面

只有单个工具执行 > 10s 才会暴露,因此触发面窄、不易被发现。本例中 tavily_research 实测 39.45s,在该 MCP 服务器上必然失败;其余四个工具(1.2s / 9.3s / 2.3s / 3.6s)均正常,形成清晰对照。

Steps to reproduce / 复现步骤

  1. 从冷启动打开应用,配置一个 HTTP 型 MCP 服务器,且其中至少一个工具的响应时间 > 10 秒。
    • 可复现的最小样本:任意对 tools/call 延迟 12 秒再返回的 HTTP MCP 端点。
    • 本报告使用的真实样本:某远程 HTTP MCP(https://<private-mcp-endpoint>/mcp),其中 tavily_research 约 39 秒。
  2. 在 Agent 模式下让模型调用该慢工具(工具通过 ToolSearch 按需激活,无需特殊设置)。
  3. 观察:约 10 秒后调用失败;同一服务器上的快工具仍然可用。

补充:用直连 MCP 端点的方式(initialize → tools/list → tools/call,超时放宽到 240s)调用同一个 tavily_research,39.45 秒正常返回结果,证明服务端与凭据都没有问题。

Expected behavior / 预期行为

单次 tools/call 应受 MCP_CALL_TIMEOUT_MS(100s)约束。耗时 39 秒的工具调用应正常返回;超过 100 秒才应失败,且错误类型应为 TIMEOUT(mcp tools/call timed out after 100000ms),而非 fetch 的 AbortError。


Actual behavior / 实际行为

调用在 10 秒被中断,且错误类型退化为 AbortError:

{"api":"mcp.call","ok":false,"serverId":"<server-id>","transport":"http",
 "tool":"tavily_research","durationMs":10006,"errorCode":20}

message: "This operation was aborted"。

因为抛的不是 TIMEOUT,使用者无法从错误本身区分「超时」与「服务器连接中断」,排查成本被抬高。

App version / 应用版本

0.15.6

Operating system / 操作系统

Windows

Extra environment / 其他环境信息

Windows 11 Pro (build 26200) · x64 (12th Gen Intel Core i7-12700)
protocol 11 · host 0.15.6 · NSIS 安装(%LOCALAPPDATA%\Programs\PI-Desktop)
MCP transport: http (streamable HTTP) · 该服务器注册 5 个工具

Logs / 日志

同一次会话、同一个 HTTP MCP 服务器——前四个工具成功,第五个在 10 秒被截断:


{"api":"mcp.connect","ok":true,"serverId":"<server-id>","transport":"http","toolCount":5}
{"api":"mcp.call","ok":true, "tool":"tavily_search",   "durationMs":1220}
{"api":"mcp.call","ok":true, "tool":"tavily_extract",  "durationMs":9301}
{"api":"mcp.call","ok":true, "tool":"tavily_map",      "durationMs":2267}
{"api":"mcp.call","ok":true, "tool":"tavily_crawl",    "durationMs":3602}
{"api":"mcp.call","ok":false,"tool":"tavily_research", "durationMs":10006,"errorCode":20,
 "message":"This operation was aborted"}


注意 `durationMs: 10006` 与 `MCP_CONNECT_TIMEOUT_MS = 10_000` 的吻合,以及其余工具全部低于 10s——这排除了服务端或网络问题。

Screenshots / 截图

附:修复方向建议(仅供参考,供维护者判断)

原则应是按请求语义分派超时,而不是让传输层继承连接超时:

  1. McpTransport.send 增加可选的 per-request 超时参数,HTTP 传输层用它(connectTimeoutMs 仅作兜底)。
  2. request() 把 timeoutMs 下传给 send(),使 JSON-RPC 定时器与传输层 abort 同源同值——谁先到都报同一个 TIMEOUT,不再退化成 AbortError。
  3. 各调用点用自己的预算:initialize → connectTimeoutMs(10s);tools/list 每页 → 受 30s discovery deadline 约束;tools/call → callTimeoutMs(100s)。

这样修完,慢工具的默认行为即恢复为设计值 100s,无需新增任何配置项。

若后续希望开放该超时可调,需注意既有上限不变式:
host-core DESKTOP_TOOL_DISPATCH_TIMEOUT_MS = 150s(crates/host-core/src/tools/mod.rs:966-976)必须包住内层全部预算
(PLUGIN_TOOL_TIMEOUT_MS = 110s,以及 MCP 最宽 leg = 10s + 30s + 100s = 140s)。
因此 callTimeoutMs 若对外开放,上限不能超过 100s,否则 host-core 会先报通用 TOOL_TIMEOUT,
内层永远报不出自己的错误——那会比当前这个 bug 更难诊断。

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions