Skip to content

fix(server): exit cleanly on KeyboardInterrupt in MCPServer.run - #3027

Open
devansh-dek wants to merge 1 commit into
modelcontextprotocol:mainfrom
devansh-dek:fix/2663-keyboardinterrupt-clean-exit
Open

fix(server): exit cleanly on KeyboardInterrupt in MCPServer.run#3027
devansh-dek wants to merge 1 commit into
modelcontextprotocol:mainfrom
devansh-dek:fix/2663-keyboardinterrupt-clean-exit

Conversation

@devansh-dek

@devansh-dek devansh-dek commented Jun 30, 2026

Copy link
Copy Markdown

Summary

  • Catch KeyboardInterrupt at the MCPServer.run() sync boundary so Ctrl-C during stdio server execution exits cleanly instead of printing a noisy anyio/asyncio cancellation traceback.
  • Adds unit tests verifying KeyboardInterrupt is suppressed while other exceptions still propagate.

Test plan

  • uv run pytest tests/server/mcpserver/test_server.py::TestServer::test_run_suppresses_keyboard_interrupt tests/server/mcpserver/test_server.py::TestServer::test_run_reraises_other_exceptions -v
  • uv run ruff check src/mcp/server/mcpserver/server.py tests/server/mcpserver/test_server.py

Fixes #2663

Catch KeyboardInterrupt at the sync run() boundary so Ctrl-C during stdio
server execution does not print a noisy anyio/asyncio cancellation traceback.

Fixes modelcontextprotocol#2663

@ErenAta16 ErenAta16 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix is correct, and this is the tightest of the three open PRs for #2663 in terms of diff: one test file, no unrelated edits.

Worth knowing that the source change here is byte-identical to #2745 (Jun 1) and #2778 (Jun 4). All three wrap the match transport: block in try/except KeyboardInterrupt: return, same lines, same position. Yours is the most recent of the three, so #2745 has priority by date, though yours has the cleanest diff. Full comparison on #2663.

Exit code. except KeyboardInterrupt: return makes run() return normally, so the process exits 0. Measured both shapes:

except KeyboardInterrupt: return   -> exit code 0
raise SystemExit(130)              -> exit code 130

130 is 128 + SIGINT, which is what an uncaught KeyboardInterrupt produces today and what a shell or supervisor expects from a Ctrl-C. After this change a script doing mcp-server || handle_failure reads an interrupted run as a clean success.

If the goal is only to drop the traceback, raise SystemExit(130) gets that without changing the signal semantics. If exiting 0 is deliberate (say, because a supervisor is meant to treat Ctrl-C as a normal stop) that's a defensible choice, it just deserves a comment, otherwise someone will "fix" it later assuming it was an oversight.

Your two tests (suppress KeyboardInterrupt, re-raise RuntimeError) are the right pair: the second is what stops a future change from widening the except into something that swallows real startup failures. If the exit-code point above is adopted, the first would want updating to assert SystemExit with code 130 rather than a plain None return.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

FastMCP.run(transport="stdio") produces noisy traceback on KeyboardInterrupt instead of clean exit

2 participants