【Bug已解决】MCP error -32001 (Request timed out) when connecting Claude to Node.js MCP server 解决方案
一、现象长什么样
你把自写的 Node.js MCP server 接进 Claude,调用工具时收到:
MCP error -32001 (Request timed out);- 或
Request timed out在工具调用几秒后返回; - Claude 侧显示工具"超时",但你的 Node server 其实收到了请求、只是还没返回;
- 有时 server 完全没收到请求(卡在初始化握手阶段);
- 本地直连 server(用
mcpCLI 或 inspector)有时正常,一接 Claude 就超时; - 工具逻辑里若有阻塞同步操作(大文件读取、同步网络请求),更容易触发。
一句话:MCP 客户端(Claude)在规定时间内没收到 server 的响应,于是按 JSON-RPC 协议返回 -32001 Request timed out——本质是 server 响应太慢或根本没响应。
二、背景
MCP 基于 JSON-RPC 2.0,客户端每发一个请求(如tools/call)都期待一个响应。协议层通常有一个超时:若 server 在超时窗口内未回response(或progress),客户端就主动判定Request timed out(-32001)。
Node.js MCP server 常见超时来源:
- 初始化握手慢:server 启动要做重活(加载大模型、连库),
initialize阶段就超时; - 工具调用是同步阻塞:在
request处理器里写了fs.readFileSync大文件、或axios同步等待外部 API,事件循环被卡住,响应发不出去; - 忘了
await/ 返回 Promise:handler 里漏了return,客户端永远等不到结果; - server 抛错但没回复错误响应:异常未被捕获,请求悬空。
三、根因
根因是server 在超时窗口内未能返回合法的 JSON-RPC 响应:
// 伪代码:问题 handler server.setRequestHandler(CallToolRequestSchema, async (req) => { // 同步阻塞:事件循环卡住,超时 const data = fs.readFileSync("/huge/file"); // 阻塞 // 或:漏了 return / await doSomethingAsync(req); // 没 return,客户端等不到 response });修复方向:把阻塞操作改成异步、确保 handler 一定return一个响应、给慢操作加进度通知(progress)、或在客户端侧调大超时。
四、最小可运行复现
下面用 Node 模拟"handler 忘记 return,导致请求永不响应":
// 用 @modelcontextprotocol/sdk 的简化示意 const handlers = {}; function register(name, fn) { handlers[name] = fn; } // bug:async 里没 return register("tools/call", async (req) => { await new Promise(r => setTimeout(r, 100)); // 漏了 return 响应 -> 客户端超时 const result = { content: [{ type: "text", text: "done" }] }; // 应该 return result; 但忘了 }); // 模拟客户端等待 + 超时判定 function clientCall(timeoutMs) { return new Promise((resolve, reject) => { const t = setTimeout(() => reject(new Error("MCP error -32001 (Request timed out)")), timeoutMs); Promise.resolve(handlers["tools/call"]({})).then(res => { clearTimeout(t); resolve(res); }); }); } clientCall(50).catch(e => console.log("ERR:", e.message));运行后因为 handler 没返回,客户端在 50ms 后抛-32001 Request timed out。
五、解决方案(第一层:最小直接修复)
最小修复:确保 handler异步化且一定返回响应,并给客户端合理超时:
import { readFile } from "fs/promises"; server.setRequestHandler(CallToolRequestSchema, async (req) => { // 1. 用异步 API,不阻塞事件循环 const data = await readFile("/huge/file", "utf8"); // 2. 务必 return 响应 return { content: [{ type: "text", text: String(data.length) }], }; });客户端侧(如 Claude Desktop 配置)若 server 确实慢,可确认是否有超时配置可调;部分 MCP 客户端支持设置更长超时。同时,在慢操作里发进度通知:
await server.sendProgress({ progressToken: req.meta?.progressToken, progress: 0.5, total: 1 });六、解决方案(第二层:结构化改进)
把"工具调用超时治理"抽成策略,集中管理超时、异步化、以及必须返回响应:
from dataclasses import dataclass, field from typing import Awaitable, Callable, Dict, Any @dataclass(frozen=True) class McpTimeout32001Policy: """MCP 工具调用策略:防 -32001 超时。 规则: - 每个 handler 必须返回响应(禁止漏 return) - 阻塞操作必须异步化(调用方用 asyncio.to_thread) - 慢操作发进度通知,重置客户端超时预期 """ client_timeout_ms: int = 10_000 async def run_handler( self, handler: Callable[[Dict], Awaitable[Dict]], req: Dict, slow_work: Callable[[], Any] = None, ) -> Dict: # 若有阻塞工作,丢到线程池,避免卡事件循环 import asyncio if slow_work is not None: await asyncio.to_thread(slow_work) result = await handler(req) if not isinstance(result, dict) or "content" not in result: raise ValueError("handler 必须返回含 content 的响应对象") return result def ensure_returns(self, handler) -> Callable: async def wrapped(req): res = await handler(req) assert res is not None, "handler 禁止返回 None(会导致 -32001)" return res return wrapped def demo() -> None: policy = McpTimeout32001Policy() wrapped = policy.ensure_returns(lambda req: {"content": [{"type": "text", "text": "ok"}]}) import asyncio print(asyncio.run(policy.run_handler(wrapped, {}))) if __name__ == "__main__": demo()七、解决方案(第三层:断言 / CI 守护)
import asyncio import pytest from your_module import McpTimeout32001Policy def test_handler_must_return_content(): policy = McpTimeout32001Policy() async def bad(req): return {"no_content": 1} with pytest.raises(ValueError): asyncio.run(policy.run_handler(bad, {})) def test_handler_none_rejected(): policy = McpTimeout32001Policy() wrapped = policy.ensure_returns(lambda req: None) with pytest.raises(AssertionError): asyncio.run(wrapped({})) def test_valid_handler_ok(): policy = McpTimeout32001Policy() async def good(req): return {"content": [{"type": "text", "text": "ok"}]} res = asyncio.run(policy.run_handler(good, {})) assert res["content"][0]["text"] == "ok" def test_slow_work_offloaded(): import time policy = McpTimeout32001Policy() def blocking(): time.sleep(0.01) async def h(req): return {"content": [{"type": "text", "text": "done"}]} res = asyncio.run(policy.run_handler(h, {}, slow_work=blocking)) assert res["content"]把这些测试加进 MCP server 的 CI,确保每个工具 handler 都返回合规响应,从根上杜绝 -32001。
八、排查清单
- server 是否收到了请求?看 server 日志,没收到说明卡在初始化握手。
- handler 里是否有同步阻塞(readFileSync / 同步网络)?改成异步。
- handler 是否
return了响应?漏 return 是超时头号原因。 - 是否抛了异常却没回复错误响应?异常要转成 JSON-RPC error 返回。
- 慢操作是否发了 progress 通知?没进度客户端会判超时。
- 客户端超时是否可调大?确认 MCP 客户端配置。
- 是否用 inspector 单独测过 server?先排除 server 自身问题。
九、小结
MCP error -32001 (Request timed out)是客户端在超时窗口内没收到 server 的 JSON-RPC 响应。根因几乎都在 server 侧:handler 同步阻塞、漏了return、异常未回复、或初始化过慢。最小修复是异步化阻塞操作、确保 handler 一定返回含content的响应、给慢操作发 progress;结构化做法是抽成McpTimeout32001Policy,集中治理超时与响应合规;最后用 pytest 守护"handler 必须返回合规响应",从根上消除 -32001。