mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-09 15:19:10 +02:00
Migration guides now map the SDK's Resolve to Elicit rather than claiming FastMCP has no resolver injection; the FastMCP 3 guide leads its ctx.elicit era-gate section with declaring the parameter, since that is the fix that serves both eras. Client docs cover answering the rounds yourself, including the two same-named ElicitResult types. Also fixes a pre-existing broken anchor in the FastMCP 3 guide.
205 lines
8.4 KiB
Text
205 lines
8.4 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.
|
|
|
|
<Note>
|
|
**These sections show the server-initiated flow, which the handshake-era protocol uses.** On `2026-07-28` the server asks by returning a request instead — see [input-required rounds](#input-required-rounds). One `elicitation_handler` serves both, so the examples below pin `mode="legacy"` only to exercise the pushed form.
|
|
</Note>
|
|
|
|
## 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 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,
|
|
)
|
|
```
|
|
|
|
### Answering the rounds yourself
|
|
|
|
A handler answers a question the moment it is asked, which only works when the answer is available right then. Often it is not — a web app shows a form and the reply arrives on a different request, minutes later, possibly in another process. That is what the sessionless protocol is built for, and a callback cannot span it.
|
|
|
|
So a client with **no** `elicitation_handler` is handed the request instead of being asked to answer it. `CallToolResult.input_required` carries what the server asked, and you answer by calling again with `input_responses` and the `request_state` it came with:
|
|
|
|
```python
|
|
from fastmcp import Client
|
|
from mcp_types import ElicitResult
|
|
|
|
async with Client("https://example.com/mcp") as client:
|
|
result = await client.call_tool("book_flight")
|
|
|
|
while result.input_required:
|
|
answers = {
|
|
key: ElicitResult(action="accept", content={"value": show_form(request)})
|
|
for key, request in result.input_required.input_requests.items()
|
|
}
|
|
result = await client.call_tool(
|
|
"book_flight",
|
|
input_responses=answers,
|
|
request_state=result.input_required.request_state,
|
|
)
|
|
|
|
print(result.data)
|
|
```
|
|
|
|
Every question the server grouped into one round arrives together, so a form can render them as one screen — which is what a server that batched its questions intended. A handler receives them one at a time and loses that grouping.
|
|
|
|
`request_state` is opaque and sealed by the server; pass it back exactly as received. Nothing is stored between rounds, so the next call can be served by a different worker entirely.
|
|
|
|
<Warning>
|
|
Answering by hand uses `mcp_types.ElicitResult`, whose `content` is a plain dict matching the requested schema. That is a different class from the `fastmcp.client.elicitation.ElicitResult` an `elicitation_handler` returns, where `content` is an instance of the `response_type` FastMCP built for you. The names are the same and the `content` types are not, so import the one that matches the path you are on.
|
|
</Warning>
|
|
|
|
<Note>
|
|
This applies to `2026-07-28` connections, where the request is a result you can inspect. On handshake-era connections the server is blocked mid-call waiting on the back-channel, so there is nothing to hand back — a client without an `elicitation_handler` gets an "Elicitation not supported" error, which is accurate there.
|
|
</Note>
|