Elicitation: asking the user for input mid-tool
Sometimes the tool — not the model — needs the user: confirming a destructive
step, filling in required parameters the conversation never provided, or
completing an external flow. MCP elicitation is a server→client request that
pops a structured form (or a URL) in the host UI. nanohub-mcp supports both
modes through the injected ctx.
Form mode
@server.tool(input_schema=..., output_schema=...)
def submit_batch(run_handle, ctx=None):
"""Submit the run to the cluster after user confirmation."""
try:
answer = ctx.elicit(
"Submit this 4-hour batch run?",
requested_schema={
"type": "object",
"properties": {
"queue": {"type": "string", "enum": ["standby", "long"],
"default": "standby"},
"notify": {"type": "boolean", "default": True},
},
"required": ["queue"],
},
)
except RuntimeError:
# Client declared no elicitation capability — degrade to conversation.
return ToolResult(
content="This client cannot show forms. Ask the user which queue "
"to use (standby or long), then call submit_batch_confirmed.",
is_error=True)
if answer.get("action") != "accept":
return {"status": "cancelled_by_user"}
queue = (answer.get("content") or {}).get("queue", "standby")
...
Facts that matter:
Handle all three actions. The result is
{"action": "accept" | "decline" | "cancel", "content": {...}}— treat decline and cancel as first-class outcomes, not errors.Capability-gated. The framework raises
RuntimeErrorwhen the client didn’t declare theelicitationcapability (form mode also accepts the bare{}declaration). Always wrap and provide a conversational fallback — many MCP clients still don’t implement it.Keep the schema flat and small. Hosts render it as a form; nested objects and long enums make bad forms. Provide
defaults — they prefill.Default timeout is 60 s (
ctx.elicit(..., timeout=…)); a user who walked away raises a timeout error you should catch and report calmly.
URL mode
result = ctx.elicit_url(
"Authorize access to the external data source",
url="https://example.org/authorize?state=...",
)
The host opens/offers the URL and reports completion (elicitationId
correlates the flow). Use for third-party authorization or anything requiring
a real browser context. Never use elicitation of either mode to collect
passwords or secrets into tool results — results enter the model context.
Transport constraints (nanoHUB-specific)
Server→client requests ride the MCP session channel. They work through
POST /mcpwith an active session; the direct REST convenience route (POST /tools/{name}) refuses context-bearing tools with 409 for exactly this reason. If you addctxto a tool, script users lose the REST shortcut for it — that is the intended trade.Elicit before starting long work, from a fast sync tool (a
confirm_and_submitstep), not from inside an async task: the model and user may be several poll cycles away when a background thread asks.
When to elicit vs when to let the model ask
Situation |
Use |
|---|---|
destructive/expensive confirmation with options |
|
a required parameter with a small closed choice set |
elicit — beats a wrong guess |
open-ended scientific intent (“what sweep range?”) |
let the model ask in chat — it has context |
external browser flow (OAuth to a data source) |
|
clients without the capability |
ToolResult telling the model what to ask |