Skip to content

Commit ac3f477

Browse files
committed
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
1 parent 8d0f928 commit ac3f477

2 files changed

Lines changed: 31 additions & 7 deletions

File tree

src/mcp/server/mcpserver/server.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -358,13 +358,16 @@ def run(
358358
if transport not in TRANSPORTS.__args__: # type: ignore # pragma: no cover
359359
raise ValueError(f"Unknown transport: {transport}")
360360

361-
match transport:
362-
case "stdio":
363-
anyio.run(self.run_stdio_async)
364-
case "sse": # pragma: no cover
365-
anyio.run(lambda: self.run_sse_async(**kwargs))
366-
case "streamable-http": # pragma: no cover
367-
anyio.run(lambda: self.run_streamable_http_async(**kwargs))
361+
try:
362+
match transport:
363+
case "stdio":
364+
anyio.run(self.run_stdio_async)
365+
case "sse": # pragma: no cover
366+
anyio.run(lambda: self.run_sse_async(**kwargs))
367+
case "streamable-http": # pragma: no cover
368+
anyio.run(lambda: self.run_streamable_http_async(**kwargs))
369+
except KeyboardInterrupt:
370+
return
368371

369372
async def _handle_list_tools(
370373
self, ctx: ServerRequestContext[LifespanResultT], params: PaginatedRequestParams | None

tests/server/mcpserver/test_server.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,27 @@ def test_dependencies(self):
8787
mcp_no_deps = MCPServer("test")
8888
assert mcp_no_deps.dependencies == []
8989

90+
def test_run_suppresses_keyboard_interrupt(self, monkeypatch: pytest.MonkeyPatch) -> None:
91+
mcp = MCPServer("test")
92+
93+
def raise_keyboard_interrupt(*args: Any, **kwargs: Any) -> None:
94+
raise KeyboardInterrupt
95+
96+
monkeypatch.setattr("mcp.server.mcpserver.server.anyio.run", raise_keyboard_interrupt)
97+
98+
assert mcp.run(transport="stdio") is None
99+
100+
def test_run_reraises_other_exceptions(self, monkeypatch: pytest.MonkeyPatch) -> None:
101+
mcp = MCPServer("test")
102+
103+
def raise_runtime_error(*args: Any, **kwargs: Any) -> None:
104+
raise RuntimeError("startup failed")
105+
106+
monkeypatch.setattr("mcp.server.mcpserver.server.anyio.run", raise_runtime_error)
107+
108+
with pytest.raises(RuntimeError, match="startup failed"):
109+
mcp.run(transport="stdio")
110+
90111
async def test_sse_app_returns_starlette_app(self):
91112
"""Test that sse_app returns a Starlette application with correct routes."""
92113
mcp = MCPServer("test")

0 commit comments

Comments
 (0)