Skip to content

Commit 8ee2cc6

Browse files
committed
fix: exit stdio server cleanly on interrupt
1 parent 616476f commit 8ee2cc6

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
@@ -74,6 +74,14 @@ def test_dependencies(self):
7474
mcp_no_deps = MCPServer("test")
7575
assert mcp_no_deps.dependencies == []
7676

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

0 commit comments

Comments
 (0)