Title: ClientDisconnect during _handle_post_request crashes stateless session with ClosedResourceError · Issue #2064 · modelcontextprotocol/python-sdk · GitHub
Open Graph Title: ClientDisconnect during _handle_post_request crashes stateless session with ClosedResourceError · Issue #2064 · modelcontextprotocol/python-sdk
X Title: ClientDisconnect during _handle_post_request crashes stateless session with ClosedResourceError · Issue #2064 · modelcontextprotocol/python-sdk
Description: Initial Checks I confirm that I'm using the latest version of MCP Python SDK I confirm that I searched for my issue in https://github.com/modelcontextprotocol/python-sdk/issues before opening this issue Description When a client disconne...
Open Graph Description: Initial Checks I confirm that I'm using the latest version of MCP Python SDK I confirm that I searched for my issue in https://github.com/modelcontextprotocol/python-sdk/issues before opening this ...
X Description: Initial Checks I confirm that I'm using the latest version of MCP Python SDK I confirm that I searched for my issue in https://github.com/modelcontextprotocol/python-sdk/issues before opening t...
Opengraph URL: https://github.com/modelcontextprotocol/python-sdk/issues/2064
X: @github
Domain: github.com
{"@context":"https://schema.org","@type":"DiscussionForumPosting","headline":"ClientDisconnect during _handle_post_request crashes stateless session with ClosedResourceError","articleBody":"### Initial Checks\n\n- [x] I confirm that I'm using the latest version of MCP Python SDK\n- [x] I confirm that I searched for my issue in https://github.com/modelcontextprotocol/python-sdk/issues before opening this issue\n\n### Description\n\n When a client disconnects while a stateless streamable-HTTP server is reading the request body, `_handle_post_request` catches the\n `ClientDisconnect` but the error handler in `_handle_message` (lowlevel/server.py:694) then tries to `send_log_message()` back to the\n client. Since the session was already terminated and the write stream closed, this raises `ClosedResourceError`, which is unhandled and\n crashes the stateless session with an `ExceptionGroup`.\n\n This is a **different code path** from what PR #1384 fixed. That PR addressed `ClosedResourceError` in the message router loop. This bug\n is in the error recovery path: catch exception → try to log it to client → write stream already closed → crash.\n\n ## Versions\n\n - **mcp**: 1.26.0 (also reproduced on 1.25.0; believed to affect \u003e= 1.12.0)\n - **Python**: 3.14.2 (also reproducible on 3.12+)\n - **starlette**: 0.48.0\n - **uvicorn**: 0.34.3\n\n ## Steps to reproduce\n Run the attached repro script to reproduce the problem.\n\n ## Expected behavior\n\n The server should log a warning about the client disconnect and cleanly discard the failed request, without crashing the stateless session.\n\n ## Root cause\n\n In `lowlevel/server.py`, `_handle_message` has a catch-all exception handler (line ~690) that calls `session.send_log_message()` to notify\n the client about the error. When the error *is* a client disconnect, the write stream is already closed, so `send_log_message` →\n `send_notification` → `_write_stream.send()` raises `ClosedResourceError`. This is unhandled in the TaskGroup and crashes the session.\n\n A possible fix would be to catch `ClosedResourceError` (and/or `BrokenResourceError`) in the error handler at `_handle_message`, since\n failing to notify a disconnected client is expected and harmless.\n\n ## Related issues\n\n - #1190 — closed, partially fixed by PR #1384 (message router path only)\n - #1219 — closed as duplicate of #1190\n - #1658 — closed as duplicate of #1190\n\n None of these cover the `_handle_post_request` → `_handle_message` → `send_log_message` path.\n\n\n\n### Example Code\n\n```Python\n\"\"\"Minimal reproduction: MCP SDK crashes with ClosedResourceError on client disconnect.\"\"\"\n\nimport asyncio\nimport contextlib\nimport logging\nimport time\n\nimport httpx\nimport mcp.types as types\nimport uvicorn\nfrom mcp.server.lowlevel.server import Server\nfrom mcp.server.streamable_http_manager import StreamableHTTPSessionManager\nfrom starlette.applications import Starlette\nfrom starlette.routing import Mount\n\nlogging.basicConfig(level=logging.INFO, format=\"%(levelname)s %(name)s: %(message)s\")\n\nmcp_server = Server(name=\"repro-server\", version=\"0.1.0\")\n\n\n@mcp_server.list_tools()\nasync def list_tools() -\u003e list[types.Tool]:\n return [\n types.Tool(\n name=\"hello\",\n description=\"A trivial tool.\",\n inputSchema={\"type\": \"object\", \"properties\": {}},\n )\n ]\n\n\n@mcp_server.call_tool()\nasync def call_tool(name: str, arguments: dict) -\u003e list[types.TextContent]:\n return [types.TextContent(type=\"text\", text=\"hello\")]\n\n\nsession_manager = StreamableHTTPSessionManager(app=mcp_server, stateless=True)\n\n\n@contextlib.asynccontextmanager\nasync def lifespan(app: Starlette):\n async with session_manager.run():\n yield\n\n\napp = Starlette(\n routes=[Mount(\"/\", app=session_manager.handle_request)],\n lifespan=lifespan,\n)\n\n\nasync def run_client() -\u003e None:\n await asyncio.sleep(1)\n url = \"http://127.0.0.1:19876/\"\n\n # Step 1: Normal MCP initialize\n async with httpx.AsyncClient() as client:\n await client.post(url, json={\n \"jsonrpc\": \"2.0\", \"id\": 1, \"method\": \"initialize\",\n \"params\": {\n \"protocolVersion\": \"2025-03-26\",\n \"capabilities\": {},\n \"clientInfo\": {\"name\": \"repro-client\", \"version\": \"0.1.0\"},\n },\n }, headers={\"Content-Type\": \"application/json\", \"Accept\": \"application/json, text/event-stream\"})\n\n await client.post(url, json={\n \"jsonrpc\": \"2.0\", \"method\": \"notifications/initialized\",\n }, headers={\"Content-Type\": \"application/json\", \"Accept\": \"application/json, text/event-stream\"})\n\n # Step 2: Send truncated body, then disconnect\n _, writer = await asyncio.open_connection(\"127.0.0.1\", 19876)\n writer.write((\n \"POST / HTTP/1.1\\r\\nHost: 127.0.0.1:19876\\r\\n\"\n \"Content-Type: application/json\\r\\nAccept: application/json, text/event-stream\\r\\n\"\n \"Content-Length: 10000\\r\\n\\r\\n\"\n '{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/call\"'\n ).encode())\n await writer.drain()\n await asyncio.sleep(0.5)\n writer.close()\n await writer.wait_closed()\n await asyncio.sleep(3)\n\n\nasync def main() -\u003e None:\n config = uvicorn.Config(app, host=\"127.0.0.1\", port=19876, log_level=\"warning\")\n server = uvicorn.Server(config)\n server_task = asyncio.create_task(server.serve())\n try:\n await run_client()\n finally:\n server.should_exit = True\n await server_task\n\n\nif __name__ == \"__main__\":\n start = time.monotonic()\n asyncio.run(main())\n print(f\"Done in {time.monotonic() - start:.1f}s — check ERROR logs above.\")\n```\n\n### Python \u0026 MCP Python SDK\n\n```Text\nmcp 1.26.0 (also reproduced on 1.25.0; believed to affect \u003e= 1.12.0)\nPython 3.14.2 (also reproducible on 3.12+)\nstarlette 0.48.0\nuvicorn 0.34.3\n```","author":{"url":"https://github.com/einarfd","@type":"Person","name":"einarfd"},"datePublished":"2026-02-14T13:11:18.000Z","interactionStatistic":{"@type":"InteractionCounter","interactionType":"https://schema.org/CommentAction","userInteractionCount":3},"url":"https://github.com/2064/python-sdk/issues/2064"}
| route-pattern | /_view_fragments/issues/show/:user_id/:repository/:id/issue_layout(.:format) |
| route-controller | voltron_issues_fragments |
| route-action | issue_layout |
| fetch-nonce | v2:5a54122a-132f-1cf0-df5d-ea0252015e1f |
| current-catalog-service-hash | 81bb79d38c15960b92d99bca9288a9108c7a47b18f2423d0f6438c5b7bcd2114 |
| request-id | 9B38:23F578:68C0B0:89B4F2:6A5B3291 |
| html-safe-nonce | 29807eac96843e194bdd34d03563499269d85bc12bd1bf24ffd68cc6e6a36fda |
| visitor-payload | eyJyZWZlcnJlciI6IiIsInJlcXVlc3RfaWQiOiI5QjM4OjIzRjU3ODo2OEMwQjA6ODlCNEYyOjZBNUIzMjkxIiwidmlzaXRvcl9pZCI6IjMyOTkxMjE4MDA5ODMyOTQ2MDkiLCJyZWdpb25fZWRnZSI6ImlhZCIsInJlZ2lvbl9yZW5kZXIiOiJpYWQifQ== |
| visitor-hmac | 7736258b1a12f5e8cd74aff4e179161d5b4dd36b5e6ecb77ceec6f409769c321 |
| hovercard-subject-tag | issue:3941421632 |
| github-keyboard-shortcuts | repository,issues,copilot |
| google-site-verification | Apib7-x98H0j5cPqHWwSMm6dNU4GmODRoqxLiDzdx9I |
| octolytics-url | https://collector.github.com/github/collect |
| analytics-location | / |
| fb:app_id | 1401488693436528 |
| apple-itunes-app | app-id=1477376905, app-argument=https://github.com/_view_fragments/issues/show/modelcontextprotocol/python-sdk/2064/issue_layout |
| twitter:image | https://opengraph.githubassets.com/524346a331b7e445c775790236c3d1409cb5c57bb3bdbd3f612816d32bab9788/modelcontextprotocol/python-sdk/issues/2064 |
| twitter:card | summary_large_image |
| og:image | https://opengraph.githubassets.com/524346a331b7e445c775790236c3d1409cb5c57bb3bdbd3f612816d32bab9788/modelcontextprotocol/python-sdk/issues/2064 |
| og:image:alt | Initial Checks I confirm that I'm using the latest version of MCP Python SDK I confirm that I searched for my issue in https://github.com/modelcontextprotocol/python-sdk/issues before opening this ... |
| og:image:width | 1200 |
| og:image:height | 600 |
| og:site_name | GitHub |
| og:type | object |
| og:author:username | einarfd |
| hostname | github.com |
| expected-hostname | github.com |
| None | 5290d7e14309ad1e76106a9c4237bd1041517e83ea182c8ab756752cb0c6940b |
| turbo-cache-control | no-preview |
| go-import | github.com/modelcontextprotocol/python-sdk git https://github.com/modelcontextprotocol/python-sdk.git |
| octolytics-dimension-user_id | 182288589 |
| octolytics-dimension-user_login | modelcontextprotocol |
| octolytics-dimension-repository_id | 862584018 |
| octolytics-dimension-repository_nwo | modelcontextprotocol/python-sdk |
| octolytics-dimension-repository_public | true |
| octolytics-dimension-repository_is_fork | false |
| octolytics-dimension-repository_network_root_id | 862584018 |
| octolytics-dimension-repository_network_root_nwo | modelcontextprotocol/python-sdk |
| turbo-body-classes | logged-out env-production page-responsive |
| disable-turbo | false |
| browser-stats-url | https://api.github.com/_private/browser/stats |
| browser-errors-url | https://api.github.com/_private/browser/errors |
| release | 9c975978430e9ad293956f2bbdaf153b1bd84a99 |
| ui-target | full |
| theme-color | #1e2327 |
| color-scheme | light dark |
Links:
Viewport: width=device-width