Files
modelcontextprotocol--pytho…/examples/stories/parallel_calls/README.md
T
wehub-resource-sync 49b9bb6724
Deploy Docs / deploy-docs (push) Failing after 1s
Conformance Tests / client-conformance (push) Failing after 3s
Conformance Tests / server-conformance (push) Failing after 1s
GitHub Actions Security Analysis / zizmor (push) Failing after 1s
CI / checks (push) Failing after 59m20s
CI / all-green (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 12:10:27 +08:00

61 lines
2.6 KiB
Markdown

# parallel-calls
Two `Client`s connected to the same server, each with a `call_tool` in flight
at once. The `meet` tool is a rendezvous: a handler signals its own arrival,
then blocks until every named peer has arrived too — so neither call can return
unless the server runs both handlers concurrently. Each caller's
`progress_callback=` sees only the notifications for *its* request — each
`Client` is a separate connection, so there's no shared wire for them to cross
on.
## Run it
The tested legs run in-memory (`Client(server)`); the identical `main` body
works unchanged over HTTP — both clients just reach the same server. Under
`--http` the client self-hosts that server on a free port, runs, then tears it
down:
```bash
# --legacy because handler-emitted progress is dropped on the modern
# streamable-HTTP path today (see Caveats).
uv run python -m stories.parallel_calls.client --http --legacy
# same, against the lowlevel-API server variant
uv run python -m stories.parallel_calls.client --http --legacy --server server_lowlevel
```
There is no stdio run for this story: the stdio default spawns a fresh server
subprocess per connection, so two clients there could never rendezvous.
## What to look at
- **`client.py` — the two visible `Client(targets(), mode=...)` blocks.** Each
connection is constructed inside `attend(...)`; `targets()` yields a fresh
target on every call and both land on the same server instance. The two
blocks run in one `anyio` task group.
- **`server.py` — the `arrivals` barrier.** Each handler sets its own
`anyio.Event` then waits for every peer's. A server that processed requests
sequentially would never set the second event, so the client would time out —
the timeout *is* the concurrency assertion. No sleeps.
- **`client.py``progress_callback=` per call.** Each call passes its own
callback; `received == {"a": ["a"], "b": ["b"]}` shows each connection
delivered its own progress, and — combined with the rendezvous — that both
calls were genuinely in flight at once.
- **`server_lowlevel.py`** — same wire contract on the lowlevel `Server`,
reporting via `ctx.session.report_progress(...)`.
## Caveats
- Over Streamable HTTP in the modern (2026-07-28) era, handler-emitted progress
is currently dropped (the single-exchange dispatch context no-ops `notify()`).
In-memory (both eras) and legacy-era HTTP deliver progress correctly — hence
the `--legacy` above.
## Spec
[Progress flow](https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/progress)
## See also
`streaming/` (progress + cancellation on one call), `reconnect/` (the other
multi-connection client), `tools/` (basics).