Quick Start: MCP Elicitations
An MCP server can use elicitation to request additional user input while it is processing another client request. Elicitation is not an unsolicited or background prompt.
The quickstart demonstrates the complete modern flow:
- The client calls
start_t4_small_sandbox. - The server needs a region, duration, and maximum budget.
- The active tool request pauses and presents an elicitation form.
- The user accepts, declines, or cancels the question.
- The client retries that same request with the user's response.
- The server returns a simulated start result.
No infrastructure is created and no charges are incurred.
Setup
Make sure you have the uv package manager
installed, then:
Run the demo:
Choose a region, duration, and maximum simulated budget, then select Accept
to approve starting the sandbox at $0.40 per hour. Use Decline or
Cancel to finish without creating anything. The budget is an in-band
numeric limit, not payment information, and the example never creates a
charge.
Approval actions
Approval is part of the elicitation result, not a field in the form schema. Every elicitation resolves with one of these protocol actions:
accept: approve the question and, for form mode, return the form content.decline: explicitly refuse the request.cancel: dismiss the interaction without making a decision.
The example therefore does not add an approve/disapprove property to
SandboxRequest. The question itself carries the price, and the tool handles
AcceptedElicitation, DeclinedElicitation, and CancelledElicitation
separately.
Server
The resolver describes input needed to continue the active tool request:
def request_sandbox_details() -> Elicit[SandboxRequest]:
return Elicit(
"Start t4-small sandbox at $0.40 per hour?",
SandboxRequest,
)
The resolved response is injected into the tool only when the same request continues:
@server.tool()
def start_t4_small_sandbox(
request: Annotated[
ElicitationResult[SandboxRequest],
Resolve(request_sandbox_details),
],
) -> str: ...
The resolver must be deterministic and side-effect free because it may run again as the request continues. Perform the operation only in the tool body after validating the accepted response.
Form elicitation is in-band. Never use it for passwords, API keys, payment details, or other sensitive data.
Client
The demo calls the tool directly, so it does not need an LLM:
sandbox = cast("McpAgent", agent.sandbox)
result = await sandbox.call_tool("start_t4_small_sandbox", {})
An LLM-backed agent can select the same tool from a natural-language request. The elicitation remains associated with that active tool call either way.
Custom handler
Pass elicitation_handler to an agent to replace the built-in form UI. The
custom-handler demo supplies deterministic content for the simulated sandbox:
async def development_sandbox_handler(
context: ClientRequestContext,
params: ElicitRequestParams,
) -> ElicitResult:
assert isinstance(params, ElicitRequestFormParams)
return ElicitResult(
action="accept",
content={
"region": "eu-west-1",
"duration_hours": 2,
"max_budget_usd": 1.00,
},
)
@fast.agent(
"sandbox_custom",
servers=["sandbox_server"],
elicitation_handler=development_sandbox_handler,
)
This pattern is useful for deterministic development and test flows. Do not automatically approve consequential production operations without an equivalent user-control boundary.
Modern URL elicitation
Run the URL example with:
The URL server returns an InputRequiredResult while processing
request_console_access. The embedded URL request is associated with that
tool call:
return InputRequiredResult(
input_requests={
"authorize_console": ElicitRequest(
params=ElicitRequestURLParams(
message=f"Authorize browser access to sandbox {sandbox_id}.",
url=authorization_url,
)
)
},
request_state=request_state,
)
url_demo.py registers a per-agent handler that displays the URL and waits
for an explicit accept, decline, or cancel choice. The MCP client then
retries the original tool with its original arguments, the response keyed by
authorize_console, and the opaque requestState.
The custom handler is intentional: the current built-in URL handler displays
or queues the URL and immediately returns accept. Override it when the
application requires an explicit consent choice.
In URL mode, accept means consent to navigate—it does not mean OAuth,
payment, credential entry, or another external operation completed. The
example therefore returns:
A production server must independently verify its browser callback before
granting access or performing a protected operation. The example uses
example.com, omits the legacy elicitationId, and does not exchange
credentials.
Configuration
The example forces modern protocol negotiation and uses the built-in forms handler:
mcp:
servers:
sandbox_server:
target: "uv run sandbox_server.py"
protocol_mode: modern
elicitation:
mode: forms
Elicitation handling can be configured as forms (the default),
auto-cancel, or none. In auto-cancel mode, fast-agent advertises the
capability but cancels requests automatically. With none, it does not
advertise the capability.