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 declines 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 { submit?, decline? }, each 1–40 characters; renames the decision buttons in both modes: submit is the submit button (default Submit), decline the decline button (default Decline). Labels only rename — button placement and styling stay fixed.
expiresInSeconds number no Default 3600 (1 hour), clamped 60604800 (1 minute to 7 days) — how long the request stays decidable.

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

{ "message": "Deploy to production", "context": { "target": "prod" }, "expiresInSeconds": 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 Submit / Decline 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>"
}
displayTemplate — expanded for readability
<dl>
<dt>Customer</dt><dd>{{ context.customer }}</dd>
<dt>Amount</dt><dd>{{ context.amount }}</dd>
<dt>Reason</dt><dd>{{ context.reason }}</dd>
</dl>
What the reviewer sees

Approve refund

SubmitDecline

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>"
}
displayTemplate — expanded for readability
<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>
What the reviewer sees

Approve production deploy

SubmitDecline

{% 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>"
}
displayTemplate — expanded for readability
<p><strong>{{ context.field }}</strong></p>
<p><s>{{ context.current }}</s><mark>{{ context.proposed }}</mark></p>
What the reviewer sees

Approve plan change

SubmitDecline

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

{
"message": "Approve the deploy",
"context": {
"service": "elicitly-api",
"version": "v2.4.0"
},
"displayTemplate": "<p>Deploying <strong>{{ context.service }} {{ context.version }}</strong></p>",
"requestedSchema": {
"type": "object",
"properties": {
"approver": {
"type": "string",
"title": "Your name",
"minLength": 2
},
"confidence": {
"type": "integer",
"title": "Confidence %",
"minimum": 0,
"maximum": 100
}
},
"required": [
"approver",
"confidence"
]
}
}
displayTemplate — expanded for readability
<p>Deploying <strong>{{ context.service }} {{ context.version }}</strong></p>
What the reviewer sees

Approve the deploy

SubmitDecline

Pass displayTemplate and requestedSchema together: the custom rendering of the immutable context appears first, with the reviewer’s form directly below it. The template does not pre-fill the form — fields are seeded only from requestedSchema defaults — and the reviewer’s validated values come back as formSubmission from elicit_result.

{
"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 on a legacy (pre-2026-07-28) connection — that revision removed server-initiated push, so it is always false there. The prompt is non-blocking either way; reviewUrl is always returned so any host can reach the review page.