Skip to content

Elicitation

Elicitation is the MCP feature where a server pauses and asks the user something — “which environment?”, “confirm this deletion”. The agent process stays alive and blocked while the human answers.

This is genuinely hard in an orchestrator, because the human isn’t at a terminal. Zimmer’s answer: surface the question as a banner in the web UI, flip the session to needs_input so it lands on your homepage, and let the MCP server poll for your answer over HTTP.

The key insight in the design: block_on_elicitation deliberately does not call cleanup_running_job. A normal pause tears down the agent process. Doing that here would break the round-trip — the MCP server would poll forever into a corpse. So the session shows as needs_input (for your attention queue and notifications) while the process stays alive.

pendingaccept | decline | expired.

There is also a cancel status in the model, but no code path ever writes it. It’s reserved.

Default 10 minutes (Elicitation::DEFAULT_EXPIRATION), overridable by the MCP server via _meta["com.pulsemcp/expires-at"].

Expiry happens two ways: lazily, on each poll (expire_if_needed!), and via CleanupExpiredElicitationsJob every 5 minutes.

If the reactive unblock is missed, the blocked_on_elicitation marker is left set with nothing to clear it, and the session sits in needs_input showing a phantom “blocked on elicitation” that never resolves. This happens when:

  • a swallowed AASM::InvalidTransition (a state race) skips the after block that would have cleared the marker, or
  • the MCP server crashes or is killed mid-round-trip, so no resolve or expire commit ever fires.

CleanupExpiredElicitationsJob calls clear_stale_elicitation_block! to restore the invariant. It strips the marker but leaves the session in needs_input — flipping a minutes-stale block back to running would create a phantom running session with no monitoring job.