【Bug已解决】Model Context Protocol (MCP) server exits unexpectedly after initial response when integrating with Claude desktop 解决方案
一、现象长什么样
你的 MCP server 接进 Claude Desktop 后,第一次响应正常,之后突然退出:
MCP server exits unexpectedly after initial response;- 或客户端报 server 断开、工具变灰;
- server 启动后能正确响应第一、二个请求,随后进程消失;
- 你没主动退出,日志里也没有明显报错(或最后一行是正常响应);
- 本地用
mcpinspector 单测时反而稳定,一接 Claude Desktop 就退出; - 有时退出发生在某次特定工具调用之后。
一句话:MCP server 进程在处理完初始响应后意外终止——通常是 stdin 被关闭/EOF、未捕获的异常让事件循环停下、或 server 在响应后错误地调用了process.exit/返回后主协程结束,导致进程退出。
二、背景
MCP stdio server 是长驻进程:它启动后通过 stdin 读请求、stdout 写响应,应该一直活着直到客户端关闭。任何"响应完就结束进程"的逻辑都是错的。
常见退出诱因:
- stdin EOF 误判:代码里监听 stdin 的
end事件,一旦某次读取边界处理不当就process.exit;或用了会关闭 stdin 的读取方式; - 未捕获异常:某个工具 handler 抛了没 catch 的错,Node 默认会让事件循环退出(尤其在
unhandledRejection未被监听时); - 主函数返回即退出:
async main()处理完一个请求就 return,事件循环空了进程退; - transport 配置错:用了一次性 request/response transport 而非持续监听的 stdio server。
Claude Desktop 的 inspector 单测有时用不同的生命周期管理,所以"本地稳、接客户端退"。
三、根因
根因是server 没有保持长驻,或在异常/EOF 时退出进程:
// 错误:监听 stdin end 就退出 process.stdin.on("end", () => process.exit(0)); // 错误:未捕获异常 -> 事件循环退出 server.setRequestHandler(SomeSchema, (req) => { throw new Error("boom"); // 未 catch -> 进程退出 });修复方向:用官方 SDK 的server.connect(stdio())保持监听、不要主动 exit、给unhandledRejection加监听兜底。
四、最小可运行复现
下面用 Python 模拟"处理完就退出"与"长驻"的区别:
from dataclasses import dataclass import sys @dataclass class _McpLifecycle: keep_alive: bool = False def serve(self) -> str: # 模拟读一个请求并响应 request = sys.stdin.readline() response = '{"jsonrpc":"2.0","result":"ok"}' sys.stdout.write(response + "\n") sys.stdout.flush() if not self.keep_alive: return "exit" # 错误:响应完就退出 return "keep_listening" # 正确:继续监听 def main(): bad = _McpLifecycle(keep_alive=False) print("bad serve ->", bad.serve()) # exit(客户端会断开) good = _McpLifecycle(keep_alive=True) print("good serve ->", good.serve()) # keep_listening if __name__ == "__main__": main()真实 Node server 里,若 handler 抛未捕获异常或 main 提前 return,进程就会在初始响应后退出。
五、解决方案(第一层:最小直接修复)
最小修复是让 server 长驻 + 兜底未捕获异常:
import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; const server = new Server({ name: "my-server", version: "1.0.0" }, { capabilities: {} }); // 1. 所有 handler 必须 catch,绝不让异常冒泡退出进程 server.setRequestHandler(SomeToolSchema, async (req) => { try { return await doWork(req); } catch (e) { return { content: [{ type: "text", text: String(e) }], isError: true }; } }); // 2. 兜底未捕获异常,避免进程退出 process.on("unhandledRejection", (e) => console.error("unhandledRejection:", e)); process.on("uncaughtException", (e) => console.error("uncaughtException:", e)); // 3. 用 stdio transport 长驻,不要主动 process.exit const transport = new StdioServerTransport(); await server.connect(transport); // 不要在这里 return/exit,保持监听注意:把错误作为isError: true的响应返回,而不是抛出——这样客户端能收到错误信息,server 也不退出。
六、解决方案(第二层:结构化改进)
把"server 生命周期纪律"做成策略,集中约束:长驻、异常兜底、不主动退出:
from dataclasses import dataclass import sys from typing import Callable @dataclass(frozen=True) class McpServerExitPolicy: """MCP server 生命周期策略:保持长驻,异常不退出进程。 规则: - 处理完请求必须继续监听,绝不主动 exit - 任何 handler 异常必须转为错误响应,不得上抛 - 提供 '安全包装' 把 handler 包成不退出版本 """ def safe_handler(self, handler: Callable) -> Callable: async def wrapped(req): try: return await handler(req) except Exception as e: # 转成错误响应,进程存活 return {"content": [{"type": "text", "text": str(e)}], "isError": True} return wrapped def assert_keep_alive(self, would_exit: bool) -> None: if would_exit: raise RuntimeError("server 不应在响应后退出:必须保持长驻监听") def demo() -> None: policy = McpServerExitPolicy() async def boom(req): raise ValueError("boom") wrapped = policy.safe_handler(boom) import asyncio res = asyncio.run(wrapped({})) assert res["isError"] is True # 错误转响应,进程不退出 print("handler 安全包装 OK") if __name__ == "__main__": demo()七、解决方案(第三层:断言 / CI 守护)
import asyncio import pytest from your_module import McpServerExitPolicy def test_safe_handler_catches(): policy = McpServerExitPolicy() async def boom(req): raise ValueError("boom") wrapped = policy.safe_handler(boom) res = asyncio.run(wrapped({})) assert res["isError"] is True assert "boom" in res["content"][0]["text"] def test_safe_handler_passes_through(): policy = McpServerExitPolicy() async def ok(req): return {"content": [{"type": "text", "text": "ok"}]} wrapped = policy.safe_handler(ok) res = asyncio.run(wrapped({})) assert res["content"][0]["text"] == "ok" def test_keep_alive_guard(): policy = McpServerExitPolicy() with pytest.raises(RuntimeError): policy.assert_keep_alive(True) def test_keep_alive_ok(): policy = McpServerExitPolicy() policy.assert_keep_alive(False) # 不抛 def test_policy_frozen(): policy = McpServerExitPolicy() assert policy.safe_handler(lambda r: r) is not NoneCI 里加一条:启动 server,连发多个请求,断言进程在 N 次请求后仍存活(不退出),并断言异常 handler 返回isError而非崩进程。
八、排查清单
- server 是否在响应后
process.exit或 main 提前 return?必须长驻。 - 是否给
unhandledRejection/uncaughtException加了监听兜底? - handler 异常是否转成
isError响应,而非上抛退出? - 是否误监听 stdin
end并退出?stdio server 不该因 EOF 退出。 - 是否用了正确的持续监听 transport(stdio server)?
- inspector 单测稳、接客户端退?多半是生命周期/异常未兜底。
九、小结
MCP server 在初始响应后意外退出,根因是进程没有保持长驻——要么 handler 抛了未捕获异常让事件循环停下,要么 main 提前 return/主动 exit,要么误判 stdin EOF。最小修复是用官方 SDK 的 stdio transport 长驻、给未捕获异常加监听、把 handler 异常转成isError响应而非上抛;结构化做法是抽成McpServerExitPolicy,集中约束"长驻 + 异常兜底";最后用 pytest 守护"多请求后仍存活、异常不退出进程",确保 server 稳定对接 Claude Desktop。