Title: ClientSession receive loop swallows callback exceptions, replying "Invalid request parameters" to the server instead of propagating to the call_tool awaiter · Issue #2673 · modelcontextprotocol/python-sdk · GitHub
Open Graph Title: ClientSession receive loop swallows callback exceptions, replying "Invalid request parameters" to the server instead of propagating to the call_tool awaiter · Issue #2673 · modelcontextprotocol/python-sdk
X Title: ClientSession receive loop swallows callback exceptions, replying "Invalid request parameters" to the server instead of propagating to the call_tool awaiter · Issue #2673 · 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 I hit this while wirin...
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/2673
X: @github
Domain: github.com
{"@context":"https://schema.org","@type":"DiscussionForumPosting","headline":"ClientSession receive loop swallows callback exceptions, replying \"Invalid request parameters\" to the server instead of propagating to the call_tool awaiter","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\nI hit this while wiring an MCP tool that uses `ctx.elicit(...)` into a\nLangGraph agent — the natural pattern of calling `interrupt()` from the\nclient elicitation callback does not work because the callback's exception\nis swallowed by `_receive_loop` and replied to the server as\n`Invalid request parameters`. The same limitation blocks any framework that\nrelies on exception-based control flow on the awaiter's task (custom\nasync-cancellation strategies, Trio nursery patterns, retry/backoff\nlibraries, any human-in-the-loop integration), so a small SDK-level escape\nhatch would unblock a broad class of MCP client developers, not just this\none use case.\n\nAny exception raised inside a `ClientSession` callback\n(`elicitation_callback`, `sampling_callback`, `list_roots_callback`) is\ncaught by the blanket `except Exception` in `BaseSession._receive_loop`\n(`src/mcp/shared/session.py`) and turned into a JSON-RPC\n`Invalid request parameters` reply to the server:\n\n```python\nexcept Exception:\n logging.warning(\"Failed to validate request\", exc_info=True)\n error_response = JSONRPCError(\n jsonrpc=\"2.0\",\n id=message.message.id,\n error=ErrorData(code=INVALID_PARAMS, message=\"Invalid request parameters\", data=\"\"),\n )\n await self._write_stream.send(SessionMessage(message=error_response))\n```\n\nConsequences:\n\n1. The server is blamed for \"invalid params\" when the failure was on the\n client. The original callback traceback is lost from the awaiter of\n `session.call_tool(...)`.\n2. Callbacks have no way to abort back to the awaiter. Frameworks that use\n exception-based flow control on the caller's task (LangGraph's\n `interrupt()` for human-in-the-loop pause/resume is the motivating\n case) cannot be wired through MCP without per-framework workarounds.\n\n**Expected:** an opt-in escape hatch so a callback can raise an exception\nthat propagates out of the in-flight `session.call_tool(...)` instead of\nbecoming a JSON-RPC error to the server. Defaulting opt-in to off preserves\nexisting behaviour.\n\n**Proposed minimal fix (single file, ~30 lines, three coordinated edits):**\n\n```python\n# src/mcp/shared/session.py\n#\n# (1) BaseSession.__init__: add a per-session stash for marked exceptions.\nself._propagate_errors: dict[RequestId, BaseException] = {}\n\n# (2) Inside _receive_loop's request-handler `except Exception as e:` block,\n# branch on the marker before the existing INVALID_PARAMS path.\nif getattr(e, \"__mcp_propagate__\", False):\n # Notify the peer so their request doesn't hang.\n error_response = JSONRPCError(\n jsonrpc=\"2.0\",\n id=message.message.root.id,\n error=ErrorData(code=INTERNAL_ERROR, message=\"Handler raised\", data=\"\"),\n )\n await self._write_stream.send(SessionMessage(message=JSONRPCMessage(error_response)))\n # Surface to the awaiter of any in-flight outgoing request on this session.\n for in_flight_id, stream in list(self._response_streams.items()):\n self._propagate_errors[in_flight_id] = e\n await stream.aclose()\n continue\n# ...existing INVALID_PARAMS path unchanged below this point.\n\n# (3) Inside send_request, alongside the existing `except TimeoutError:`,\n# consume the stash on EndOfStream and re-raise the original exception.\nexcept anyio.EndOfStream:\n propagate = self._propagate_errors.pop(request_id, None)\n if propagate is not None:\n raise propagate from None\n raise\n```\n\n**I've already implemented and tested this locally** against `v1.x` (and the\nsame shape on `main` for the V2 rework): ~33 lines added to\n`src/mcp/shared/session.py` plus regression tests in\n`tests/shared/test_session.py`. The patch is strictly additive — without the\n`__mcp_propagate__` marker, behaviour is byte-identical to today. End-to-end\nverified by surfacing a LangGraph `interrupt()` from an elicitation callback\nas a normal `__interrupt__` on the agent's first invoke. `ruff` / `pyright`\nclean, existing session tests still pass. Happy to open a PR once the issue\nis accepted and you've confirmed the direction.\n\n\n### Example Code\n\n```Python\n# Self-contained reproduction. No third-party agent framework needed.\n#\n# Terminal A: python repro_server.py\n# Terminal B: python repro_client.py\n#\n# Observed in A: WARNING:root:Failed to validate request: CallbackBoom(...)\n# Observed in B: tool result.isError = True, CallbackBoom was NEVER raised\n# on the awaiter — silently converted to INVALID_PARAMS.\n\n# --- repro_server.py ---\nfrom mcp.server.fastmcp import Context, FastMCP\nfrom pydantic import BaseModel\n\nclass Answer(BaseModel):\n value: str\n\nserver = FastMCP(name=\"repro\", port=8765)\n\n@server.tool()\nasync def ask(ctx: Context) -\u003e str:\n result = await ctx.elicit(message=\"hello?\", schema=Answer)\n return f\"got: {result}\"\n\nif __name__ == \"__main__\":\n server.run(transport=\"streamable-http\")\n\n\n# --- repro_client.py ---\nimport asyncio\nfrom mcp import ClientSession\nfrom mcp.client.streamable_http import streamablehttp_client\n\nclass CallbackBoom(Exception):\n \"\"\"Stand-in for any framework's flow-control exception (e.g. GraphInterrupt).\"\"\"\n\nasync def on_elicit(ctx, params):\n raise CallbackBoom(\"callback wants to abort back to the awaiter\")\n\nasync def main():\n async with streamablehttp_client(\"http://127.0.0.1:8765/mcp\") as (r, w, _):\n async with ClientSession(r, w, elicitation_callback=on_elicit) as session:\n await session.initialize()\n try:\n result = await session.call_tool(\"ask\", {})\n except CallbackBoom:\n print(\"OK: CallbackBoom propagated to the awaiter\")\n return\n print(\"BUG: awaiter did not raise; result.isError =\", result.isError)\n print(\" content =\", result.content)\n\nasyncio.run(main())\n```\n\n### Python \u0026 MCP Python SDK\n\n```Text\nPython: 3.13\nmcp: 1.27.1\n```","author":{"url":"https://github.com/danielgshea","@type":"Person","name":"danielgshea"},"datePublished":"2026-05-24T03:45:45.000Z","interactionStatistic":{"@type":"InteractionCounter","interactionType":"https://schema.org/CommentAction","userInteractionCount":4},"url":"https://github.com/2673/python-sdk/issues/2673"}
| 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:aaafd363-a85b-a586-d82b-577cc6e6c7e0 |
| current-catalog-service-hash | 81bb79d38c15960b92d99bca9288a9108c7a47b18f2423d0f6438c5b7bcd2114 |
| request-id | 84FE:11ACF6:12DF904:19516A6:6A5AB3FC |
| html-safe-nonce | b6323050f37042293d26f97a8cc434651d6f4770f1e11cd028aac00ffd86f5e2 |
| visitor-payload | eyJyZWZlcnJlciI6IiIsInJlcXVlc3RfaWQiOiI4NEZFOjExQUNGNjoxMkRGOTA0OjE5NTE2QTY6NkE1QUIzRkMiLCJ2aXNpdG9yX2lkIjoiNDYzMzQxNzgzMzE3NDk3MTM4OCIsInJlZ2lvbl9lZGdlIjoiaWFkIiwicmVnaW9uX3JlbmRlciI6ImlhZCJ9 |
| visitor-hmac | 13b8e4014f250e7e137cdaf069fbf07ebf6118f1ab991f303c4f2ef0437c0691 |
| hovercard-subject-tag | issue:4510397688 |
| 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/2673/issue_layout |
| twitter:image | https://opengraph.githubassets.com/a441669998c2a201c25cba640f750cbfecc2ccbe748bc299f0605a78f4c08e9e/modelcontextprotocol/python-sdk/issues/2673 |
| twitter:card | summary_large_image |
| og:image | https://opengraph.githubassets.com/a441669998c2a201c25cba640f750cbfecc2ccbe748bc299f0605a78f4c08e9e/modelcontextprotocol/python-sdk/issues/2673 |
| 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 | danielgshea |
| hostname | github.com |
| expected-hostname | github.com |
| None | 2702febe50e53f9152dda552dc2049a5eb182778b2655abd5b8619e82e899b4a |
| 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 | c0211a077f9b056c5e115f996d9049616d85c3b5 |
| ui-target | full |
| theme-color | #1e2327 |
| color-scheme | light dark |
Links:
Viewport: width=device-width