Skip to content

Approval Flow

Agents increasingly take consequential actions, so they need a clean way to pause and ask a human — confirm, approve — and then continue. MCP’s URL-mode elicitation can push a native “open this link?” prompt to the host, but host support for it is inconsistent.

Write the approval skill once; the server adapts. Your skill always makes the same calls — elicit_approval, then elicit_await, then elicit_result. Underneath, the server pushes a URL-mode elicitation prompt only to hosts that advertise support for it; every host reaches the same outcome through the same long-poll.

Diagram

elicit_approval takes either (or both) of two inputs, and the review page adapts:

  • JSON mode — pass context, a read-only payload. The reviewer sees it and approves or rejects it as-is; elicit_result hands it back verbatim, so the human approves exactly what runs.
  • Form mode — pass a requestedSchema (the flat MCP elicitation subset: strings, numbers, booleans, single/multi-select enums). The review page renders a validated web form; the reviewer’s submission comes back as formSubmission. Use form mode to collect a decision and structured input — a rollback plan, an amount, sign-off initials — in one approval step. Form mode must not request secrets or credentials (per the MCP spec).

Pass both to show read-only context above a form. Whichever you use, the pause/resume flow below is identical.

Four tools drive the flow — each has its own reference page with exact input/output shapes:

  • elicit_approval — record the request and (best-effort) push a URL-mode prompt; always returns reviewUrl.
  • elicit_await — bounded long-poll until the request is decided or the wait bound elapses.
  • elicit_status — non-blocking status read.
  • elicit_result — fetch the reviewer’s decision plus the approved data (idempotent, every fetch recorded).
Diagram

The skill’s column never changes. The URL push is the only capability-dependent step, and both paths converge on the same elicit_await wake.

elicit_await is bounded so it always returns before the host’s tool-call timeout. Two ways it returns:

  • A decision landed — the reviewer approved or rejected; the call wakes immediately with that status.
  • The bound elapsed — it returns "pending"; call elicit_await again to keep waiting.

That single loop works everywhere: on a URL-capable host the reviewer likely acts on the pushed prompt; on any other host they open reviewUrl — either way the same wake resolves the same loop.

Every request carries a TTL (timeoutSeconds, default 1 hour, clamped 60 seconds to 24 hours). Once it passes, the request is expired — a terminal state. elicit_await and elicit_status both report "expired" at the deadline, and the review page refuses a late decision. Ask the agent to resend if that happens.