You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Consult request_state only for the question a resolver is asking
The resolver framework restored recorded outcomes from the client-echoed
request_state before running the resolver body, so an entry under a
resolver's wire key could stand in for the body's own computation on a
branch that never asks, and a decline/cancel entry could abort a resolver
with no Elicit arm at all. Restore now happens only inside _elicit, after
the body returned an Elicit this round: the body's concrete return always
wins, and a recorded answer satisfies exactly the question being asked,
validated against the live Elicit.schema.
That makes the statically-extracted elicit_schema unused, so it is
removed - which also fixes resolvers annotated `-> Elicit[BaseModel]`
(dynamic create_model schemas): restoring through the static schema
called BaseModel.model_validate, whose PydanticUserError is not a
ValidationError and hard-failed the call after the user had answered.
A tool combining Resolve(...) parameters with a hand-rolled
InputRequiredResult body flow shares the call's single
input_responses/request_state channel; the flows overwrite each other's
state and the call never converges. A declared InputRequiredResult
return arm is now rejected at registration (InvalidSignature, seeing
through Annotated wrappers, type aliases, and unions) and an undeclared
one fails the call at runtime.
The cost of body-wins is that an eliciting resolver's body re-runs on
each round instead of being restored without running; the docs scope
the once-per-call guarantee to the question accordingly.
Copy file name to clipboardExpand all lines: docs/advanced/multi-round-trip.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -19,7 +19,7 @@ That's the whole protocol. Every leg is an ordinary request from the client to t
19
19
20
20
## The server side
21
21
22
-
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:
22
+
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:
Copy file name to clipboardExpand all lines: docs/tutorial/dependencies.md
+10-8Lines changed: 10 additions & 8 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -123,19 +123,21 @@ That's the right default for a precondition: no answer, no order. When declining
123
123
answers it, and the `Client` retries the call for you (**[Multi-round-trip requests](../advanced/multi-round-trip.md)**). On
124
124
**2025-11-25** and earlier it is a synchronous elicitation request mid-call. Each question is
125
125
asked exactly once per call - a guarantee about the question, not the resolver. In the
126
-
multi-round-trip form an eliciting resolver runs again to consume its answer, so code before
127
-
its `return Elicit(...)` runs on the asking round and again on the answering one; a resolver
128
-
that answered *without* asking, like `check_stock`, may run again whenever the call resumes
129
-
after a question. When it resumes, each answer is matched back to its question, so an
130
-
eliciting resolver must derive its question deterministically from the tool's arguments and
131
-
earlier answers - a per-call generated value (a `default_factory` id, a timestamp) is
132
-
re-derived on each round and must not appear in a question the answer is meant to bind to.
126
+
multi-round-trip form any resolver may run again whenever the call resumes after a question,
127
+
so code before a `return Elicit(...)` runs on each of those rounds; the recorded answer then
128
+
satisfies the repeated question without prompting the user again. A recorded answer is only
129
+
ever consulted when the resolver asks - a resolver that answers *without* asking, like
130
+
`check_stock`, always supplies its own computed value. Because each answer is matched back to
131
+
its question, an eliciting resolver must derive its question deterministically from the
132
+
tool's arguments and earlier answers - a per-call generated value (a `default_factory` id, a
133
+
timestamp) is re-derived on each round and must not appear in a question the answer is meant
134
+
to bind to.
133
135
134
136
## Recap
135
137
136
138
*`Annotated[T, Resolve(fn)]` on a tool parameter: the SDK runs `fn` and injects its return value.
137
139
* 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.
138
-
* 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.
140
+
* 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.
139
141
* Bad graphs fail at registration with `InvalidSignature`, not mid-call.
140
142
* Return `Elicit(message, Model)` to ask the user, only when you have to. Unwrapped annotations abort on decline; `ElicitationResult[T]` lets the tool branch.
0 commit comments