Skip to content

Commit 60d99f3

Browse files
committed
fix: exit stdio server cleanly on interrupt
1 parent ac96f88 commit 60d99f3

2 files changed

Lines changed: 18 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
@@ -291,13 +291,16 @@ def run(
291291
if transport not in TRANSPORTS.__args__: # type: ignore # pragma: no cover
292292
raise ValueError(f"Unknown transport: {transport}")
293293

294-
match transport:
295-
case "stdio":
296-
anyio.run(self.run_stdio_async)
297-
case "sse": # pragma: no cover
298-
anyio.run(lambda: self.run_sse_async(**kwargs))
299-
case "streamable-http": # pragma: no cover
300-
anyio.run(lambda: self.run_streamable_http_async(**kwargs))
294+
try:
295+
match transport:
296+
case "stdio":
297+
anyio.run(self.run_stdio_async)
298+
case "sse": # pragma: no cover
299+
anyio.run(lambda: self.run_sse_async(**kwargs))
300+
case "streamable-http": # pragma: no cover
301+
anyio.run(lambda: self.run_streamable_http_async(**kwargs))
302+
except KeyboardInterrupt:
303+
return
301304

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

tests/server/mcpserver/test_server.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,14 @@ def test_dependencies(self):
7373
mcp_no_deps = MCPServer("test")
7474
assert mcp_no_deps.dependencies == []
7575

76+
def test_stdio_keyboard_interrupt_exits_cleanly(self):
77+
mcp = MCPServer("test")
78+
79+
with patch("mcp.server.mcpserver.server.anyio.run", side_effect=KeyboardInterrupt) as run:
80+
mcp.run("stdio")
81+
82+
run.assert_called_once_with(mcp.run_stdio_async)
83+
7684
async def test_sse_app_returns_starlette_app(self):
7785
"""Test that sse_app returns a Starlette application with correct routes."""
7886
mcp = MCPServer("test")

0 commit comments

Comments
 (0)