|
1 | 1 | # Asking for input |
2 | 2 |
|
3 | 3 | Some handlers cannot finish in one go: they need the user to confirm something, fill in a |
4 | | -form, name a directory, or have the client's model draft a paragraph. The way to write that |
5 | | -is to **return** the ask — an `InputRequiredResult` naming what you need — and read the |
6 | | -answer off `RequestContext` when the call comes back. |
| 4 | +form, name a directory, or have the client's model draft a paragraph. There are two ways to |
| 5 | +write that: ask for it, or return the ask. |
7 | 6 |
|
8 | | -Write it that way once and it serves both [protocol eras](../protocol-versions.md). |
9 | | -Revision `2026-07-28` has no server-initiated requests at all, so the client retries the |
10 | | -original call carrying the answers; the specification calls that a multi round-trip request |
11 | | -(MRTR). On a handshake-era connection the SDK fulfils the same ask over that connection's own |
12 | | -channel instead. Your handler does not fork on which — see |
13 | | -[What a handler forks on](#what-a-handler-forks-on). |
| 7 | +## Just asking |
| 8 | + |
| 9 | +For elicitation, ask and use the answer: |
| 10 | + |
| 11 | +```php |
| 12 | +static function (RequestContext $context): string { |
| 13 | + $answer = $context->getClientGateway()->elicit('Your name?', $schema, key: 'who'); |
| 14 | + |
| 15 | + return "Hello, {$answer->content['name']}!"; |
| 16 | +} |
| 17 | +``` |
| 18 | + |
| 19 | +`key` names an ask, so its answer keeps finding the question it belongs to. Leave it out and |
| 20 | +asks are keyed by position — `elicitation_1`, `elicitation_2`, … — which holds as long as the |
| 21 | +handler reaches them in the same order every time. |
| 22 | + |
| 23 | +**Write the handler so it can run more than once.** Some clients answer inside the open |
| 24 | +request; others answer by re-sending the whole call, which enters your handler again from the |
| 25 | +top, once per question. Everything above an ask therefore has to be safe to repeat — put side |
| 26 | +effects after the last one, and re-derive where you are from the answers rather than from |
| 27 | +anything you kept. |
| 28 | + |
| 29 | +Answers given in an earlier round travel in the [`requestState`](#requeststate), so a handler |
| 30 | +asking more than once needs `Builder::setRequestState()` configured. |
| 31 | +[`examples/server/elicitation`](https://github.com/modelcontextprotocol/php-sdk/tree/main/examples/server/elicitation) |
| 32 | +is written this way. |
| 33 | + |
| 34 | +## Returning the ask |
| 35 | + |
| 36 | +The explicit form: **return** an `InputRequiredResult` naming what you need, and read the |
| 37 | +answer off `RequestContext` when the call comes back. It is more to write, and it is the only |
| 38 | +way to ask several things in **one** round trip, to carry your own state, or to ask for |
| 39 | +anything other than elicitation. |
| 40 | + |
| 41 | +> **Revision `2026-07-28`.** Multi round-trip requests (MRTR) are that revision's feature, and |
| 42 | +> only there does the protocol itself carry this shape. Over a handshake-era connection the SDK |
| 43 | +> emulates it: the input-required shim sends each ask as the real `elicitation/create` / |
| 44 | +> `sampling/createMessage` / `roots/list` and re-enters your handler with the answers. That is |
| 45 | +> on by default and bounded by `setInputRequiredLimits()` — each round holds the originating |
| 46 | +> request open, so it holds a worker for as long as the user takes — and |
| 47 | +> `withoutInputRequiredShim()` turns it off, after which such a handler fails there. See |
| 48 | +> [Server builder](../run/server-builder.md). |
14 | 49 |
|
15 | 50 | ```php |
16 | 51 | use Mcp\Schema\Result\CallToolResult; |
@@ -63,49 +98,14 @@ form mode only. |
63 | 98 |
|
64 | 99 | ## What not to call |
65 | 100 |
|
66 | | -`ClientGateway::sample()`, `elicit()`, `elicitUrl()` and `listRoots()` belong to the |
67 | | -handshake era. Calling one under this revision raises a `LogicException` naming |
68 | | -`InputRequiredResult` as the replacement. |
69 | | - |
70 | | -## What a handler forks on |
71 | | - |
72 | | -Nothing. Tools, resources, prompts, structured output, progress and errors do not care |
73 | | -which era called, and neither does the one thing that looks like it should: **asking the |
74 | | -user something**. |
75 | | - |
76 | | -Write it the 2026-07-28 way — return an `InputRequiredResult` naming what you need, read the |
77 | | -answer off `RequestContext::getInputContext()` when the call comes back. On a handshake-era |
78 | | -connection the SDK's input-required shim fulfils the same ask over that connection's own |
79 | | -channel: each embedded request goes out as the real `elicitation/create` / |
80 | | -`sampling/createMessage` / `roots/list`, and the handler is re-entered with the answers under |
81 | | -the keys it asked for. It is on by default; |
82 | | -[`examples/server/elicitation`](https://github.com/modelcontextprotocol/php-sdk/tree/main/examples/server/elicitation) |
83 | | -and |
84 | | -[`examples/server/client-communication`](https://github.com/modelcontextprotocol/php-sdk/tree/main/examples/server/client-communication) |
85 | | -are written this way and name no era anywhere. |
| 101 | +`ClientGateway::sample()` and `listRoots()` belong to the handshake era — revision |
| 102 | +`2026-07-28` removed both outright, so calling one there raises a `LogicException`. Take what |
| 103 | +they gave you from tool arguments, resource URIs or server configuration instead. `elicit()` |
| 104 | +and `elicitUrl()` are unaffected: elicitation survived that revision, as an ask carried in the |
| 105 | +result. |
86 | 106 |
|
87 | | -Two things to know about it. |
88 | | - |
89 | | -**Re-entry is re-execution.** The handler runs again from the top each round, so it has to |
90 | | -re-derive where it is from what came back rather than from anything it kept. That is already |
91 | | -true of the modern era — the client retries the whole call there — so a portable handler is |
92 | | -written that way regardless. It is only new if you were relying on `ClientGateway::elicit()` |
93 | | -suspending mid-body and keeping your locals; that keeps working untouched, since nothing here |
94 | | -runs unless a handler *returns* an ask. |
95 | | - |
96 | | -**Each round holds the request open.** The shim waits for the client's answer inside the |
97 | | -originating request, which on a process-per-request runtime means it holds a worker for as |
98 | | -long as the user takes. That is the same cost `ClientGateway::elicit()` already pays on that |
99 | | -leg, but the shim makes it reachable from handlers that never mention it — so size |
100 | | -`setInputRequiredLimits()` against your pool. |
101 | | - |
102 | | -```php |
103 | | -$server = Server::builder() |
104 | | - ->setServerInfo('My Server', '1.0.0') |
105 | | - // Re-entries per request, and seconds to wait for one answer. |
106 | | - ->setInputRequiredLimits(maxRounds: 4, roundTimeout: 120) |
107 | | - ->build(); |
108 | | -``` |
| 107 | +## Which revision called |
109 | 108 |
|
110 | | -`withoutInputRequiredShim()` turns it off, so such a handler fails on a handshake-era |
111 | | -connection instead of being fulfilled behind your back. |
| 109 | +Nothing above forks on it. `elicit()` works the same on every revision — only the mechanics |
| 110 | +underneath differ, and the SDK picks them. The one thing to keep in mind is the rule already |
| 111 | +stated: a handler that asks may be entered again from the top, so let it repeat safely. |
0 commit comments