|
2 | 2 |
|
3 | 3 | A tool that is halfway through its job and missing one answer doesn't have to fail. |
4 | 4 |
|
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. |
6 | 6 |
|
7 | 7 | There are two modes: |
8 | 8 |
|
9 | 9 | * **Form mode**: you need a value (a confirmation, a date, a quantity). You describe the fields, the client renders the form. |
10 | 10 | * **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. |
11 | 11 |
|
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. |
13 | 13 |
|
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: |
15 | 46 |
|
16 | 47 | ```python title="server.py" hl_lines="9-11 20-23 25" |
17 | 48 | --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 |
79 | 110 | `"maybe"` for a `bool` doesn't corrupt your booking: the call fails with a |
80 | 111 | schema-mismatch error, your `if` never runs. |
81 | 112 |
|
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 | | - |
100 | 113 | ## Send the user to a URL |
101 | 114 |
|
102 | 115 | 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( |
132 | 145 |
|
133 | 146 | ### Try it |
134 | 147 |
|
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. |
136 | 149 |
|
137 | 150 | The callback prints the question it was sent: |
138 | 151 |
|
@@ -162,10 +175,10 @@ Now swap in the URL-mode `server.py` and point the same `main()` at `pay_deposit |
162 | 175 |
|
163 | 176 | ## Recap |
164 | 177 |
|
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. |
166 | 179 | * The schema is a flat Pydantic model: primitive fields only, validated on the way back. |
167 | 180 | * `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. |
169 | 182 | * The client answers with one `elicitation_callback`, branching on the params type; registering it is what declares the capability. |
170 | 183 | * 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)**. |
171 | 184 |
|
|
0 commit comments