AIARTICLE

How to use MCP elicitation to make the agent pause and ask the user for data

The 2025-06-18 revision of the Model Context Protocol specification formalized elicitation, the mechanism that lets an MCP server pause a tool call and ask the user directly for missing data. See how the flow works under the hood and what to consider before implementing it.

How to use MCP elicitation to make the agent pause and ask the user for data
Image: Alan Andrade
Article · AI
Reading
7 min
Tables
1
Code
4

The 2025-06-18 revision of the Model Context Protocol specification formalized elicitation, the mechanism that lets an MCP server pause a tool call and ask the user directly for missing data. See how the flow works under the hood and what to consider before implementing it.

The 2025-06-18 revision of the Model Context Protocol specification formalized elicitation, the mechanism that lets an MCP server pause a tool call and ask the user directly for missing data. See how the flow works under the hood and what to consider before implementing it.

Anyone who has already built a simple MCP server has run into this problem: a tool needs a piece of data that wasn't included in the call, and the server only has two bad options, guess a value or return an error. The Model Context Protocol specification solves this with a utility called elicitation, described in the 2025-06-18 revision of the official documentation (published in June 2025). It lets the server pause execution midway through and ask, in a structured way, for exactly the data that's missing.

What changes with elicitation

Before this mechanism, the flow of a tool call in MCP was one-way: the client calls, the server processes what it received, and returns a result. Elicitation introduces a pause in the middle of that flow. The server sends an elicitation/create request back to the client, the client decides how to present this to the user (a form, a dialog, whatever makes sense in the interface), and the response goes back to the server to resume processing.

The specification makes clear that the protocol doesn't dictate the interface: "Implementations are free to expose elicitation through any interface pattern that suits their needs." In other words, the visual design is up to the client (IDE, chat app, CLI); the server just needs to speak the protocol correctly.

The handshake: elicitation is a capability, not a guarantee

Not every MCP client supports elicitation. The specification requires clients that support the feature to declare it explicitly during initialization:

json
{
 "capabilities": {
 "elicitation": {}
 }
}

This has a direct practical consequence: before calling elicitation/create, the server should check whether the client declared this capability in the initial handshake. If it didn't, calling the method has no reliable effect, and the server needs a fallback path, such as using a default value or simply refusing to execute with a clear error message.

Anatomy of an elicitation/create request

The simplest example in the specification asks for a plain piece of text:

json
{
 "jsonrpc": "2.0",
 "id": 1,
 "method": "elicitation/create",
 "params": {
 "message": "Please provide your GitHub username",
 "requestedSchema": {
 "type": "object",
 "properties": { "name": { "type": "string" } },
 "required": ["name"]
 }
 }
}

The accept response returns the content already validated against the schema:

json
{ "result": { "action": "accept", "content": { "name": "octocat" } } }

The same mechanism works for more structured requests, like name, email, and age in a single request, as long as all the fields are primitives and sit at the same level of the object.

Why the schema is restricted to primitive types

The specification deliberately limits requestedSchema to flat objects with primitive properties. This means arrays of objects, nested structures, and other advanced JSON Schema features are left out on purpose, to simplify implementation on the client side, which needs to generate a form from that schema without having to guess the layout.

TypeWhat it acceptsExtra fields
stringfree textminLength, maxLength, format (email, uri, date, date-time)
number / integernumeric valuesminimum, maximum
booleantrue/falsedefault
enumclosed list of optionsenumNames for friendly labels

In practice, this means that a request for a full address, for example, turns into several string fields at the same level (street, number, city) instead of a nested address object. If the missing data is genuinely complex, the way forward is to break it into more than one elicitation round, not force the schema.

The three possible responses: accept, decline, cancel

The specification explicitly separates three actions, and handling all three differently is part of the contract:

  • Accept: the user filled in and confirmed; content carries the data validated against the schema.
  • Decline: the user explicitly refused (clicked "Reject" or equivalent); content is usually not included.
  • Cancel: the user closed the dialog without deciding anything (Escape, clicking outside, closing the window).

The document recommends distinct handling for each case: process the data on accept, offer an alternative on decline, and on cancel consider asking again later instead of treating it as a definitive refusal. A server that handles decline and cancel the same way loses information about the user's intent.

Implementing it on the server: the roadmap

Editor's note: the code in this section was not run in a real environment. Validate it before using it in production.

In practice, implementing this flow on the server side means following the roadmap the specification itself describes: serialize the elicitation/create message, wait for the response before proceeding, and handle the returned object with action and content. The official Python and TypeScript SDKs should offer some way to encapsulate this roadmap, but it's worth checking the documentation for the installed version, since the exact API and method names can vary between releases. The conceptual roadmap, regardless of the exact name of the call, is always the same:

python
# Flow pseudocode, based on the JSON-RPC structure from the specification
async def create_issue(ctx, title: str, repo: str | None = None):
 if not repo:
 schema = {
 "type": "object",
 "properties": {
 "repo": {"type": "string", "description": "owner/nome do repositório"}
 },
 "required": ["repo"]
 }
 response = await ctx.elicit(message="Qual repositório devo usar?", requestedSchema=schema)
 if response["action"] != "accept":
 return "Operação cancelada pelo usuário."
 repo = response["content"]["repo"]
 # proceed with repo filled in

In TypeScript, the equivalent logic would follow the same roadmap: wait for the response before proceeding and check the result's action field before using content. The central point, in any SDK, is to treat the call as blocking: the tool shouldn't keep processing with incomplete data just because the response hasn't arrived yet.

Testing locally with a compatible client

To see the flow working end to end, you need an MCP client (host) that declares elicitation in the handshake. It's worth checking the capabilities object returned at initialization before trusting that the dialog will appear: if the key isn't there, the server is talking to itself. In a typical local test, the way to go is to spin up the MCP server pointing to a compatible host running on the same machine and deliberately trigger all three response paths, accept, decline, and cancel, to make sure each one has sensible behavior in the code, instead of assuming only the happy path (accept) will happen.

What the protocol doesn't leave open to negotiation

The specification treats part of this as a security rule, not a style suggestion:

Servers MUST NOT use elicitation to request sensitive information.

Official Model Context Protocol specification, revision 2025-06-18

Passwords, tokens, and secrets are out of scope for elicitation by definition, the flow was designed to fill a gap in functional data (repository name, email, confirming a choice), not to replace an authentication flow. Beyond that, the specification recommends that clients implement rate limiting, make clear which server is requesting the information, and allow reviewing the response before sending it, and that both sides, client and server, validate the content against the agreed schema.

When elicitation isn't the right answer

Elicitation solves one specific problem well: simple, structured data missing from a tool call. Outside that case, it's worth thinking twice. Sensitive data is forbidden by the specification, not just discouraged. Nested data structures or lists of objects don't fit the restricted schema, so they'd require several sequential rounds, which can make the experience feel drawn out. And, since not every MCP client in use today declares the elicitation capability in the handshake, any server that relies on it as the only path risks silently getting stuck in environments that simply don't support the feature yet.

Source: Official Model Context Protocol specification, revision 2025-06-18, Elicitation utility (https://modelcontextprotocol.io/specification/2025-06-18/client/elicitation).

Translated from the Brazilian Portuguese original · Read the original

View profile →