Skip to content

Commit 308f466

Browse files
committed
fix: exit stdio server cleanly on interrupt
1 parent 91941ed commit 308f466

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
@@ -416,13 +416,16 @@ def run(
416416
if transport not in TRANSPORTS.__args__: # type: ignore # pragma: no cover
417417
raise ValueError(f"Unknown transport: {transport}")
418418

419-
match transport:
420-
case "stdio":
421-
anyio.run(self.run_stdio_async)
422-
case "sse": # pragma: no cover
423-
anyio.run(lambda: self.run_sse_async(**kwargs))
424-
case "streamable-http": # pragma: no cover
425-
anyio.run(lambda: self.run_streamable_http_async(**kwargs))
419+
try:
420+
match transport:
421+
case "stdio":
422+
anyio.run(self.run_stdio_async)
423+
case "sse": # pragma: no cover
424+
anyio.run(lambda: self.run_sse_async(**kwargs))
425+
case "streamable-http": # pragma: no cover
426+
anyio.run(lambda: self.run_streamable_http_async(**kwargs))
427+
except KeyboardInterrupt:
428+
return
426429

427430
async def _handle_list_tools(
428431
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
@@ -111,6 +111,14 @@ def test_dependencies(self):
111111
mcp_no_deps = MCPServer("test")
112112
assert mcp_no_deps.dependencies == []
113113

114+
def test_stdio_keyboard_interrupt_exits_cleanly(self):
115+
mcp = MCPServer("test")
116+
117+
with patch("mcp.server.mcpserver.server.anyio.run", side_effect=KeyboardInterrupt) as run:
118+
mcp.run("stdio")
119+
120+
run.assert_called_once_with(mcp.run_stdio_async)
121+
114122
async def test_sse_app_returns_starlette_app(self):
115123
"""Test that sse_app returns a Starlette application with correct routes."""
116124
mcp = MCPServer("test")

0 commit comments

Comments
 (0)