fastmcp/docs/clients/elicitation.mdx
Jeremiah Lowin 1a788cf349
Merge remote-tracking branch 'origin/main' into feature/client-auto-default
# Conflicts:
#	tests/client/test_streamable_http.py
#	tests/server/middleware/test_initialization_middleware.py
#	tests/server/tasks/test_task_status_notifications.py
2026-07-20 12:02:38 -04:00

166 lines
6.6 KiB
Text

---
title: User Elicitation
sidebarTitle: Elicitation
description: Handle server requests for structured user input.
icon: message-question
---
import { VersionBadge } from "/snippets/version-badge.mdx";
<VersionBadge version="2.10.0" />
Use this when you need to respond to server requests for user input during tool execution.
Elicitation allows MCP servers to request structured input from users during operations. Instead of requiring all inputs upfront, servers can interactively ask for missing parameters, request clarification, or gather additional context.
Two routes reach that outcome, and the protocol era the client negotiates decides which one applies. Handshake-era connections let the server push an elicitation request down the back-channel the handshake opens, which is the flow the next few sections describe. Modern-era connections work through [input-required rounds](#input-required-rounds) instead, where the server returns a description of what it needs and the client answers with a fresh call. You write the same `elicitation_handler` either way — FastMCP routes it to whichever mechanism the connection supports.
A client left at its default `mode="auto"` negotiates the newest era it shares with the server, so the examples below pin `mode="legacy"` to exercise the server-initiated flow. See [protocol negotiation](/clients/client#protocol-negotiation) for the full picture.
## Handler Template
```python
from fastmcp import Client
from fastmcp.client.elicitation import ElicitResult, ElicitRequestParams, RequestContext
async def elicitation_handler(
message: str,
response_type: type | None,
params: ElicitRequestParams,
context: RequestContext
) -> ElicitResult | object:
"""
Handle server requests for user input.
Args:
message: The prompt to display to the user
response_type: Python dataclass type for form responses (None for URL requests or empty schemas)
params: Original MCP elicitation parameters
context: Request context with metadata
Returns:
- Data directly (implicitly accepts the elicitation)
- ElicitResult for explicit control over the action
"""
# Present the message and collect input
user_input = input(f"{message}: ")
if not user_input:
return ElicitResult(action="decline")
# URL requests and empty-object schemas have no response type to construct,
# so accepting is the whole response.
if response_type is None:
return ElicitResult(action="accept")
# Create response using the provided dataclass type
return response_type(value=user_input)
client = Client(
"my_mcp_server.py",
mode="legacy",
elicitation_handler=elicitation_handler,
)
```
## How It Works
When a server needs user input, it sends an elicitation request with a message prompt. Form elicitation requests include a JSON schema describing the expected response structure, and FastMCP automatically converts that schema into a Python dataclass type. URL elicitation requests and empty-object schemas use `response_type=None`.
The handler receives four parameters:
<Card icon="code" title="Handler Parameters">
<ResponseField name="message" type="str">
The prompt message to display to the user
</ResponseField>
<ResponseField name="response_type" type="type | None">
A Python dataclass type that FastMCP created from a form request's JSON schema. Use this to construct your response with proper typing. For URL requests or empty-object schemas, this will be `None`.
</ResponseField>
<ResponseField name="params" type="ElicitRequestParams">
The original MCP elicitation parameters. Form requests carry the raw JSON schema on `params.requested_schema`; URL requests carry `params.url` instead and have no schema.
</ResponseField>
<ResponseField name="context" type="RequestContext">
Request context containing metadata about the elicitation request
</ResponseField>
</Card>
## Response Actions
You can return data directly, which implicitly accepts the elicitation:
```python
async def elicitation_handler(message, response_type, params, context):
user_input = input(f"{message}: ")
return response_type(value=user_input) # Implicit accept
```
Or return an `ElicitResult` for explicit control over the action:
```python
from fastmcp.client.elicitation import ElicitResult
async def elicitation_handler(message, response_type, params, context):
user_input = input(f"{message}: ")
if not user_input:
return ElicitResult(action="decline") # User declined
if user_input == "cancel":
return ElicitResult(action="cancel") # Cancel entire operation
return ElicitResult(
action="accept",
content=response_type(value=user_input)
)
```
**Action types:**
- **`accept`**: User provided valid input. Include the data in the `content` field.
- **`decline`**: User chose not to provide the requested information. Omit `content`.
- **`cancel`**: User cancelled the entire operation. Omit `content`.
## Example
A file management tool might ask which directory to create:
```python
from fastmcp import Client
from fastmcp.client.elicitation import ElicitResult
async def elicitation_handler(message, response_type, params, context):
print(f"Server asks: {message}")
user_response = input("Your response: ")
if not user_response:
return ElicitResult(action="decline")
# Use the response_type dataclass to create a properly structured response
return response_type(value=user_response)
client = Client(
"my_mcp_server.py",
mode="legacy",
elicitation_handler=elicitation_handler
)
```
## Input-required rounds
<VersionBadge version="4.0.0" />
On modern-era connections (protocol version `2026-07-28` and later), a server can ask for input before it returns a final result. Nothing is held open: the tool *returns* a description of what it needs, which completes that round as an ordinary response, and the client answers by issuing a **new** `call_tool`, `get_prompt`, or `read_resource` request carrying the answer. `fastmcp.Client` drives that loop for you — it fulfils each round's requests using the callbacks you already configured (your `elicitation_handler`, `sampling_handler`, and roots) and repeats until the call reaches a terminal result. No extra wiring is needed beyond the handlers described above.
The `input_required_max_rounds` parameter caps how many rounds the client will answer before giving up, guarding against a server that never terminates. It defaults to `10`.
```python
client = Client(
"https://example.com/mcp",
mode="auto",
elicitation_handler=elicitation_handler,
input_required_max_rounds=5,
)
```