Skip to content

elicit_approval

Record an approval request and (best-effort) push a URL-mode elicitation prompt to capable hosts. The first step of the Approval Flow.

A decision that must be durable: it survives disconnects, reaches any reviewer on any device via the hosted review page, and leaves an audit trail. JSON mode shows a read-only payload the reviewer approves or rejects as-is; form mode has the reviewer fill in fields that become the approved data. For an in-host dialog answered by the person at the keyboard right now, see Choosing a tool.

At least one of context / requestedSchema is required.

Parameter Type Required Description
message string yes Human-readable summary shown to the reviewer.
context any one of these two A read-only payload displayed to the reviewer verbatim and handed back unchanged from elicit_result, so the human approves exactly what runs.
requestedSchema object one of these two A JSON Schema restricted to the MCP elicitation requestedSchema subset: a flat object whose properties are strings, numbers/integers, booleans, or single/multi-select enums — no nested objects, arrays of objects, $ref, or allOf. The reviewer fills these fields on the review page; their validated values come back as formSubmission from elicit_result.
displayTemplate string no A LiquidJS template that customizes how context is rendered on the review page, replacing the default pretty-printed JSON. The payload is exposed as context (e.g. {{ context.field }}). Max 20 KB. Combinable with requestedSchema — the template renders first, the form below it. See Display templates.
labels object no { ok?, cancel? }, each 1–40 characters; renames the decision buttons in both modes: ok is the approve button (default Approve), cancel the reject button (default Reject). Labels only rename — button placement and styling stay fixed.
timeoutSeconds number no Default 3600, clamped 6086400 — how long the request stays decidable.

JSON mode — a read-only payload the reviewer approves or rejects as-is:

{ "message": "Deploy to production", "context": { "target": "prod" }, "timeoutSeconds": 3600 }

Form mode — a schema the reviewer fills in; their submission becomes the approved data:

{
"message": "Approve the deploy",
"requestedSchema": {
"type": "object",
"properties": {
"approver": { "type": "string", "title": "Your name", "minLength": 2 },
"confidence": { "type": "integer", "title": "Confidence %", "minimum": 0, "maximum": 100 }
},
"required": ["approver", "confidence"]
}
}

Both may be passed together: context is then shown as read-only context alongside the form — it is not merged into or used to pre-fill the fields (those are seeded only from requestedSchema defaults).

By default, context is shown as pretty-printed JSON in a code block. A displayTemplate replaces that with your own LiquidJS markup — a table, a diff, a labeled summary — so the reviewer sees the decision, not a blob. The payload is available as the context variable. See Templates for the shared engine rules — JavaScript-style truthiness, escaping, and limits.

How it’s rendered (and why it’s safe):

  • Rendered server-side; the output is displayed inside a sandboxed, null-origin iframe (sandbox="allow-scripts", no same-origin access). Template markup and any scripts it contains are fully isolated — they cannot read the reviewer’s session, cookies, or the surrounding page.
  • The Approve / Reject controls live outside the iframe, so a template can never spoof or intercept the decision.
  • Interpolated values ({{ context.… }}) are HTML-escaped, so payload content can’t break the layout or inject markup — your template’s own tags render as-is, the data does not.
  • If the template fails to parse or render, the review page falls back to the pretty-printed JSON — a bad template never blocks an approval.
  • Templates run against limits (size ≤ 20 KB, parse/render/memory caps) and have no filesystem or include/render access.
{
"message": "Approve refund",
"context": { "customer": "acme@co", "amount": "$1,240.00", "reason": "duplicate charge" },
"displayTemplate": "<dl><dt>Customer</dt><dd>{{ context.customer }}</dd><dt>Amount</dt><dd>{{ context.amount }}</dd><dt>Reason</dt><dd>{{ context.reason }}</dd></dl>"
}

Renders the payload as a clean definition list instead of raw JSON.

{
"message": "Approve production deploy",
"context": {
"service": "elicitly-api",
"version": "v2.4.0",
"changes": [
{ "file": "src/auth.ts", "added": 12, "removed": 3 },
{ "file": "src/db.ts", "added": 5, "removed": 40 }
]
},
"displayTemplate": "<strong>Deploy {{ context.service }} {{ context.version }}</strong><table><tr><th>File</th><th>+</th><th>−</th></tr>{% for c in context.changes %}<tr><td>{{ c.file }}</td><td>+{{ c.added }}</td><td>−{{ c.removed }}</td></tr>{% endfor %}</table>"
}

{% for %} iterates the array; each row shows the file and its added/removed counts.

{
"message": "Approve plan change",
"context": { "field": "Plan", "current": "Starter", "proposed": "Enterprise" },
"displayTemplate": "<p><strong>{{ context.field }}</strong></p><p><s>{{ context.current }}</s> → <mark>{{ context.proposed }}</mark></p>"
}

Highlights what changes — the old value struck through, the new value marked.

Pass displayTemplate and requestedSchema together to show a custom rendering of the immutable context first, with the reviewer’s form directly below it (the template does not pre-fill the form; fields are seeded only from requestedSchema defaults).

{
"elicitationId": "",
"status": "pending",
"reviewUrl": "https://…/review?elicitationId=…",
"expiresAt": "2026-07-16T12:00:00.000Z",
"elicitationDelivered": true
}
  • elicitationDelivered is true only when the host advertised the elicitation: { url: {} } capability. The prompt is non-blocking; reviewUrl is always returned so any host can reach the review page.