【Bug已解决】Model Context Protocol (MCP) server exits unexpectedly after initial response when…
【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_aliveFalse) print(bad serve -, bad.serve()) # exit客户端会断开 good _McpLifecycle(keep_aliveTrue) 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(frozenTrue) 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响应而非上抛退出是否误监听 stdinend并退出stdio server 不该因 EOF 退出。是否用了正确的持续监听 transportstdio serverinspector 单测稳、接客户端退多半是生命周期/异常未兜底。九、小结MCP server 在初始响应后意外退出根因是进程没有保持长驻——要么 handler 抛了未捕获异常让事件循环停下要么 main 提前 return/主动 exit要么误判 stdin EOF。最小修复是用官方 SDK 的 stdio transport 长驻、给未捕获异常加监听、把 handler 异常转成isError响应而非上抛结构化做法是抽成McpServerExitPolicy集中约束长驻 异常兜底最后用 pytest 守护多请求后仍存活、异常不退出进程确保 server 稳定对接 Claude Desktop。