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

  1. Refactor to the guard pattern: return InputRequiredResult(input_requests=[...]) instead of calling ctx.elicit().
  2. When the task re-runs after the client answers, read the supplied values via ctx.input_responses / ctx.request_state.
  3. If imperative elicitation is essential, run the tool in the foreground instead of as a background task.
  4. 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 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


AI-assisted analysis of PrefectHQ/fastmcp@1f02114297 (2026-08-29). Data as JSON: /api/errors/ababb475a2b30da7. Report an issue: GitHub.