Skip to content
Chapter 7 of 7 · ElicitationPrompts per operation

13 min read

Prompts per operation

On this page

The measurement that makes this concrete is a funnel — the count of users still present at each successive step of an operation. It costs nothing to build for a server that already logs its elicitations, because every prompt sent and every response received is already an event, and the count that matters is how many operations reach step n against how many reached step n − 1.

Here is one, from a migrate_workspace tool that moves collections, dashboards and saved queries from one workspace into another and elicits four times inside a single call: the source workspace, the target workspace, a conflict strategy, and a final Proceed with migration?. Of 1,204 invocations, 1,204 reached the first dialog, 1,101 answered it, 968 answered the second, 604 answered the third, 478 reached the confirmation, and 402 completed. Two-thirds of the people who asked for a migration did not get one. The median elapsed time from the first dialog to a completed migration was four minutes and twelve seconds, of which roughly nine seconds was the migration.

Those six counts are worth turning into five ratios, because the ratios are what a design is accountable for. About nine in ten of the people who saw the first dialog answered it, and about nine in ten of those answered the second; the third kept a little under two thirds, the fourth about four fifths, and the last about five sixths. The five multiply back to four hundred and two out of one thousand two hundred and four, which they must, because each count is a subset of the one above it — and that nesting is the property worth confirming before trusting any funnel, since it says nobody was lost between the steps, only at them. What follows is that the end figure can only be raised by raising one of the five ratios. Removing a step raises the product by the whole of whatever that step was contributing, which is why the count of prompts, rather than the wording of any one of them, is the lever.

The largest single drop is at the third dialog, where 364 people stopped. That dialog asks whether conflicting items should be skipped, overwritten, or duplicated with a suffix, and the reason people stall on it is straightforward once you look at what they were shown: a three-option enum, with no indication of whether there are any conflicts, how many, or which items are involved. It is asking for a decision about information the user does not have and the dialog does not carry.

Sorting the four questions

Chapter 2 divided interruptions into three rows — a precise error for a value something you hold already knows, an elicitation for a value only the user has, and the destructive annotation for approval. Running each of the four dialogs through those rows disposes of most of them.

DialogWhat is being askedWhere the answer livesRow
1. Source workspacea valuein the user's request, already in the model's contextprecise error, or a required argument
2. Target workspacea valuesameprecise error, or a required argument
3. Conflict strategya valuewith the user, but only once conflicts are knownelicitation, conditionally
4. Proceed?approvalin the host's permission modeldestructiveHint

Dialogs 1 and 2 are chapter 1's opening case with different names. A user who types "move everything from acme-staging into acme-prod" has put both values in front of the model, and a tool that interrupts them to retype acme-staging is charging a person for a schema that failed to ask for it. Both belong in the tool's inputSchema as required arguments with descriptions good enough that the model populates them, and when one is genuinely absent, an error naming the argument and listing the workspaces the user can reach costs a model turn rather than a minute of attention.

Dialog 4 has no value in it at all. Migration overwrites data in a production workspace, which is what destructiveHint: true on the tool definition exists to declare, and the host that already prompts for tool approval will gate it with the tool name and the arguments visible — which is more than Proceed with migration? ever showed.

Dialog 3 is the one that survives the sort, and it survives conditionally. It is a real choice with real consequences, nothing in the model's context establishes which the user wants, and it is exactly the kind of preference chapter 1 said was worth a prompt. What disqualifies it in its current form is timing: it is asked before the migration has looked at anything, so at the moment the question appears, neither the user nor the server knows whether there is a single conflict to resolve. In this system 82 percent of migrations have no conflicting items at all, which means four out of five of those 604 answered dialogs decided nothing.

Two of those figures carry further as counts than as percentages. Four fifths of six hundred and four answered third dialogs is close to five hundred interruptions that changed nothing about what the tool then did: a decision collected, rendered, read and returned, against a conflict set that was empty. And four minutes and twelve seconds against nine seconds of actual migration means better than nineteen twentieths of a successful operation's elapsed time is the tool waiting for a person. Neither number is an argument for removing prompts as such. Both are the reason prompts have to be counted, because an operation whose duration is dominated by its questions is an operation whose design lives in the questions.

Ownership is necessary and was never sufficient

Answering the defence on its own terms is the useful exercise. The defence is that all four values are genuinely the user's to choose, and on dialogs 1 through 3 that is arguable. Two other things have to hold, and this design fails both.

The first is chapter 1's second condition: the answer must be the last thing standing between the operation and success. After the user answers dialog 1, dialog 2 appears; after dialog 2, dialog 3. Every one of the first three prompts is followed by another prompt, which is what makes the abandonment curve look the way it does. Each dialog is an independent opportunity to leave, so the completion rate is a product rather than a sum — four steps at 90 percent apiece land at 66 percent, and this tool's steps were worse than 90 percent.

The second is that elicitation is the most expensive channel available for collecting a value, and a value the user owns can usually be collected somewhere cheaper. A workspace name the user has already said can come in as a tool argument. A conflict strategy the user has expressed a preference about before can be stored and read on the next migration. A question the model asks in the transcript reaches every client, is visible when the user scrolls back, and costs no capability. Elicitation buys one thing those cannot: the ability to pause an operation that is already underway and holds state you cannot cheaply rebuild. Spending it on values that were available before the operation started is spending it where it has no advantage.

How the prompts are rendered compounds this, and it is outside your control. The specification says nothing about presentation, so one client shows an elicitation as an inline card in the transcript that the user can ignore and come back to, another as a side panel, and another as an application-modal window that takes focus and blocks the rest of the interface. A design that assumes the gentlest of those behaviours will be deployed against the harshest, and a sequence of four prompts is the design most exposed to the difference: an inline card four times is mildly annoying, and four consecutive modals is an interrogation. The number of prompts is the only part of that you decide.

Putting all four into a single flat elicitation improves the numbers and does not fix the design. The schema restriction from chapter 4 is the first obstacle: users who have hit conflicts want per-collection decisions — keep the staging version of revenue_daily, overwrite user_events, duplicate the rest — and a flat object of primitives cannot express a list of per-item choices. Any single-dialog version therefore forces one strategy across every conflicting item, which is the thing the users complaining about the tool were complaining about. The deeper problem survives the merge untouched: four decisions demanded before anything has happened is the same interruption compressed onto one screen, and the conflict strategy is still being chosen blind.

Let the operation produce the information the question needs

The redesign splits the tool in two, into the shape usually called plan then apply. A planning call, which writes nothing, does the work that generates the information, returns it as an ordinary tool result, and holds a plan for a short window. An apply call takes the decisions as arguments and performs the change.

python
@mcp.tool(annotations={"readOnlyHint": True})
async def plan_migration(source: str, target: str) -> dict:
    """Compare two workspaces and report what a migration would change.
    Nothing is written. Returns a plan_id valid for 30 minutes."""
    plan = await migrations.plan(source, target)
    return {
        "plan_id": plan.id,
        "expires_at": plan.expires_at.isoformat(),
        "will_create": len(plan.creates),
        "conflicts": [
            {"name": c.name, "kind": c.kind,
             "target_modified": c.target_modified.isoformat()}
            for c in plan.conflicts
        ],
    }

@mcp.tool(annotations={"destructiveHint": True, "idempotentHint": True})
async def apply_migration(
    plan_id: str,
    default_strategy: Literal["skip", "overwrite", "duplicate"] = "skip",
    per_item: dict[str, Literal["skip", "overwrite", "duplicate"]] | None = None,
) -> dict:
    ...

per_item is a nested object, which is legal here because tool arguments are full JSON Schema and only elicitation schemas are restricted. The model fills it from a conversation in which the user could see the conflict list, scroll back, and change their mind. default_strategy has a value that is right for the 82 percent of migrations with no conflicts, so the common case takes one call with two arguments and no questions.

Sequence: User → Model (move acme-staging into acme-prod); Model → Server (plan_migration(source, target)); Server → Model (41 to create, 3 conflicts (named, with dates)); Model → User (3 items already exist in acme-prod. Overwrite, skip, or duplicate?); User → Model (overwrite user_events, skip the rest); Model → Server (apply_migration(plan_id, "skip", {"user_events": "overwrite"})); Server → Model (41 created, 1 overwritten, 2 skipped).

The conversation is doing what the third dialog was doing, with the conflict list on screen, at a point where the user can answer per item. The confirmation is doing what the fourth dialog was doing, in the host's own approval flow.

The two frames in that exchange account for each other exactly, which is a property the four-dialog version had no way to offer. The plan named forty-one items to create and three conflicts; the result reports forty-one created, one overwritten and two skipped. Every conflict lands in exactly one of the two outcome columns, nothing appears in the result that was not in the plan, and there is no row left over. That is what makes a plan id worth holding for half an hour rather than recomputing on demand: it is a commitment about a set, so the apply result can be checked against it without comparing the two workspaces a second time, and a mismatch between them is a statement that the world moved rather than a statement that the migration misbehaved.

Four dialogsPlan then apply
Elicitations per migration40, or 1 when a conflict needs resolving mid-run
Conflict decision madebefore anything is inspectedafter the conflicts are named
Per-item strategyimpossiblea nested argument
Works without the elicitation capabilitynoyes
Completion rate33%the plan call cannot be abandoned; apply is one call
Where the user can scroll backnowhere, modal blocks itthe transcript

Zero prompts is not the target either

The conclusion to avoid is that elicitation is a smell and a well-designed server never asks. A server that removes every prompt by guessing has moved the failure somewhere less visible — a migration that silently skipped three items the user needed is worse than a dialog, and the invoice in the wrong currency from chapter 1 is a document somebody has to void.

There is still a place for a prompt inside apply_migration, and it is the one chapter 1 identified with the booked meeting room. If the plan has expired or the target changed since it was made, the operation is underway, holding a snapshot and a lock, and the question of whether to re-plan or abort is genuinely the user's with no answer anywhere in the system. That prompt is a single enum, it happens in perhaps one call in fifty, and the capability check and fallback from chapter 3 cover the clients that cannot answer it — the error names plan_id and tells the model to call plan_migration again.

On a client with no elicitation support, the redesigned tools work in full. Both calls are ordinary tool calls, the plan comes back as text the model reads to the user, and the decisions arrive as arguments. That property is the more valuable outcome of the exercise: the original tool did not function at all on a non-eliciting client, and the version that does is also the version people finish.

A useful audit takes about an hour on a server you already run. For each tool that elicits, count the prompts per successful operation, and for each prompt name which of chapter 2's three rows it is in. Check that a capability test precedes every one of them. Check that decline and cancel are distinguished, that a deadline exists, and that whatever the operation holds is released on all of the terminal paths. Then look at the accept branch and confirm the value is validated on arrival rather than trusted because the schema said so.

The rule that has been running through this topic is about which questions exist, not how few: ask when the user owns a value nothing you hold can supply, ask at a point where the answer completes the operation, ask once, and have a defined behaviour for the clients that cannot answer. A count of four is not wrong because four is a large number; it is wrong because three of the four failed one of those tests, and the fourth was asked before the information that would answer it existed. The tools that come out of an audit with more than one prompt are usually the tools whose arguments are wrong, and fixing the arguments is what removes the prompts.

For deeper reference on the request and response shapes, FastMCP's User Elicitation documentation is the most complete practical treatment, and the Agentic AI Foundation session Beyond API Wrappers: Workflow-Based MCP Servers with Elicitation and Sampling works through the plan-then-apply restructuring on larger examples than the one here.

Elicitation — read the current version at https://daviddominguez.dev/learn/en/elicitation/prompts-per-operation

Back to top