Introduction
Have you ever interacted with an AI agent that asked you for missing details before it could help—like clarifying what you meant or asking follow-up questions? That process is called elicitation.
Elicitation is how AI agents gather the right pieces of information from you, step by step, in order to complete a task. Instead of needing everything upfront, the agent engages in a back-and-forth conversation—filling in gaps, confirming choices, and adjusting as needed. It’s what makes modern AI feel interactive, guided, and helpful.
Now imagine this process extended across multiple turns in a conversation that’s multi-turn elicitation.
Rather than overwhelming users with all the questions at once, multi-turn elicitation allows AI agents to ask for one piece of information at a time, based on context. For example, when booking a meeting, the AI might ask:
“What’s the meeting title?”
Then, “How long should it be?”
Then, “Is it urgent?”
At each turn, users can respond, skip, or cancel—giving them control while keeping the interaction fluid. This makes the experience feel more like talking to a helpful assistant than filling out a rigid form.
With the support of frameworks like FastMCP, multi-turn elicitation is not only simple to implement but also designed to be robust. It helps developers build AI agents that are responsive, adaptive, and able to guide users through complex workflows—one meaningful step at a time.
Why Does Elicitation Matter for AI Agents?
In traditional systems, everything has to be filled out upfront—every detail, all at once. But let’s be honest: real-life conversations don’t work that way.
People forget things.
Sometimes the information isn’t clear or complete.
And often, the needs or priorities change as the conversation goes on.
That’s where elicitation makes a big difference. It helps AI agents be more flexible and human-like—asking for the right details at the right time, adapting as things unfold, and guiding users step by step instead of expecting perfection from the start.
Real-World Use Cases for AI Agents
1. IT Support Ticketing
A user wants to submit a support ticket. The AI agent needs:
The type of issue (Network, Hardware, Software etc.)
A description
Urgency
Confirmation
If the user omits any detail, the agent asks for it—one step at a time.
2. Healthcare Intake
A digital health agent can ask for symptoms, duration, and severity, adapting its questions based on the patient’s responses.
3. Banking and Finance
When opening an account, the AI agent can prompt for missing documents or clarify ambiguous answers.
How Does Elicitation Work? (Technical Flow)
Let’s break down the flow using our IT Support Ticket AI agent example:
- User initiates a ticket submission.
- Agent checks for missing/invalid fields.
- Agent sends an elicitation request for the missing field.
- Client prompts the user and collects the answer.
- Client sends the answer back to the agent.
- Agent validates and either continues or asks for more.
- Once all info is collected, the agent processes the request.
Let us implement IT support use case using FastMCP MCP server.
IT Support Ticket Elicitation
*Server-side :MCP Server *
from mcp.server.fastmcp import FastMCP, Context
from mcp.server.elicitation import AcceptedElicitation
from pydantic import BaseModel, Field
import anyio
import logging
logging.getLogger("mcp").setLevel(logging.WARNING)
mcp = FastMCP("Sreeni's IT Support Ticket Agent")
class IssueTypeSchema(BaseModel):
issue_type: str = Field(
description="Select the type of issue (Network/Hardware/Software/Other)",
pattern="^(Network|Hardware|Software|Other)$"
)
class DescriptionSchema(BaseModel):
description: str = Field(description="Describe your issue in detail", min_length=10)
class UrgencySchema(BaseModel):
urgency: str = Field(description="How urgent is this issue?", pattern="^(Low|Medium|High)$")
class ConfirmSchema(BaseModel):
confirm: bool = Field(description="Submit this ticket?")
notes: str = Field(default="", description="Any additional notes (optional)")
async def elicit(ctx: Context, message: str, schema: type[BaseModel], field: str = None):
try:
result = await ctx.elicit(message=message, schema=schema)
if isinstance(result, AcceptedElicitation):
return getattr(result.data, field) if field else result.data
except (anyio.ClosedResourceError, ConnectionError):
return None
return None
@mcp.tool()
async def submit_ticket(ctx: Context, issue_type: str = "", description: str = "", urgency: str = "") -> str:
allowed_types = {"Network", "Hardware", "Software", "Other"}
while not issue_type or issue_type not in allowed_types:
issue_type = await elicit(ctx, "What type of issue are you experiencing?", IssueTypeSchema, "issue_type")
if not issue_type:
return "Ticket submission cancelled."
while not description or len(description) < 10:
description = await elicit(ctx, "Please describe your issue (at least 10 characters):", DescriptionSchema, "description")
if not description:
return "Ticket submission cancelled."
while urgency not in {"Low", "Medium", "High"}:
urgency = await elicit(ctx, "How urgent is this issue? (Low/Medium/High)", UrgencySchema, "urgency")
if not urgency:
return "Ticket submission cancelled."
confirmation = await elicit(ctx, f"Submit ticket?\nType: {issue_type}\nUrgency: {urgency}\nDescription: {description}", ConfirmSchema)
if not confirmation or not getattr(confirmation, 'confirm', False):
return "❌ Ticket not submitted."
notes = getattr(confirmation, 'notes', '')
notes_text = f"\nAdditional notes: {notes}" if notes else ""
return f"✅ Ticket submitted!\nType: {issue_type}\nUrgency: {urgency}\nDescription: {description}{notes_text}"
if __name__ == "__main__":
mcp.run(transport="streamable-http")
*MCP Client with Elictation Handler *
import asyncio
from mcp.client.streamable_http import streamablehttp_client
from mcp.client.session import ClientSession
import mcp.types as types
from mcp.shared.context import RequestContext
import re
import signal
import sys
MCP_SERVER_URL = "http://localhost:8000/mcp"
def get_input(prompt, pattern=None, cast=str, allow_empty=False, choices=None, min_length=None):
while True:
try:
value = input(prompt).strip()
if allow_empty and not value:
return value
if choices and value not in choices:
print(f"Please choose from: {', '.join(choices)}")
continue
if min_length and len(value) < min_length:
print(f"Please enter at least {min_length} characters.")
continue
if pattern and not re.match(pattern, value):
print("Invalid format.")
continue
return cast(value)
except (ValueError, KeyboardInterrupt, EOFError):
print("Cancelled.")
raise KeyboardInterrupt
async def smart_elicitation_callback(context: RequestContext["ClientSession", None], params: types.ElicitRequestParams):
msg = params.message.lower()
try:
if "type of issue" in msg:
return types.ElicitResult(action="accept", content={"issue_type": get_input("Issue type (Network/Hardware/Software/Other): ", choices=["Network", "Hardware", "Software", "Other"] )})
elif "describe your issue" in msg:
return types.ElicitResult(action="accept", content={"description": get_input("Describe your issue: ", min_length=10)})
elif "urgent" in msg:
return types.ElicitResult(action="accept", content={"urgency": get_input("Urgency (Low/Medium/High): ", choices=["Low", "Medium", "High"] )})
elif "submit ticket" in msg:
confirm = input("Submit this ticket? (y/n): ").lower().strip() in ['y', 'yes', '1', 'true']
notes = input("Any additional notes? (optional): ").strip() if confirm else ""
return types.ElicitResult(action="accept", content={"confirm": confirm, "notes": notes})
else:
value = input("Your response: ").strip()
return types.ElicitResult(action="accept", content={"response": value})
except KeyboardInterrupt:
return types.ElicitResult(action="cancel", content={})
async def run():
try:
async with streamablehttp_client(url=MCP_SERVER_URL) as (read_stream, write_stream, _):
async with ClientSession(read_stream=read_stream, write_stream=write_stream, elicitation_callback=smart_elicitation_callback) as session:
await session.initialize()
tools = await session.list_tools()
print(f"Available tools: {[tool.name for tool in tools.tools]}")
print("\n--- Submit an IT Support Ticket ---")
result = await session.call_tool(name="submit_ticket", arguments={})
print(f"\nServer response:\n{result.content[0].text}")
except KeyboardInterrupt:
print("Demo cancelled.")
def signal_handler(signum, frame):
print("\nShutting down...")
sys.exit(0)
if __name__ == "__main__":
signal.signal(signal.SIGINT, signal_handler)
asyncio.run(run())
MCP Server and Client Interaction
MCPServer & Client in Action
The Support Ticket MCP Server runs using the streamable-http transport, listening on the default port 8000. You can access it using the following endpoint: http://127.0.0.1:8000/mcp
MCP Client
Conclusion
Elicitation is what transforms AI agents from passive responders into proactive, intelligent collaborators. In the world of Model Context Protocol (MCP), elicitation isn't just a nice feature—it’s a core capability that allows agents to drive the conversation, gather missing inputs, and adjust to the user’s pace and intent.
With multi-turn elicitation, agents no longer rely on receiving every parameter up front. Instead, they ask for what they need, when they need it—creating interactions that feel more natural, guided, and context-aware.
So whether you’re designing an AI agent powered by MCP or building workflows that rely on dynamic user input, ask yourself:
What can my agent elicit to move forward, rather than stall because something’s missing?
In the MCP world, asking the right question at the right time is how agents truly become helpful.
Thanks
Sreeni Ramadorai
Top comments (2)
Wonderful...
Sreeni, any reason why you haven't used FastMCP for client implementation?
Well, I need to show multiple way to invoke client .