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.
The idea
Section titled “The idea”Write the approval skill once; the server adapts. Your skill always makes the
same calls — request_approval, then await_approval, then apply. 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.
The tools
Section titled “The tools”Four tools, each returning its result as JSON. Shapes match the server handlers exactly.
request_approval — record an approval request and (best-effort) push a
URL-mode prompt to capable hosts.
Input: { "purpose": "Deploy to production", "payload": { … }, "ttlSeconds": 3600 }Output: { "elicitationId": "…", "status": "pending", "reviewUrl": "https://…/review?elicitationId=…", "expiresAt": "2026-07-16T12:00:00.000Z", "elicitationDelivered": true }payloadis the exact thing that will be executed on approval — the server stores it and hands it back fromapply, so the human approves what actually runs.elicitationDeliveredistrueonly when the host advertised theelicitation: { url: {} }capability. The prompt is non-blocking;reviewUrlis always returned so any host can reach the review page.ttlSecondsis optional (default3600, clamped60–86400).
await_approval — block until the request is decided, or the wait bound
elapses.
Input: { "elicitationId": "…" }Output: { "status": "approved" } // or "rejected" | "pending" | "applied" | "expired"- A bounded long-poll: waits up to
min(server bound, time until expiry), returning the instant a decision lands. If it returns"pending", the wait bound was hit before a decision — just call it again (see below).
check_status — a non-blocking status read.
Input: { "elicitationId": "…" }Output: { "status": "pending" } // pending | approved | rejected | applied | expiredapply — execute an approved request; returns the stored payload.
Input: { "elicitationId": "…" }Output: { "status": "applied", "payload": { … } }- Succeeds only once the request is
approved. The returnedpayloadis the one stored at request time, never a value the agent re-supplies.
How a request plays out
Section titled “How a request plays out”The skill’s column never changes. The URL push is the only capability-dependent
step, and both paths converge on the same await_approval wake.
The long-poll loop
Section titled “The long-poll loop”await_approval 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"; callawait_approvalagain 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.
Expiry
Section titled “Expiry”Every request carries a TTL (ttlSeconds, default 1 hour, clamped 60 seconds to
24 hours). Once it passes, the request is expired — a terminal state.
await_approval and check_status both report "expired" at the deadline, and
the review page refuses a late decision. Ask the agent to resend if that happens.