54 lines
2.4 KiB
Markdown
54 lines
2.4 KiB
Markdown
# mrtr
|
|
|
|
Multi-round tool result: on the 2026-07-28 protocol a tool that needs user
|
|
input mid-call **returns** `resultType: "input_required"` with embedded
|
|
`inputRequests` and an opaque `requestState`, instead of pushing a
|
|
server→client request. The client fulfils the embedded requests and retries the
|
|
original `tools/call` carrying `inputResponses` and the echoed `requestState`.
|
|
The story shows both the `Client` auto-loop (one `await call_tool`, callbacks
|
|
fired transparently) and a manual `client.session` loop (the persistable form).
|
|
|
|
## Run it
|
|
|
|
```bash
|
|
# HTTP — the client self-hosts the server on a free port, runs, then tears it
|
|
# down (the InputRequiredResult round-trip is 2026-era only)
|
|
uv run python -m stories.mrtr.client --http
|
|
# same, against the lowlevel-API server variant
|
|
uv run python -m stories.mrtr.client --http --server server_lowlevel
|
|
```
|
|
|
|
## What to look at
|
|
|
|
- `client.py` `main` — the auto-loop is invisible at the call site:
|
|
`Client(target, mode=mode, elicitation_callback=on_elicit)` then
|
|
`await client.call_tool("deploy", ...)`. The same `on_elicit` callback the
|
|
legacy push path uses is dispatched for each embedded `inputRequests` entry.
|
|
- `client.py` manual block — `client.session.call_tool(...,
|
|
allow_input_required=True)` returns the raw `InputRequiredResult` so
|
|
`request_state` can be persisted between rounds; the retry is just another
|
|
`tools/call` with `input_responses=` / `request_state=`.
|
|
- `server.py` `deploy` — `ctx.input_responses` / `ctx.request_state` read the
|
|
retry payload; the first round returns
|
|
`InputRequiredResult(input_requests={...}, request_state=...)`, the second
|
|
returns the final string.
|
|
- `server_lowlevel.py` — same wire contract via `params.input_responses` /
|
|
`params.request_state` and a hand-built `InputRequiredResult`.
|
|
|
|
## Caveats
|
|
|
|
- **Loop bound.** The auto-loop gives up after `input_required_max_rounds`
|
|
(default 10) with `InputRequiredRoundsExceededError`; raise it on the
|
|
`Client` ctor or drop to the manual loop.
|
|
- **`requestState` integrity is the server's job.** The client echoes it
|
|
byte-exact and never inspects it; the server MUST treat it as
|
|
attacker-controlled. The SDK ships no signing helper yet.
|
|
|
|
## Spec
|
|
|
|
[Multi-round results — server features](https://modelcontextprotocol.io/specification/draft/server/tools#multi-round-results)
|
|
|
|
## See also
|
|
|
|
`legacy_elicitation/` and `sampling/` — the handshake-era push equivalents this
|
|
mechanism replaces on the 2026 protocol.
|