Skip to content

Commit b6e4689

Browse files
committed
docs: lead the elicitation page with the resolver, not the legacy verb
The page taught ctx.elicit() first and unqualified, with the resolver second (under a heading about WHEN it runs, 'Ask before the tool runs') and the protocol-era constraint disclosed only in an info box inside the client section, 115 lines down. That is backwards. ctx.elicit() and ctx.elicit_url() are requests from the server to the client, a channel that only exists for a client on a legacy connection (spec 2025-11-25 and earlier); a Resolve-annotated parameter is deliberately era-portable -- the SDK sends elicitation/create on a legacy connection and a multi-round-trip result on a modern one, and the handler never knows the difference. The page now opens by naming both ways to ask and saying which one to reach for; 'Ask with a resolver' is the first section and states the portability out loud; and the two ctx verbs' section carries the legacy-connection warning at its top instead of a hundred lines later. The 'Try it' walkthrough named its server as 'the first one on this page', which the reorder would have silently falsified; it now names it. No example changes: the resolver example the page now leads with was already there and already CI-tested. It was just filed second.
1 parent e742faa commit b6e4689

1 file changed

Lines changed: 37 additions & 24 deletions

File tree

docs/handlers/elicitation.md

Lines changed: 37 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,47 @@
22

33
A tool that is halfway through its job and missing one answer doesn't have to fail.
44

5-
**Elicitation** lets it ask. In the middle of a tool call the server sends the client a question, the client puts it to the user, and the answer comes back into the same function call.
5+
**Elicitation** lets it ask. In the middle of a tool call the user gets a question, and their answer comes back into the same function call.
66

77
There are two modes:
88

99
* **Form mode**: you need a value (a confirmation, a date, a quantity). You describe the fields, the client renders the form.
1010
* **URL mode**: you need the user to go somewhere else (an OAuth consent screen, a payment page). Nothing they do there passes through the protocol.
1111

12-
## Ask with a form
12+
And there are two ways to ask. The one to reach for is a **resolver**: you hang the question on a parameter, and the SDK asks - on any connection, whatever protocol era the client speaks. The direct way, `await ctx.elicit(...)`, is a request from the *server* to the *client*, a channel that only exists for a client on a legacy connection (spec version 2025-11-25 or earlier). Both are on this page; start with the resolver.
1313

14-
`ctx.elicit()` takes a message and a Pydantic model:
14+
## Ask with a resolver
15+
16+
A question that gates the whole tool - *are you sure? which of the three matching accounts?* - can be lifted out of the tool body into a **resolver**, and the framework asks it for you.
17+
18+
A parameter annotated `Annotated[T, Resolve(fn)]` is filled by running `fn` before the tool body. The resolver returns the value directly when it already knows it, or returns `Elicit(...)` to have the framework ask:
19+
20+
```python title="server.py" hl_lines="24-30 35-36"
21+
--8<-- "docs_src/elicitation/tutorial004.py"
22+
```
23+
24+
* `confirm_delete` reads the tool's own `path` argument by name, lists the folder, and **only elicits when it must** - an empty folder resolves to `Confirm(ok=True)` with no round-trip to the client.
25+
* `delete_folder` annotates `ElicitationResult[Confirm]`, so the framework injects the whole outcome and the tool `match`es every case: accept-and-confirm, accept-but-keep (`ok=False`), decline, cancel.
26+
* The `confirm` parameter never appears in the tool's input schema - the client supplies `path`, the resolver supplies `confirm`.
27+
28+
Annotate the unwrapped model (`Annotated[Confirm, Resolve(confirm_delete)]`) instead when the tool doesn't need to branch: it receives the model on accept and the call aborts with an error on decline or cancel.
29+
30+
A resolver works on **every** connection. For a client on a legacy connection the SDK sends it the question directly; on a **2026-07-28** connection the SDK *returns* the question from the call, and the client's next attempt carries the answer. Your resolver never knows the difference; what happens underneath is **[Multi-round-trip requests](multi-round-trip.md)**.
31+
32+
Asking is only one thing a resolver can do. The general mechanism - dependencies that compute without asking, dependencies of dependencies, what the model can and cannot supply - is the **[Dependencies](dependencies.md)** page.
33+
34+
## Ask from inside the tool
35+
36+
A tool can also stop in the middle of its own body and ask.
37+
38+
!!! warning
39+
`ctx.elicit()` and `ctx.elicit_url()` are requests from the *server* to the *client* - a
40+
channel that only exists for a client on a legacy connection (spec version **2025-11-25**
41+
or earlier). On a **2026-07-28** connection there are no server-initiated requests, so
42+
these calls fail. A resolver works on both. **[Protocol versions](../protocol-versions.md)**
43+
has the whole story.
44+
45+
`await ctx.elicit()` takes a message and a Pydantic model:
1546

1647
```python title="server.py" hl_lines="9-11 20-23 25"
1748
--8<-- "docs_src/elicitation/tutorial001.py"
@@ -79,24 +110,6 @@ A refusal is not an error. The tool decides what declining means (here, no booki
79110
`"maybe"` for a `bool` doesn't corrupt your booking: the call fails with a
80111
schema-mismatch error, your `if` never runs.
81112

82-
## Ask before the tool runs
83-
84-
The booking tool above weaves the question into its own body. When the question is really a *precondition* - confirm before deleting, authenticate before acting - you can lift it out of the tool into a **resolver** and let the framework ask for you.
85-
86-
A parameter annotated `Annotated[T, Resolve(fn)]` is filled by running `fn` before the tool body. The resolver returns the value directly when it already knows it, or returns `Elicit(...)` to have the framework ask:
87-
88-
```python title="server.py" hl_lines="24-30 35-36"
89-
--8<-- "docs_src/elicitation/tutorial004.py"
90-
```
91-
92-
* `confirm_delete` reads the tool's own `path` argument by name, lists the folder, and **only elicits when it must** - an empty folder resolves to `Confirm(ok=True)` with no round-trip to the client.
93-
* `delete_folder` annotates `ElicitationResult[Confirm]`, so the framework injects the whole outcome and the tool `match`es every case: accept-and-confirm, accept-but-keep (`ok=False`), decline, cancel.
94-
* The `confirm` parameter never appears in the tool's input schema - the client supplies `path`, the resolver supplies `confirm`.
95-
96-
Annotate the unwrapped model (`Annotated[Confirm, Resolve(confirm_delete)]`) instead when the tool doesn't need to branch: it receives the model on accept and the call aborts with an error on decline or cancel.
97-
98-
Asking is only one thing a resolver can do. The general mechanism - dependencies that compute without asking, dependencies of dependencies, what the model can and cannot supply - is the **[Dependencies](dependencies.md)** page.
99-
100113
## Send the user to a URL
101114

102115
Some things must not go through the model or the client: credentials, card numbers, OAuth consent. For those you don't ask for data; you ask the user to go somewhere:
@@ -132,7 +145,7 @@ Servers ask. Clients answer by passing an **`elicitation_callback`** to `Client(
132145

133146
### Try it
134147

135-
Start the form-mode `server.py` (the first one on this page) on Streamable HTTP (**[Running your server](../run/index.md)** has the one-liner), then run the client's `main()` and ask `book_table` for Christmas day.
148+
Start the `ctx.elicit` form-mode `server.py` (the `book_table` one) on Streamable HTTP (**[Running your server](../run/index.md)** has the one-liner), then run the client's `main()` and ask `book_table` for Christmas day.
136149

137150
The callback prints the question it was sent:
138151

@@ -162,10 +175,10 @@ Now swap in the URL-mode `server.py` and point the same `main()` at `pay_deposit
162175

163176
## Recap
164177

165-
* `await ctx.elicit(message, schema=Model)` asks mid-call; your tool resumes with the answer.
178+
* A parameter annotated `Annotated[T, Resolve(fn)]` is filled by a resolver, which returns `Elicit(...)` when it has to ask. It works on every connection.
166179
* The schema is a flat Pydantic model: primitive fields only, validated on the way back.
167180
* `result.action` is `"accept"`, `"decline"` or `"cancel"`; `result.data` exists only on accept.
168-
* `await ctx.elicit_url(message, url, elicitation_id)` is for everything that must not pass through the model; `ctx.session.send_elicit_complete(elicitation_id)` says the out-of-band part is done.
181+
* `await ctx.elicit(message, schema=Model)` asks from inside the tool body, and `await ctx.elicit_url(message, url, elicitation_id)` is for everything that must not pass through the model (`ctx.session.send_elicit_complete(elicitation_id)` says the out-of-band part is done). Both are server-to-client requests: they need the client on a legacy connection.
169182
* The client answers with one `elicitation_callback`, branching on the params type; registering it is what declares the capability.
170183
* On a 2026-07-28 connection the server returns the question instead of pushing it; the same callback is fed by **[Multi-round-trip requests](multi-round-trip.md)**.
171184

0 commit comments

Comments
 (0)