PrefectHQ/fastmcp · error · ToolError
Imperative ctx.elicit() is not supported inside a background
Error message
Imperative ctx.elicit() is not supported inside a background task. Gather input with the guard pattern instead: return an InputRequiredResult from the tool (with input_requests), and read ctx.input_responses / ctx.request_state when the task re-runs after the client answers.
What it means
ctx.elicit() performs an imperative, blocking server-to-client input request. Inside a background task the worker cannot block on a client round-trip, so FastMCP raises ToolError directing you to the guard pattern: return an InputRequiredResult (with input_requests) from the tool, let the client answer, then read ctx.input_responses / ctx.request_state when the task re-runs.
Source
Thrown at fastmcp_slim/fastmcp/server/context.py:1083
``value`` field. Same scope rules as ``response_title``.
Note:
Imperative elicitation is not available inside a background task
(calling it there raises a ``ToolError``). A task gathers input with
the guard pattern: return an ``InputRequiredResult`` and read
``ctx.input_responses`` / ``ctx.request_state`` when the task re-runs.
"""
config = parse_elicit_response_type(
response_type,
response_title=response_title,
response_description=response_description,
)
if self.is_background_task:
# Background tasks gather input with the guard/return pattern, not
# imperative elicitation — the worker never blocks on a client
# round-trip. Fail fast with the guidance to use InputRequiredResult.
raise ToolError(_TASK_ELICIT_ERROR)
# Foreground push path: server-initiated elicitation needs a back-channel,
# which the 2026-07-28 era removed (SEP-2577). Raise a clear era-aware
# error before hitting the wire instead of the SDK's opaque "Method not
# found". Handshake-era behavior is unchanged.
if self._is_modern_protocol():
raise ToolError(_ELICIT_MODERN_ERROR)
# Standard request mode: use session.elicit directly
result = await self.session.elicit(
message=message,
requested_schema=config.schema,
related_request_id=self.request_id,
)
if result.action == "accept":
return handle_elicit_accept(config, result.content)
elif result.action == "decline":
return DeclinedElicitation()
elif result.action == "cancel":View on GitHub (pinned to 1f02114297)
Solutions
- Refactor to the guard pattern: return InputRequiredResult(input_requests=[...]) instead of calling ctx.elicit().
- When the task re-runs after the client answers, read the supplied values via ctx.input_responses / ctx.request_state.
- If imperative elicitation is essential, run the tool in the foreground instead of as a background task.
- Split the tool: do the pre-input work in one call, gather input, then run the remainder in a second call.
Example fix
// before
async def deploy(ctx: Context):
r = await ctx.elicit("Proceed?", schema)
...
// after
async def deploy(ctx: Context):
if "confirm" not in ctx.request_state:
return InputRequiredResult(input_requests=[ElicitRequest(message="Proceed?", schema=schema)])
confirmed = ctx.input_responses["confirm"]
... Defensive patterns
Strategy: try-catch
Validate before calling
if getattr(ctx, "is_background_task", False):
raise RuntimeError("use InputRequiredResult guard pattern instead of ctx.elicit()") Try / catch
try:
res = await ctx.elicit("Proceed?", schema)
except ToolError as e:
if "background task" in str(e):
return InputRequiredResult(input_requests=[build_request(schema)])
raise Prevention
- When converting a tool to a background task, audit it for ctx.elicit() calls first.
- Prefer the guard pattern (InputRequiredResult + ctx.input_responses) for all task-based tools.
- Keep interactive prompts at tool boundaries, never mid-task in workers.
When it happens
Trigger: Calling await ctx.elicit(...) inside a tool running as a background task (self.is_background_task is True) — e.g. a submitted long-running tool that asks for confirmation or extra input mid-run.
Common situations: Converting an existing interactive tool into a task-based (background) tool without changing its elicitation calls; worker-queue deployments where tools run detached from the client connection.
Related errors
- elicitation via server-initiated requests is unavailable on
- session_id is not available because no session exists. This
- Elicitation responses must be serializable as a JSON object
- No access token available. Cannot perform OBO exchange.
- FastMCP instance is no longer available
AI-assisted analysis of PrefectHQ/fastmcp@1f02114297 (2026-08-29).
Data as JSON: /api/errors/ababb475a2b30da7.
Report an issue: GitHub.