chore: import upstream snapshot with attribution
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) Has been cancelled
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) Has been cancelled
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
# 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).
|
||||
Reference in New Issue
Block a user