Symptom
Over any transport that goes through JSONRPCDispatcher, notifications/progress callbacks can finish in any order and after call_tool() returns. The report's snippet printed ['call_tool returned', 'progress 3.0', 'progress 2.0', 'progress 1.0'].
Evidence what you'll see
- The same snippet with in-process
Client(server)(DirectDispatcher, which awaits the callback inline) printed['progress 1', 'progress 2', 'progress 3', 'call_tool returned']. - v1 delivered progress in order, before the result.
- The mis-order appears as soon as the callback awaits anything.
tests/interaction/lowlevel/test_progress.pydoes not catch it because its callback never awaits.
Why it happens
In src/mcp/shared/jsonrpc_dispatcher.py, _dispatch_notification spawns self._spawn(_shielded_progress(pending.on_progress), ...) once per notifications/progress. The response resolves the pending request independently of those tasks. The server send_request(..., progress_callback=...) path has the same split.
Affected versions
Python 3.12.14, mcp 2.3.0, also reproduced on main @ 2af9e30, anyio 4.15.1, Windows 11. Transports that go through JSONRPCDispatcher: stdio, SSE, streamable HTTP, and InMemoryTransport.
Fix steps
0 of 2 done
Your check marks stay in this browser only.
How to check it worked
- The report's snippet then prints progress
1then2then3beforecall_tool returned, matching the in-processClient(server)order.
If it still fails
- A callback that never awaits can still look ordered. The mis-order needs an
awaitinsideprogress_callback. - The linked issue does not name a release that queues progress per request.
Sources and credit
Summarized in our own words from these public reports. Credit goes to their authors.
Provided as is, without warranty. Try changes in a non-production environment and back up first. Security-related steps: verify against the official docs. Terms · Request removal or attribution change
Hit an error we don't cover? Submit it