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
61 lines
2.6 KiB
Markdown
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).
|