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 / 复现步骤
- 从冷启动打开应用,配置一个 HTTP 型 MCP 服务器,且其中至少一个工具的响应时间 > 10 秒。
- 可复现的最小样本:任意对
tools/call 延迟 12 秒再返回的 HTTP MCP 端点。
- 本报告使用的真实样本:某远程 HTTP MCP(
https://<private-mcp-endpoint>/mcp),其中 tavily_research 约 39 秒。
- 在 Agent 模式下让模型调用该慢工具(工具通过
ToolSearch 按需激活,无需特殊设置)。
- 观察:约 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 / 截图
附:修复方向建议(仅供参考,供维护者判断)
原则应是按请求语义分派超时,而不是让传输层继承连接超时:
McpTransport.send 增加可选的 per-request 超时参数,HTTP 传输层用它(connectTimeoutMs 仅作兜底)。
request() 把 timeoutMs 下传给 send(),使 JSON-RPC 定时器与传输层 abort 同源同值——谁先到都报同一个 TIMEOUT,不再退化成 AbortError。
- 各调用点用自己的预算:
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 更难诊断。
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:10MCP_CONNECT_TIMEOUT_MS = 10_000:12MCP_CALL_TIMEOUT_MS = 100_000:497-498handshake()里取的是连接超时,并把它传进createTransport()::567createTransport()再把它交给 HTTP 传输层作为timeoutMs:304HTTP 传输层用它给每一次send()挂 abort 定时器:因此
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 / 复现步骤
tools/call延迟 12 秒再返回的 HTTP MCP 端点。https://<private-mcp-endpoint>/mcp),其中tavily_research约 39 秒。ToolSearch按需激活,无需特殊设置)。补充:用直连 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 / 截图
附:修复方向建议(仅供参考,供维护者判断)
原则应是按请求语义分派超时,而不是让传输层继承连接超时:
McpTransport.send增加可选的 per-request 超时参数,HTTP 传输层用它(connectTimeoutMs仅作兜底)。request()把timeoutMs下传给send(),使 JSON-RPC 定时器与传输层 abort 同源同值——谁先到都报同一个TIMEOUT,不再退化成 AbortError。initialize→connectTimeoutMs(10s);tools/list每页 → 受 30s discovery deadline 约束;tools/call→callTimeoutMs(100s)。这样修完,慢工具的默认行为即恢复为设计值 100s,无需新增任何配置项。