MCP Elicitation — Checkpoints of AI Actions
This article is part of a 6-part series. To check out the other articles, see:
Primitives: Tools | Resources | Prompts | Sampling | Roots | Elicitation
Think for a bank’s AI app is preparing to move money on your behalf 💸
It’s ready to click “send,” but it can’t decide if the amount is what you really intended. Rather than guess, it pops up a small form that asks, “Is $1 500.75 the exact amount you want to transfer?”
You tick a box, the AI records your answer, and the payment goes through. That quick question is Elicitation in action: the built-in way for an AI to stop, ask for missing facts, and then carry on—without breaking the flow or your trust.
In Part I we’ll see why every AI hits moments like this and how Elicitation turns them into simple, safe checkpoints. Part II walks the wire-level protocol in plain JSON-RPC. Part III offers copy-ready code that turns the pattern into real forms and audit logs, and Part IV closes with the must-know security checklist so you can ship without surprises.
Specification Note: This guide is based on the official & latest MCP Elicitation specification (Protocol Revision: 2025-06-18). Where this guide extends beyond the official spec with patterns or recommendations, it is clearly marked.
Part I: Foundation - The "Why & What"
This section establishes the conceptual foundation. If a reader only reads this part, they will understand what the primitive is, why it's critically important, and where it fits in the ecosystem.
1. Core Concept
1.1 One-Sentence Definition
Elicitation is a formal, server-initiated MCP primitive that pauses an AI-driven workflow to interactively request a specific piece of information or a judgment call from a human user.
1.2 The Core Analogy
Think of Elicitation as a skilled surgeon's assistant.
During a complex operation, an AI assistant processing medical data might encounter a situation it wasn't trained for—an anomaly in a patient's genomics report. Instead of guessing (and risking a catastrophic misdiagnosis) or failing completely (and stopping the analysis), the AI acts like a trained assistant:
It formally pauses the procedure, turns to the senior surgeon (the user), and presents the situation with clear options: "Dr. Chen, I've detected an unusual gene variant. Based on the data, I require your expert assessment. Should I: (A) Classify it as benign based on standard markers, (B) Flag it for urgent review by the oncology board, or (C) Proceed with the analysis, noting it as an outlier?"
The surgeon provides their expert judgment, which the assistant securely records and seamlessly integrates to continue the task with precision and authority. Elicitation is this formal, auditable pause-and-ask mechanism that makes AI a safe, collaborative partner.
1.3 Architectural Position & Control Model
Elicitation is a server-initiated primitive that orchestrates a three-party interaction within the standard MCP architecture. Implementations are free to expose elicitation through any interface pattern that suits their needs—the protocol itself does not mandate any specific user interaction model.

The Control Model is a unique tripartite system designed for maximum safety:
- Server-Initiated: The AI application (Server) determines when it lacks information and initiates the request.
- Client-Controlled: The user's application (Client/Host) is responsible for rendering the UI and managing the state of the interaction. It acts as the user's trusted agent.
- User-Approved: The human user has the final and absolute authority. They decide what information to provide and can accept, decline, or completely cancel the workflow at any time.
This distribution of control ensures the AI can ask for help, but the user and their trusted application remain in complete command of the interaction.
1.4 What It Is NOT
- It is NOT a simple chat message. Elicitation is a formal, structured, and stateful protocol for capturing validated data, not a free-form text conversation that is prone to errors.
- It is NOT a general-purpose form builder. While it renders forms, its purpose is to unblock a specific, in-progress AI workflow, not to build complex, standalone user interfaces.
2. The Problem It Solves
2.1 The LLM-Era Catastrophe: The Judgment Gap
Every AI, no matter how advanced, eventually hits The Judgment Gap—the moment an algorithm encounters a situation that requires human wisdom, authority, or context it simply does not possess.
- The World Without Elicitation: When an AI hits this gap, it has three catastrophic options:
- Guess: It makes an assumption based on incomplete patterns, leading to unpredictable and legally indefensible actions.
- Fail: The entire workflow aborts, losing context and forcing the user to start over in frustration.
- Exceed Authority: It makes a decision it is not qualified to make, with potentially devastating consequences.
This broken paradigm is a primary source of failed AI projects and significant financial losses.
- The World With Elicitation: The AI doesn't guess, fail, or overstep. It formally pauses, presents the human with the precise context needed, captures their judgment through a secure and auditable protocol, and seamlessly integrates that human wisdom into the automated workflow.
2.2 Why Traditional Approaches Fail
- Multi-Turn Chat Clarification: An LLM might try to ask for information via chat ("Please tell me the account number"). This fails because the response is unstructured, requires complex and brittle parsing, lacks validation, and has no formal audit trail. It's asking for a number and getting a paragraph.
- Pre-defined "Mega-Forms": Forcing users to fill out huge forms with every possible piece of information upfront creates overwhelming friction. This "vending machine" approach fails because it cannot adapt to the dynamic context of a workflow, often asking for irrelevant information while missing the one critical detail that only becomes necessary midway through the process.
Part II: Technical Architecture - The "How"
This is the technical deep dive, detailing the protocol mechanics, message structures, and the end-to-end lifecycle. This section is the source of truth for implementers.
3. Capabilities Declaration
Clients that support this primitive MUST declare the elicitation
capability during the initial protocol handshake. This informs the server that it is safe to send elicitation/create
requests.
Client → Server: initialize
Request with Elicitation Capability
{
"method": "initialize",
"params": {
"protocolVersion": "2025-06-18",
"capabilities": {
"elicitation": {}
}
}
}
4. Protocol Specification
4.1 Request & Response Schemas
To request information, a server sends a direct elicitation/create
request to the client.
Server → Client: Elicitation Request
This message asks the client to render a form and prompt the user for input.
{
"jsonrpc": "2.0",
"method": "elicitation/create",
"params": {
// A human-readable message explaining WHY the AI is pausing.
"message": "To finalize your transaction, please confirm the details to comply with regulations.",
// A JSON schema defining the form fields the user needs to complete.
"requestedSchema": {
"type": "object",
"properties": {
"amount": {
"type": "number",
// A concise instruction for THIS specific field.
"description": "The exact transaction amount in USD."
},
"recipient_account": {
"type": "string",
"description": "The 10-digit destination account number.",
"pattern": "^[0-9]{10}$"
},
"priority": {
"type": "string",
"description": "Select the transfer priority.",
"enum": ["std", "exp", "wire"],
"enumNames": ["Standard", "Express", "Wire Transfer"]
}
},
"required": ["amount", "recipient_account"]
}
},
"id": "elicit-request-123"
}
Important Schema Restrictions: To simplify client implementation and enhance security,requestedSchema
is intentionally limited:Only flat objects (no nested objects).Only primitive property types (string
,number
,boolean
,enum
).No arrays of objects.No complex JSON Schema features likeallOf
,oneOf
, or conditional subschemas.
Supported String Formats: The format
property for string
types is restricted to: email
, uri
, date
, and date-time
.
Client → Server: Elicitation Response
After the user interacts with the form, the client sends back a response containing one of three possible actions. See "The Three-Action Model" below for a detailed explanation.
// Example "Accept" Response
{
"jsonrpc": "2.0",
"result": {
"action": "accept",
"content": {
"amount": 1500.75,
"recipient_account": "1234567890",
"priority": "exp"
}
},
"id": "elicit-request-123"
}
4.2 The Three-Action Model: Accept, Decline, and Cancel
Every elicitation response clearly communicates the user's intent through the action
field. Servers MUST be prepared to handle all three outcomes gracefully.
- Accept (
action: "accept"
)- User Intent: The user has filled out the form and explicitly approved the submission (e.g., by clicking "Submit," "OK," or "Confirm").
- Server Handling: The server should process the submitted data contained in the
content
field and continue the workflow.
- Decline (
action: "decline"
)- User Intent: The user has explicitly rejected the request for information (e.g., by clicking "Decline," "Reject," or "No Thanks").
- Server Handling: The
content
field will be omitted. The server should respect the user's refusal, attempt a fallback behavior if possible (like using default values), or terminate the specific path that required the information.
- Cancel (
action: "cancel"
)- User Intent: The user has dismissed the request without making an explicit choice (e.g., by closing the dialog box, pressing the Escape key, or clicking outside the modal).
- Server Handling: The
content
field will be omitted. The server should treat this as a non-committal dismissal, cleanly aborting the current operation without prejudice. It may be appropriate to re-initiate the request later if the user tries the same action again.
4.3 Error Handling
Elicitation adheres to the standard JSON-RPC 2.0 error specification.
Code | Meaning | Recommended Client/Server Action |
---|---|---|
-32601 |
Method not found | The server sent elicitation/create to a client that does not support it (or didn't declare it in capabilities). The server should handle this gracefully. |
-32602 |
Invalid params | The schema sent by the server was malformed or violated the primitive's constraints. This is a server-side bug. |
Note: Protocol-level timeouts are not part of the specification. It is a best practice for servers to implement their own timeout logic to handle non-responsive clients and prevent orphaned processes.
5. The 4-Step Lifecycle
5.1 Step-by-Step Communication Flow
The Elicitation lifecycle is a coordinated dance between the Server, the Client, and the User.

5.2 State Management & Dynamic Behavior
Elicitation is inherently stateful and blocking.
- Server State: When a server sends an
elicitation/create
request, its process is blocked, waiting for the client's response. This creates a fragile state. If the user closes their browser or the client crashes, the server process can be left hanging—a "zombie prompt." Production-grade servers MUST implement timeouts to automatically cancel the operation and clean up resources. - Client State: The client bears the burden of managing the UI state. It is responsible for rendering the form, performing client-side validation, managing user input, and handling the three final actions (
accept
,decline
,cancel
) reliably. This makes the client-side implementation significantly more complex than a simple request-response pattern.
Part III: Implementation Patterns - The "Show Me"
This section makes concepts concrete through a progression of code-heavy examples, from a simple "hello world" to a complex, production-ready workflow.
6. Pattern 1: The Canonical "Golden Path"
Goal: To perform the most basic Elicitation: ask the user for their name and email.
Implementation (Python Server)
Note: This example uses the fastmcp
Python library for clarity. The underlying JSON-RPC messages are the same regardless of the library used.
# server.py
from fastmcp import FastMCP, ElicitationResult
from pydantic import BaseModel, EmailStr
app = FastMCP("Basic Info Server")
# Define the structure of the data we want from the user.
class UserInfo(BaseModel):
name: str
email: EmailStr
@app.tool()
async def collect_user_info(ctx) -> str:
"""A simple tool to collect user contact information."""
# 1. Initiate the Elicitation request.
# The server process will PAUSE here until the user responds.
result: ElicitationResult[UserInfo] = await ctx.elicit(
message="Please provide your contact details to personalize your experience.",
response_type=UserInfo
)
# 2. Handle the user's decision.
if result.action == "accept":
# The happy path: we received structured, validated data.
user_data = result.data
return f"Welcome, {user_data.name}! A confirmation will be sent to {user_data.email}."
elif result.action == "decline":
return "Okay, we will proceed with an anonymous session."
else: # result.action == "cancel"
return "Operation cancelled by the user."
Key Takeaway: The await ctx.elicit(...)
call is the core mechanism. It abstracts the entire JSON-RPC flow, pausing server execution and returning a structured object containing the user's final decision and validated data.
7. Pattern 2: The Compositional Workflow
Goal: Show how Elicitation
combines with other primitives. Here, a Tool
identifies a large dataset (a Resource
), and Elicitation
is used to get the user's permission before the Tool
processes it.
Implementation (Python Server)
Note: This example uses library-specific patterns for illustration. The core logic of checking a resource and then eliciting is universal.
# server.py
from fastmcp import FastMCP, ElicitationResult
from pydantic import BaseModel
app = FastMCP("Data Processor")
class Confirmation(BaseModel):
confirm: bool
@app.tool()
async def process_financial_report(ctx, report_uri: str) -> str:
"""
Analyzes a financial report, but first asks for explicit user consent
if the report is marked as sensitive.
"""
# 1. A Resource (the report) is the input.
# The application checks metadata associated with the Resource.
is_sensitive = await check_resource_sensitivity(report_uri)
if is_sensitive:
# 2. Elicitation is used to get explicit consent for an action.
consent_message = f"The report '{report_uri}' contains sensitive financial data. Do you authorize this AI to perform a full analysis?"
result: ElicitationResult[Confirmation] = await ctx.elicit(
message=consent_message,
response_type=Confirmation
)
# If consent is not given, we abort safely.
if result.action != "accept" or not result.data.confirm:
return "Analysis cancelled. User did not provide consent."
# 3. Another Tool (or the rest of this one) performs the action.
# This part of the code only runs if consent was explicitly granted.
analysis_result = await ctx.call_tool(
"run_secure_analysis",
target_resource=report_uri
)
return f"Analysis complete. Result: {analysis_result}"
Key Takeaway: Elicitation is the crucial "human-in-the-loop" bridge between other primitives. It allows an AI to discover data (Resource
) and then seek permission before acting on it (Tool
), transforming a purely autonomous workflow into a governed, collaborative one.
8. Pattern 3: The Domain-Specific Solution (Healthcare/HIPAA)
Goal: Create a HIPAA-compliant workflow for a clinical researcher who wants to add a patient to a drug trial. The process requires justification and creates a formal audit trail.
Implementation (Python Server)
Note: This example shows how you might extend the base protocol for domain-specific requirements. HIPAAExecutionContext
is a hypothetical construct representing a secure, audited environment.
# server_hipaa.py
import logging
from datetime import datetime, timezone
from pydantic import BaseModel, Field
from fastmcp import FastMCP, ElicitationResult, HIPAAExecutionContext
app = FastMCP("Clinical Trial Assistant")
# Setup secure logging for audit trails.
audit_logger = logging.getLogger("hipaa_audit")
class TrialEnrollmentJustification(BaseModel):
reasoning: str = Field(..., min_length=50, description="Provide a detailed clinical justification for enrolling this patient (min 50 chars).")
irb_approval_code: str = Field(..., pattern=r'^IRB-\d{4}-\d{2}$', description="Enter the IRB approval code for this action.")
@app.tool(context=HIPAAExecutionContext) # Enforces HIPAA-specific rules
async def enroll_patient_in_trial(ctx, patient_id: str, trial_id: str) -> str:
"""Enrolls a patient in a trial after capturing auditable justification."""
researcher_id = ctx.security_context.user_id
# 1. Elicit for the MANDATORY justification required by HIPAA.
elicitation_message = f"You are about to enroll Patient '{patient_id}' into Trial '{trial_id}'. This is an auditable action. Please provide justification."
result: ElicitationResult[TrialEnrollmentJustification] = await ctx.elicit(
message=elicitation_message,
response_type=TrialEnrollmentJustification
)
if result.action != "accept":
log_audit_event(researcher_id, "enroll_patient", "cancelled", patient_id, trial_id, "User cancelled the operation.")
return "Enrollment cancelled."
justification = result.data
# 2. Perform the action ONLY after successful elicitation.
enrollment_status = await perform_enrollment(patient_id, trial_id, justification)
# 3. Create a detailed, immutable audit log entry.
log_audit_event(
researcher_id, "enroll_patient", "success", patient_id, trial_id,
f"Reason: {justification.reasoning}, IRB: {justification.irb_approval_code}"
)
return f"Patient {patient_id} successfully enrolled in {trial_id}. Action logged for compliance."
def log_audit_event(user_id, action, status, patient, trial, details):
log_entry = { "timestamp": datetime.now(timezone.utc).isoformat(), "user_id": user_id, "action": action, "status": status, "patient_id": patient, "trial_id": trial, "details": details, "event_signature": "SHA256:..." }
audit_logger.info(log_entry)
Key Takeaway: In regulated environments, Elicitation is not just for convenience; it is a core compliance mechanism. It serves as the formal gateway for capturing consent, justification, and data required for audit trails.
Part IV: Security Architecture
This section outlines the mandatory security considerations for any implementation of the Elicitation primitive. Failure to adhere to these principles can lead to serious trust, safety, and privacy violations.
- Servers MUST NOT Request Sensitive Information
⚠️ SECURITY CRITICAL: Under no circumstances should elicitation be used to request passwords, API keys, private keys, credit card numbers, or any other secrets. This trains users to be phished. All authentication and authorization must be handled through standard, secure, out-of-band mechanisms like OAuth 2.1. - Clients SHOULD Provide Clear Server Attribution
To prevent a malicious server from impersonating a trusted one, the client UI MUST clearly and unambiguously display which server is making the request. For example: "The 'Financial Analysis Bot' is requesting the following information..." - Clients SHOULD Implement User Approval Controls
The user must always be in control. The client UI should allow users to review and modify their responses before submitting. The three-action model (accept
/decline
/cancel
) is the primary mechanism for this, and clients must implement all three paths correctly. - Both Parties SHOULD Validate Against the Schema
- Client-Side: The client should perform validation against the
requestedSchema
before sending the response to provide immediate feedback to the user. - Server-Side: The server MUST re-validate the received
content
against the schema it originally sent. Never trust input from the client. This protects against malicious or buggy clients sending malformed data.
- Client-Side: The client should perform validation against the
- Clients SHOULD Allow Decline and Cancel at Any Time
The user's right to refuse or dismiss a request is fundamental. The client interface must make the "Decline" and "Cancel" actions just as accessible as the "Accept" action. - Clients SHOULD Implement Rate Limiting
To prevent denial-of-service attacks or user annoyance from a runaway server, clients should implement rate limiting on incomingelicitation/create
requests (e.g., no more than 10 requests per minute from a single server). - Clients SHOULD Present Requests Clearly
Themessage
anddescription
fields should be rendered in a way that is easy to understand, making it clear what information is being requested and why. This helps users make informed decisions.
Part V: Guardrails & Reality Check - The "Don't Fail"
9. Decision Framework
9.1 When to Use This Primitive
If you need to... | Then use Elicitation because... | An alternative could be... |
---|---|---|
Confirm a destructive or costly action | It provides a formal, auditable consent workflow, preventing accidental execution. | A manual user-initiated workflow, but that requires the user to start from scratch. |
Resolve ambiguity in a user's command | It can ask clarifying questions with structured responses, avoiding misinterpretation. | General LLM conversation, but this is error-prone, unstructured, and hard to validate. |
Guide a user through a complex, multi-step task | It creates an interactive "wizard" experience, asking for information progressively. | A hardcoded UI form, which is not integrated with the AI's dynamic workflow. |
9.2 Critical Anti-Patterns (The "BAD vs. GOOD")
Anti-Pattern #1: Pseudo-Elicitation via General Conversation
This pattern defeats the entire purpose of having a structured primitive.
BAD APPROACH 👎
# The AI tries to ask for information via a generic chat prompt.
llm_response = await ask_llm("I need your email address to continue. Please provide it.")
# The application now has to parse unstructured text, which will fail constantly.
email = parse_email_from_text(llm_response) # Brittle and unreliable!
Why it's wrong: You lose all benefits of Elicitation: structured data, validation, and a clear user interface. You are just hoping the user types exactly what you expect.
GOOD CODE 👍
# Use the right tool for the job.
class ContactInfo(BaseModel):
email: EmailStr
result = await ctx.elicit(
message="Please provide your email for notifications.",
response_type=ContactInfo # Guaranteed structured, validated data.
)
if result.action == "accept":
email = result.data.email
Anti-Pattern #2: Ignoring Decline/Cancel Actions
Failing to handle all user decisions leads to a brittle and frustrating user experience.
BAD CODE 👎
# This code only considers the "happy path" and will crash or error
# if the user clicks Decline or Cancel.
result = await ctx.elicit(...)
# This will raise an exception if result.data is None because the
# action was 'decline' or 'cancel'.
process_data(result.data) # Unsafe!
Why it's wrong: It violates the user's control. The user's choice to decline or cancel is valid input that the system must respect and handle gracefully.
GOOD CODE 👍
result = await ctx.elicit(...)
if result.action == "accept":
process_data(result.data)
elif result.action == "decline":
# Provide a graceful fallback or default behavior.
use_default_settings()
else: # result.action == "cancel"
# Cleanly terminate the operation and inform the user.
log_cancellation_and_exit()
10. The Reality Check
10.1 Known Limitations & Constraints
- The Client-Side Burden: The biggest challenge of Elicitation is the implementation complexity on the client. The client is responsible for rendering the UI, managing a stateful interaction, and handling timeouts, which is significantly harder than a simple stateless API call.
- Zombie Prompts: Because the server process blocks while waiting for a response, a client that disconnects without warning (e.g., user closes the browser tab) can leave an orphaned process consuming resources on the server. A robust server must implement aggressive timeouts (e.g., 30-300 seconds) on all Elicitation calls.
10.2 The Trust Model: Enforced vs. Cooperative
Understanding the division of responsibility is critical for security.
- Enforced by Protocol:
- The message method (
elicitation/create
) and response structure (result
withaction
). - The three mandatory user actions (
accept
,decline
,cancel
). - The requirement of a JSON Schema for data definition.
- The message method (
- Developer Responsibility (Cooperative):
- NEVER elicit secrets. The protocol cannot stop you from writing a bad schema that asks for a password. This is your responsibility.
- Securely render the UI. The client must clearly attribute which server is asking for information to prevent phishing by a malicious server.
- Validate ALL input. Validate on the client for good UX, but you MUST re-validate on the server to protect against malicious clients.
Part VI: Quick Reference - The "Cheat Sheet"
11. Common Operations & Snippets
Server-Side: Ask a Question (Python Example)
Note: This example uses the fastmcp
library for brevity.
from pydantic import BaseModel
from fastmcp import ElicitationResult
class UserInput(BaseModel):
name: str
result: ElicitationResult[UserInput] = await ctx.elicit(
message="What is your name?",
response_type=UserInput
)
if result.action == "accept":
print(f"Hello, {result.data.name}")
Client-Side: Handle a Request (TypeScript/VS Code Example)
import * as vscode from 'vscode';
// Assume 'request' is the incoming JSON-RPC call for elicitation
async function handleElicitationRequest(request: any): Promise<any> {
const { message, requestedSchema } = request.params;
const propertyName = Object.keys(requestedSchema.properties)[0];
const propertySchema = requestedSchema.properties[propertyName];
// 1. Show the user who is asking and why.
vscode.window.showInformationMessage(`Request from Server: ${message}`);
// 2. Render a simple input box based on the schema.
const userInput = await vscode.window.showInputBox({
prompt: propertySchema.description || propertyName
});
// 3. Handle cancellation (user pressed Escape or closed the box).
if (userInput === undefined) {
return { action: "cancel" };
}
// 4. Return the accepted data.
return { action: "accept", content: { [propertyName]: userInput } };
}
12. elicitation/create
Parameter & Error Tables
Request Parameters
Parameter | Type | Required | Description |
---|---|---|---|
message |
string |
Yes | A human-readable text explaining the context and purpose of the request. The "Why". |
requestedSchema |
object |
Yes | A JSON Schema defining the fields for the form to be rendered. Must adhere to the schema restrictions. The "What". |
Protocol Error Codes
Code | Meaning | Recommended Server Action |
---|---|---|
-32601 |
Method not found | The client does not support elicitation. Do not send further elicitation/create requests to this client. Use a non-interactive fallback if available. |
-32602 |
Invalid params | The schema you sent was invalid. This is a server bug. Log the error and correct the schema generation logic. |