Title: Concurrent server-side requests are serialized end-to-end by BaseSession · Issue #2489 · modelcontextprotocol/python-sdk · GitHub
Open Graph Title: Concurrent server-side requests are serialized end-to-end by BaseSession · Issue #2489 · modelcontextprotocol/python-sdk
X Title: Concurrent server-side requests are serialized end-to-end by BaseSession · Issue #2489 · modelcontextprotocol/python-sdk
Description: Description When a server tool issues multiple concurrent server to client requests over a single MCP session — for example, fanning out several ServerSession.create_message(...) (sampling) calls via asyncio.gather — only one is ever in ...
Open Graph Description: Description When a server tool issues multiple concurrent server to client requests over a single MCP session — for example, fanning out several ServerSession.create_message(...) (sampling) calls v...
X Description: Description When a server tool issues multiple concurrent server to client requests over a single MCP session — for example, fanning out several ServerSession.create_message(...) (sampling) calls v...
Opengraph URL: https://github.com/modelcontextprotocol/python-sdk/issues/2489
X: @github
Domain: github.com
{"@context":"https://schema.org","@type":"DiscussionForumPosting","headline":"Concurrent server-side requests are serialized end-to-end by BaseSession","articleBody":"### Description\n\nWhen a server tool issues multiple concurrent server to client requests over a single MCP session — for example, fanning out several `ServerSession.create_message(...)` (sampling) calls via `asyncio.gather` — only one is ever in flight on the receiving side: `BaseSession._receive_loop` awaits each incoming request handler inline before reading the next message off the stream, so subsequent responses can't even be dequeued until the current handler returns. A minimal reproducer using `mcp.server.fastmcp.FastMCP` + `ClientSession` over the SDK's own in-memory transport (5 concurrent `create_message` calls, sampling callback sleeps 0.5s) shows peak in-flight = 1 and elapsed ≈ 2.5s instead of ≈ 0.5s.\n\nConcrete use case: a tool that fans out dozens of independent sampling requests per invocation against an LLM deployment with provisioned capacity sitting idle, the work is embarrassingly parallel and there's token budget to burn, but wall-clock scales linearly with the fan-out because every request blocks the next one. The same shape applies to elicitation, list-roots, and any other server to client request type, since they all flow through the same loop. \n\nI'd like the SDK to support concurrent handling of incoming requests on a single session, likely as an opt-in knob to preserve today's strict-ordering semantics for handlers that depend on them, with whatever bounding/back-pressure mechanism the maintainers prefer.\n\n### References\n\n```python\n\"\"\"Reproducer: concurrent server-\u003eclient requests are serialized end-to-end.\n\nA FastMCP tool fans out N concurrent ``ServerSession.create_message`` calls\nvia ``asyncio.gather``. The client's sampling callback records peak in-flight.\n\nExpected with concurrent dispatch: peak == N, elapsed ~= SLEEP\nObserved today: peak == 1, elapsed ~= N * SLEEP\n\nRoot cause: ``BaseSession._receive_loop`` awaits each incoming request\nhandler inline before reading the next message off the stream\n(mcp/shared/session.py).\n\nRun: python repro_concurrent_sampling.py\n\"\"\"\n\nimport asyncio\n\nfrom mcp.server.fastmcp import Context, FastMCP\nfrom mcp.shared.context import RequestContext\nfrom mcp.shared.memory import create_connected_server_and_client_session\nfrom mcp.types import (\n CreateMessageRequestParams,\n CreateMessageResult,\n SamplingMessage,\n TextContent,\n)\n\nN = 5\nSLEEP = 0.5\n\n\nasync def main() -\u003e None:\n inflight = 0\n peak = 0\n\n async def sampling_callback(\n _ctx: RequestContext,\n params: CreateMessageRequestParams,\n ) -\u003e CreateMessageResult:\n nonlocal inflight, peak\n inflight += 1\n peak = max(peak, inflight)\n try:\n await asyncio.sleep(SLEEP)\n finally:\n inflight -= 1\n msg = params.messages[0].content\n echo = msg.text if isinstance(msg, TextContent) else \"\"\n return CreateMessageResult(\n role=\"assistant\",\n content=TextContent(type=\"text\", text=f\"echo:{echo}\"),\n model=\"test-model\",\n )\n\n server = FastMCP(\"repro\")\n\n @server.tool()\n async def fanout(ctx: Context) -\u003e str:\n async def one(i: int) -\u003e str:\n r = await ctx.session.create_message(\n messages=[\n SamplingMessage(\n role=\"user\",\n content=TextContent(type=\"text\", text=str(i)),\n )\n ],\n max_tokens=8,\n )\n return r.content.text if isinstance(r.content, TextContent) else \"\"\n\n return \",\".join(await asyncio.gather(*(one(i) for i in range(N))))\n\n loop = asyncio.get_running_loop()\n t0 = loop.time()\n async with create_connected_server_and_client_session(\n server, sampling_callback=sampling_callback\n ) as session:\n await session.call_tool(\"fanout\", {})\n elapsed = loop.time() - t0\n\n print(f\"N={N}, per-call sleep={SLEEP}s\")\n print(f\"peak in-flight: {peak} (concurrent: {N}, serialized: 1)\")\n print(f\"elapsed: {elapsed:.2f}s (concurrent: ~{SLEEP}s, serialized: ~{N * SLEEP}s)\")\n\n assert peak == N, (\n f\"server-\u003eclient requests were serialized: peak in-flight={peak}, expected {N}\"\n )\n\n\nif __name__ == \"__main__\":\n asyncio.run(main())\n```","author":{"url":"https://github.com/demoray","@type":"Person","name":"demoray"},"datePublished":"2026-04-21T21:00:57.000Z","interactionStatistic":{"@type":"InteractionCounter","interactionType":"https://schema.org/CommentAction","userInteractionCount":0},"url":"https://github.com/2489/python-sdk/issues/2489"}
| 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:ca3661cf-a70d-286e-9f36-d7a3a0099d70 |
| current-catalog-service-hash | 81bb79d38c15960b92d99bca9288a9108c7a47b18f2423d0f6438c5b7bcd2114 |
| request-id | D0C4:1F6472:20F08A:2E1805:6A59C3B5 |
| html-safe-nonce | bb8750c5957e1429549126cc0bb6a84ce15f4af60bba893f0bcab62de614a05e |
| visitor-payload | eyJyZWZlcnJlciI6IiIsInJlcXVlc3RfaWQiOiJEMEM0OjFGNjQ3MjoyMEYwOEE6MkUxODA1OjZBNTlDM0I1IiwidmlzaXRvcl9pZCI6IjY3ODI5MjIzNzQ5NTY4Mjc1NzMiLCJyZWdpb25fZWRnZSI6ImlhZCIsInJlZ2lvbl9yZW5kZXIiOiJpYWQifQ== |
| visitor-hmac | 0fbad6ccfe94f805ed27c03e7572f634ffea7afd06f5b8a674f0c0bc734204b5 |
| hovercard-subject-tag | issue:4305261666 |
| 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/2489/issue_layout |
| twitter:image | https://opengraph.githubassets.com/f19dde24c2af54401694511b225deb921d43f0e07a056a387c9cf676c52fce79/modelcontextprotocol/python-sdk/issues/2489 |
| twitter:card | summary_large_image |
| og:image | https://opengraph.githubassets.com/f19dde24c2af54401694511b225deb921d43f0e07a056a387c9cf676c52fce79/modelcontextprotocol/python-sdk/issues/2489 |
| og:image:alt | Description When a server tool issues multiple concurrent server to client requests over a single MCP session — for example, fanning out several ServerSession.create_message(...) (sampling) calls v... |
| og:image:width | 1200 |
| og:image:height | 600 |
| og:site_name | GitHub |
| og:type | object |
| og:author:username | demoray |
| hostname | github.com |
| expected-hostname | github.com |
| None | ba3976babb66479b1c943a8edc0777d96157da48fadc0161f9ddb219deee8353 |
| 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 | ab680789ae4a316cdaf0d5a292a1760140931cc4 |
| ui-target | full |
| theme-color | #1e2327 |
| color-scheme | light dark |
Links:
Viewport: width=device-width