Consult request_state only for the question a resolver is asking (#3019)
This commit is contained in:
parent
533c6a8226
commit
8f2c97b769
2 changed files with 321 additions and 89 deletions
|
|
@ -19,7 +19,7 @@ That's the whole protocol. Every leg is an ordinary request from the client to t
|
|||
|
||||
## The server side
|
||||
|
||||
On `@mcp.tool()` you rarely build this by hand: declare a dependency that asks the user and the SDK returns the `InputRequiredResult` for you - that form is the **[Dependencies](../tutorial/dependencies.md)** tutorial. The manual form is the **low-level** `Server`, whose `on_call_tool` handler is allowed to return either result type:
|
||||
On `@mcp.tool()` you rarely build this by hand: declare a dependency that asks the user and the SDK returns the `InputRequiredResult` for you - that form is the **[Dependencies](../tutorial/dependencies.md)** tutorial. The two forms don't mix: a call has one `input_responses`/`request_state` channel, so a tool that uses `Resolve(...)` parameters cannot also return `InputRequiredResult` from its body. A declared `InputRequiredResult` return is rejected at registration (`InvalidSignature`), and an undeclared one fails the call at runtime. The manual form is the **low-level** `Server`, whose `on_call_tool` handler is allowed to return either result type:
|
||||
|
||||
```python title="server.py" hl_lines="44-47"
|
||||
--8<-- "docs_src/mrtr/tutorial001.py"
|
||||
|
|
|
|||
|
|
@ -123,19 +123,21 @@ That's the right default for a precondition: no answer, no order. When declining
|
|||
answers it, and the `Client` retries the call for you (**[Multi-round-trip requests](../advanced/multi-round-trip.md)**). On
|
||||
**2025-11-25** and earlier it is a synchronous elicitation request mid-call. Each question is
|
||||
asked exactly once per call - a guarantee about the question, not the resolver. In the
|
||||
multi-round-trip form an eliciting resolver runs again to consume its answer, so code before
|
||||
its `return Elicit(...)` runs on the asking round and again on the answering one; a resolver
|
||||
that answered *without* asking, like `check_stock`, may run again whenever the call resumes
|
||||
after a question. When it resumes, each answer is matched back to its question, so an
|
||||
eliciting resolver must derive its question deterministically from the tool's arguments and
|
||||
earlier answers - a per-call generated value (a `default_factory` id, a timestamp) is
|
||||
re-derived on each round and must not appear in a question the answer is meant to bind to.
|
||||
multi-round-trip form any resolver may run again whenever the call resumes after a question,
|
||||
so code before a `return Elicit(...)` runs on each of those rounds; the recorded answer then
|
||||
satisfies the repeated question without prompting the user again. A recorded answer is only
|
||||
ever consulted when the resolver asks; a resolver that answers *without* asking, like
|
||||
`check_stock`, always supplies its own computed value. Because each answer is matched back to
|
||||
its question, an eliciting resolver must derive its question deterministically from the
|
||||
tool's arguments and earlier answers. A per-call generated value (a `default_factory` id, a
|
||||
timestamp) is re-derived on each round and must not appear in a question the answer is meant
|
||||
to bind to.
|
||||
|
||||
## Recap
|
||||
|
||||
* `Annotated[T, Resolve(fn)]` on a tool parameter: the SDK runs `fn` and injects its return value.
|
||||
* A resolved parameter is invisible to the model and cannot be supplied by a client. Values the model must not invent - prices, identities, permissions - belong here.
|
||||
* A resolver's parameters are resolved the same way: the `Context`, another `Resolve(...)`, or a tool argument by name. The graph runs each resolver at most once per round, however many consumers it has; each question is asked exactly once, an eliciting resolver runs again to consume its answer, and a resolver that never asked may run again when a call resumes.
|
||||
* A resolver's parameters are resolved the same way: the `Context`, another `Resolve(...)`, or a tool argument by name. The graph runs each resolver at most once per round, however many consumers it has; each question is asked exactly once, and any resolver may run again when a call resumes after a question.
|
||||
* Bad graphs fail at registration with `InvalidSignature`, not mid-call.
|
||||
* Return `Elicit(message, Model)` to ask the user, only when you have to. Unwrapped annotations abort on decline; `ElicitationResult[T]` lets the tool branch.
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue