Asking the user: elicitation

Model Context Protocol (MCP) · Advanced · 14 min

Confirmations and missing details should come from the user, not be guessed by the model.

Sometimes a tool needs something only the user can give: "Really delete 40 files?", or a choice the model shouldn't make. Elicitation lets a server ask the user through the host, which shows a small form.

There are two modes:

  • Form mode: the server sends a message and a simple flat JSON Schema (a few strings, numbers, booleans or choices). The user can accept, decline or cancel.
  • URL mode: the server sends a link the user opens in a browser, for anything sensitive such as entering a password, paying, or connecting another account. Servers must not collect secrets through form mode.

How it travels changed in 2026-07-28. Servers can no longer send requests to the client on their own. Instead they use multi round-trip requests: the tool returns an "input required" result containing its questions, the client asks the user, then repeats the original call with the answers attached. Sampling and roots requests use the same pattern.

In SDK v2 you rarely handle that by hand. Annotate a parameter with Resolve(fn): the SDK calls fn, which returns Elicit(message, Model), asks the user over whatever mechanism the connection supports (old or new protocol), and passes the answer in. The parameter is hidden from the model's schema.

On the client, provide an elicitation_callback; a client that doesn't declare the elicitation capability can't be asked. Always handle decline and cancel, and treat confirmations as a backstop, not your only safety control.

Example

from typing import Annotated
from pydantic import BaseModel
from mcp.server import MCPServer
from mcp.server.mcpserver import AcceptedElicitation, Elicit, ElicitationResult, Resolve

mcp = MCPServer("Deploys")


class Confirm(BaseModel):
    confirm: bool


async def ask_confirm(service: str) -> Elicit[Confirm]:
    return Elicit(f"Really restart {service}?", Confirm)


@mcp.tool()
async def restart(
    service: str,
    answer: Annotated[ElicitationResult[Confirm], Resolve(ask_confirm)],   # hidden from the model
) -> str:
    """Restart a service after the user confirms."""
    if isinstance(answer, AcceptedElicitation) and answer.data.confirm:
        return f"{service} restarted"
    return "Cancelled by user"

# Client side: Client(mcp, elicitation_callback=on_elicit), where on_elicit returns
# ElicitResult(action="accept", content={"confirm": True})

Remember

  • Elicitation asks the user (not the model) for input mid-tool
  • Form mode for simple data; URL mode for anything sensitive
  • 2026-07-28 uses multi round-trip requests instead of server-initiated calls
  • Resolve(fn) + Elicit(...) works across protocol versions in SDK v2

Watch out for

  • Using form mode to collect passwords or API keys; the spec requires URL mode for sensitive data.

Check yourself

  1. A tool needs the user's bank login to link an account. Which mechanism fits?
  2. Under 2026-07-28, how does a server get extra input during a tool call?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: modelcontextprotocol.io/specification/2026-07-28/client/elicitation