From ac3f477075bfbcf7877e7087d4928fd9cd23f76b Mon Sep 17 00:00:00 2001 From: Devansh Khandelwal Date: Tue, 30 Jun 2026 09:52:41 +0530 Subject: [PATCH] fix(server): exit cleanly on KeyboardInterrupt in MCPServer.run Catch KeyboardInterrupt at the sync run() boundary so Ctrl-C during stdio server execution does not print a noisy anyio/asyncio cancellation traceback. Fixes #2663 --- src/mcp/server/mcpserver/server.py | 17 ++++++++++------- tests/server/mcpserver/test_server.py | 21 +++++++++++++++++++++ 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/src/mcp/server/mcpserver/server.py b/src/mcp/server/mcpserver/server.py index 676470980..2f1117e7f 100644 --- a/src/mcp/server/mcpserver/server.py +++ b/src/mcp/server/mcpserver/server.py @@ -358,13 +358,16 @@ def run( if transport not in TRANSPORTS.__args__: # type: ignore # pragma: no cover raise ValueError(f"Unknown transport: {transport}") - match transport: - case "stdio": - anyio.run(self.run_stdio_async) - case "sse": # pragma: no cover - anyio.run(lambda: self.run_sse_async(**kwargs)) - case "streamable-http": # pragma: no cover - anyio.run(lambda: self.run_streamable_http_async(**kwargs)) + try: + match transport: + case "stdio": + anyio.run(self.run_stdio_async) + case "sse": # pragma: no cover + anyio.run(lambda: self.run_sse_async(**kwargs)) + case "streamable-http": # pragma: no cover + anyio.run(lambda: self.run_streamable_http_async(**kwargs)) + except KeyboardInterrupt: + return async def _handle_list_tools( self, ctx: ServerRequestContext[LifespanResultT], params: PaginatedRequestParams | None diff --git a/tests/server/mcpserver/test_server.py b/tests/server/mcpserver/test_server.py index b4a118458..6a2633ff5 100644 --- a/tests/server/mcpserver/test_server.py +++ b/tests/server/mcpserver/test_server.py @@ -87,6 +87,27 @@ def test_dependencies(self): mcp_no_deps = MCPServer("test") assert mcp_no_deps.dependencies == [] + def test_run_suppresses_keyboard_interrupt(self, monkeypatch: pytest.MonkeyPatch) -> None: + mcp = MCPServer("test") + + def raise_keyboard_interrupt(*args: Any, **kwargs: Any) -> None: + raise KeyboardInterrupt + + monkeypatch.setattr("mcp.server.mcpserver.server.anyio.run", raise_keyboard_interrupt) + + assert mcp.run(transport="stdio") is None + + def test_run_reraises_other_exceptions(self, monkeypatch: pytest.MonkeyPatch) -> None: + mcp = MCPServer("test") + + def raise_runtime_error(*args: Any, **kwargs: Any) -> None: + raise RuntimeError("startup failed") + + monkeypatch.setattr("mcp.server.mcpserver.server.anyio.run", raise_runtime_error) + + with pytest.raises(RuntimeError, match="startup failed"): + mcp.run(transport="stdio") + async def test_sse_app_returns_starlette_app(self): """Test that sse_app returns a Starlette application with correct routes.""" mcp = MCPServer("test")