Chapter 1 of 7 · 12 min read
Elicitation and value ownership
On this page
Elicitation is a server asking the user for a value in the middle of handling a tool call. The handler
does not return; it sends a request back down the connection the tool call arrived on, the client renders a
small form from the schema attached to that request, the person answers, and the response comes back as
structured data that the same handler carries on with. A book_meeting_room tool starts, finds no room,
sends an elicitation/create request carrying the message "Which room?" and a one-field schema, receives
{"room_id": "mercury"} a few seconds later, and books the room, all inside the single tool call the model
made. The mechanism belongs to the Model Context Protocol (MCP), whose tool half topic 6.4 covers, and
the request runs from server to client, which is the reverse of the direction a tool call travels.
It exists because a tool call otherwise has exactly one source of arguments, which is the model. The model can supply only what it can read: the conversation, the records and files it has fetched, and the descriptions your server published. When a value exists nowhere except in the head of the person watching the screen, a server's options were to guess or to fail the call and hope the model thought to ask in the transcript, and failing the call throws away whatever the operation had already done, which for a half-written migration or an open transaction is expensive. Elicitation adds a third option, which is to pause, ask, and resume without unwinding.
Where you meet it is as a dialog whose presentation the client decides: a modal window on one desktop assistant, an inline card in the transcript on another, a sequence of line-by-line prompts on standard input in a terminal client. What the user sees is a question with one or two fields, arriving while the assistant's answer sits unfinished above it. What your server sees is a handler suspended mid-operation, still holding whatever it was holding, until an answer arrives.
Who owns the value
The question that decides between eliciting and erroring is not whether a required field is empty. It is whether the user is the only party who could fill it. A value has an owner, and the owner is whoever holds the information: sometimes your server, sometimes a record in a database you can read, sometimes the model's own context, and sometimes only the person sitting in front of the client. Elicitation is the mechanism for the last case, and it is a poor mechanism for the first three because it makes a human retype something a machine already had.
room in the booking example is owned by the model's context in the common case. The invite is in the
conversation, the room is in the invite, and what your server needs to do is tell the model precisely what it
is missing and what shape an acceptable answer takes. That is a tool-result problem, and it belongs to the
error-message discipline from topic 6.4 rather than to a dialog:
@mcp.tool()
async def book_meeting_room(
room_id: str | None = None,
start: str = "",
duration_minutes: int = 30,
) -> dict:
if room_id is None:
rooms = await catalogue.list_bookable(floor=None)
raise ToolError(
"room_id is required. Pass the room's identifier, not its display "
"name. If the meeting already has a location on the calendar "
"invite, use that. "
f"Known rooms: {', '.join(r.id for r in rooms[:12])}"
)That error costs one extra model turn. On a mid-sized model at the traffic that tool sees, that is around 900 additional input tokens and 1.4 seconds of wall clock, and it is invisible to the user, who never learns that the first attempt was underspecified. An elicitation in the same position costs 24 seconds of a person's attention, which is the median answer time measured on exactly this prompt, plus a 9 percent chance of stalling the run entirely when nobody answers at all. The two options are not close on this value, and the reason is entirely that the room name was recoverable from something the model already held.
Setting those two costs beside each other takes one division and one observation. Twenty-four seconds against one and a half is about seventeen times the wall clock, and that ratio is the least important part of the comparison. The second and a half belongs to a machine that is doing nothing else with it; the twenty-four seconds belongs to a person who was doing something else and now is not, and no exchange rate between those two quantities favours the dialog. The nine percent is what settles it. A prompt nobody answers does not degrade the operation, it ends the operation — the tool call stays open until something upstream gives up — so asking carries a failure rate of its own, and roughly one interruption in eleven fails outright.
Change the value and the arithmetic flips. A
create_invoice tool that cannot proceed because currency is missing is the
same surface failure with a different owner. If the customer record carries a
billing currency — most billing systems do — then the value is yours, you should
read it, and if you cannot read it for some reason then your error should say
currency is required and is not set on customer cus_8812; it must be one of EUR, GBP, USD.
If, on the other hand, this customer bills in whichever currency the deal was
struck in and nothing in your system records that, then no amount of context
lets the model derive it. Guessing produces an invoice in the wrong currency,
which is a legal document that somebody has to void. That is the shape of a
value worth interrupting somebody for.
| Room name on the invite | Currency not on the customer record | Which room, given three are free | |
|---|---|---|---|
| Who holds it | model's context | only the user | only the user |
| Cost of guessing wrong | a failed booking, retried | a voided invoice, manual correction | a booking in a room nobody wanted |
| Is recovery plausible | yes, from the invite | yes, from a three-option enum | yes, from a three-option enum |
| Right move | precise error naming the field | elicit | elicit |
The second condition in that table is doing as much work as the first. Elicitation is only worth reaching for when the answer, once you have it, actually completes the operation. If the user supplies the currency and the invoice still fails because the tax jurisdiction is unresolved, you have spent someone's attention and gained nothing, and you are about to spend it again. Before writing an elicitation, check that the value you are asking for is the last thing standing between the operation and success. When two or three things are missing at once, that is usually a sign the tool's arguments are wrong, which chapter 7, on the number of prompts one operation should contain, comes back to at length.
The belief worth dislodging before going any further is that asking is the conservative move — that a guess can be wrong and a question cannot. A question can be wrong in three separate ways, and the booking figures show all three. It can be redundant, costing attention to return a value the system already held. It can go unanswered, costing the whole operation rather than one field. And it can be answered without being read, because a person who has learned that these dialogs are usually pointless stops reading them, which is a cost paid by the next dialog rather than by this one. Guessing and asking are both options with failure rates attached, and the choice is which failure you would rather have, not whether to have one.
The already-booked room is a different problem
Now take the second failure mode of the same tool. The user asked for Mercury at 14:00, the model passed
room_id="mercury", and Mercury is taken by somebody else's stand-up. Nothing is missing. Every argument
arrived, correctly typed, and the operation still cannot proceed.
The instinct is to treat this as an error case, because it looks like every other conflict a backend service returns — a 409, a constraint violation, a "resource unavailable". Applying the rule honestly gives a different answer. There is a value the operation now needs that it did not need before, which is which alternative the user wants, and that value has an owner. Nothing in the model's context establishes whether this user would rather move to Venus on the same floor, take Mercury an hour later, or cancel because the whole point was the projector in Mercury. The room preference exists only in the user's head, and unlike the room name, it was never written down anywhere the model could read.
Recovery is also plausible in the strong sense, because the set of acceptable answers is small, closed, and known to your server at the moment of failure. You are not asking an open question; you are presenting three concrete options plus an escape hatch, and any of the four answers completes the operation:
conflict = await calendar.check(room_id, start, duration_minutes)
if conflict:
alternatives = await catalogue.free_at(start, duration_minutes, limit=3)
result = await ctx.elicit(
message=(
f"{conflict.room_name} is booked from {conflict.start:%H:%M} "
f"to {conflict.end:%H:%M} by {conflict.organiser}. "
"How would you like to proceed?"
),
schema=AlternativeChoice, # enum: the three room ids, plus "cancel"
)The contrast between the two failures of one tool is the whole of the rule in miniature. A missing room name is a value the model mislaid, and the repair is to tell the model where to find it. A booked room is a decision that never existed until the conflict happened, and there is no record anywhere that answers it. The first is a lookup, the second is a preference, and only preferences are worth a dialog.
There is one honest caveat. The three-option elicitation is better than a guess
and it is not automatically better than an error, because a good error can also
carry the alternatives — mercury is booked 14:00–14:30; venus and io are free
— and let the model raise the options in the conversation, where the user
answers in chat rather than in a modal. Which is better depends on the client
and on how destructive the operation is, and the general shape of that trade-off
is where the next chapter picks up. What the rule settles is that the conflict
case is at least a candidate for elicitation, whereas the missing-room-name case
never was.
Collecting a user-owned value earlier
None of this is a claim that missing arguments should generally produce errors. Most missing arguments should
not happen at all, because a tool whose required fields are routinely absent has a description problem or a
schema problem, and both of those were topic 6.4's subject. If a quarter of your calls arrive without
room_id, the first thing to examine is whether the parameter is named and described well enough for the
model to know it should have populated it from the invite. Elicitation applied to that situation is a patch
over a tool definition that never told the model what it needed, and it is a patch that charges the user for
the omission.
It is also not a claim that user-owned values must always be elicited at the moment they are discovered missing. A value the user owns can often be collected earlier and more cheaply — as a configuration setting read at startup, as a persisted preference from the last time this user answered the same question, or as part of the conversation, since the model is perfectly capable of asking "which currency?" in chat if your error tells it that the currency is the user's to choose. That last route costs no dialog at all and works on every client. Elicitation earns its place when the question arises mid-operation, after work has begun, in a state you cannot cheaply reconstruct if you abandon the call and start again — a half-written migration, an open transaction, a reserved slot.
Finally, the rule says nothing about whether the user wants to be asked. It is about who can answer. A user may well be happy to confirm the room, and asking them anyway is still the wrong call when the answer was already on their screen, because every unnecessary prompt trains people to dismiss prompts. A 9 percent no-answer rate on dialogs that are mostly redundant is not a fact about those dialogs alone: the ones that mattered were sitting in the same queue as the ones that did not.
Running the log through the same arithmetic gives the size of what was being spent. One thousand one hundred and forty dialogs at a median of twenty-four seconds is a little over seven and a half hours of somebody's attention, and the seven hundred and three redundant ones account for around four and three quarter hours of that — most of a working day, spread thinly across two months, spent retyping a string that was already on screen. The nine percent that were never answered comes to roughly a hundred abandoned runs. Neither figure needed new instrumentation, since both fall out of a log the server was already writing. The reason nobody had computed them is that a dialog appears in no dashboard as a cost, which is why a mechanism that fixed the metric everyone was watching survived two months without anyone asking what it charged.
Ownership is therefore the first question and completion is the second, and a value that fails either one has a cheaper home. The room name failed the first, because it was in the invite. A currency nothing records passes both, because nobody but the user can supply it and supplying it finishes the invoice. Working through those two questions before writing an elicitation costs a minute and removes most of the dialogs a server would otherwise ship.
That leaves an obvious gap. Confirmations look exactly like elicitations — a dialog appears, the user responds, the operation continues — and everything in this chapter was about values, while a confirmation asks for no value at all. The next chapter takes the two-way decision here and shows why it is really three-way.
Elicitation — read the current version at https://daviddominguez.dev/learn/en/elicitation/elicitation-and-value-ownership