Skip to content

elicit_display

Show information to the user as rendered HTML instead of Markdown: pass a template plus JSON context, and hosts that support MCP Apps render it inline in the conversation — tables, reports, cards, images.

When the agent needs to show something the user should scan — a comparison table, a status report, a summary with images — and Markdown tables and prose fall short. It does not collect a decision: pair it with elicit_confirm, or use a Pro tool such as elicit_approval or elicit_selection when the human must answer.

Write the template once (in a skill, say) and the model only supplies data: the same layout every time, however the data changes. The rendered HTML never enters the model’s context — it gets back only your summary — and when the host renders, the model doesn’t need to repeat the information in its reply.

Parameter Type Required Description
template string yes A template producing an HTML fragment, rendered against {{ context.* }}. Max 32 KB.
context object no JSON data exposed to the template as context. Max 64 KB serialized.
title string no Heading the view shows above the render, beside the host’s Fullscreen button where there is one (1–120 characters). Leave it out when the template has its own heading.
summary string no One or two sentences describing what is shown; returned as the tool’s text result (1–500 characters).

The template contract — escaping, JavaScript-style truthiness, own-property access — is the same as the review pages’; see Templates.

{
"title": "Open invoices",
"summary": "Three open invoices totalling $4,210.",
"template": "<link href=\"https://cdn.jsdelivr.net/npm/daisyui@5\" rel=\"stylesheet\"><script src=\"https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4\"></script><table class=\"table table-zebra\">{% for i in context.invoices %}<tr><td>{{ i.number }}</td><td class=\"text-right\">{{ i.amount }}</td></tr>{% endfor %}</table>",
"context": {
"invoices": [
{ "number": "INV-1041", "amount": "$1,200" },
{ "number": "INV-1043", "amount": "$2,450" },
{ "number": "INV-1047", "amount": "$560" }
]
}
}

The rendered HTML is not returned to the model. It travels in the result’s _meta (_meta["elicitly/display"]), which hosts deliver to the view but keep out of the model’s context. The model gets a short text result: your summary (or Displayed "<title>".), plus a note when something needs its attention:

Sequence: the model calls elicit_display through the host; Elicitly renders the template and returns a summary, with the HTML in _meta. The host passes only the summary and notes to the model and the HTML to a sandboxed view, which loads images and styles only from jsDelivr or the Pro resource proxy.
  • The host didn’t advertise MCP Apps — the user may not see the render, so the note asks the model to present the key information in its reply too.
  • The template references blocked origins — the note names each origin the view will block (with a reference count) and the allowed ones, so the model can fix the template instead of guessing.

On the hosted Pro server the result also carries structuredContent, with the same notes — some clients give the model only structuredContent and drop the text:

{
"summary": "Three open invoices totalling $4,210.",
"blockedOrigins": [{ "origin": "https://upload.wikimedia.org", "references": 1 }],
"appsAdvertised": true,
"notes": [
"The view blocks content from https://upload.wikimedia.org (1 reference), so it won't load. …"
]
}

appsAdvertised is whether the host advertised MCP Apps support — null when the server couldn’t tell.

A template that fails to parse or render returns a tool error with the template engine’s message, so the model can correct it.

The view runs in the host’s sandbox under a fixed Content Security Policy. Templates may load from:

  • https://cdn.jsdelivr.net — always. Load Tailwind (https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4) and DaisyUI (https://cdn.jsdelivr.net/npm/daisyui@5) with your own <script> / <link> tags. The view sets data-theme to the host’s light/dark theme, so DaisyUI follows it.
  • Origins you allow.
    • Pro (hosted): an organization admin lists them under Organization → elicit_display allowed origins in the dashboard (up to 20; wildcard subdomains such as https://*.example.com work). Templates keep using the normal URLs: when the tool renders, the server rewrites them to signed links on Elicitly’s resource proxy, which fetches the image, stylesheet, font or script for the view. The links are bound to your organization and expire after 30 days, and removing an origin stops its links from loading within about a minute — including in past renders. The proxy serves only images, stylesheets, fonts and scripts (up to 5 MB each), and its requests carry no user or organization details.
    • Free (local): set ELICITLY_DISPLAY_RESOURCE_DOMAINS to comma-separated https:// origins (e.g. https://upload.wikimedia.org), or fill in elicit_display: extra allowed origins in the Claude Desktop extension’s settings. Wildcard subdomains (https://*.example.com) work.
  • data: URIs — for images that aren’t publicly reachable (generated, read from a local file, or from a private source). Pass them in context rather than the template: 64 KB versus 32 KB.

Everything else is blocked, and the tool’s result tells the model which origins it used that aren’t allowed, so it can drop them or ask you to allow them. The model is told not to download public images to get around the policy.

Links in a template open in the user’s browser (the view routes clicks through the host), so they don’t need target="_blank" here.

Section titled “A carousel with images and links: Jupiter’s moons”

Cards the user swipes through, each with an image, a few facts, and a link to read more. The images come from https://upload.wikimedia.org, so the origin must be allowed first (see above); the Wikipedia links need nothing — the host opens them in the browser.

{
"summary": "Jupiter and its four Galilean moons — Io, Europa, Ganymede and Callisto — with size, orbit and a link to each on Wikipedia.",
"template": "<link href=\"https://cdn.jsdelivr.net/npm/daisyui@5\" rel=\"stylesheet\"><script src=\"https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4\"></script>{% assign p = context.planet %}<div class=\"flex items-center gap-4 mb-4\"><img src=\"{{ p.image }}\" alt=\"{{ p.name }}\" class=\"size-20 rounded-full bg-black shrink-0\"><div><h2 class=\"text-lg font-bold\">{{ p.name }}</h2><p>{{ p.blurb }}</p><a class=\"link\" href=\"{{ p.url }}\">{{ p.name }} on Wikipedia</a></div></div><div class=\"carousel carousel-center w-full gap-3 pb-2 motion-reduce:scroll-auto focus-visible:outline-2 focus-visible:outline-primary rounded-box\"tabindex=\"0\" role=\"region\" aria-label=\"Moons — scroll sideways for more\">{% for m in context.moons %}<div class=\"carousel-item w-64\"><div class=\"card bg-base-200 w-full\"><figure class=\"bg-black\"><img src=\"{{ m.image }}\" alt=\"{{ m.name }}\" class=\"h-36 object-contain\"></figure><div class=\"card-body p-4 gap-1\"><h3 class=\"card-title text-base\">{{ m.name }}<span class=\"badge badge-sm\">{{ forloop.index }} of {{ forloop.length }}</span></h3><p>{{ m.fact }}</p><p class=\"text-sm\">{{ m.diameterKm }} km across · orbits in {{ m.orbitDays }} days</p><a class=\"link\" href=\"{{ m.url }}\">{{ m.name }} on Wikipedia</a><p class=\"text-xs mt-1\">Image: {{ m.credit }}</p></div></div></div>{% endfor %}</div>",
"context": {
"planet": {
"name": "Jupiter",
"blurb": "The largest planet: 139,820 km across, one orbit every 11.86 years.",
"image": "https://upload.wikimedia.org/wikipedia/commons/thumb/e/e2/Jupiter_OPAL_2024.png/330px-Jupiter_OPAL_2024.png",
"url": "https://en.wikipedia.org/wiki/Jupiter"
},
"moons": [
{
"name": "Io",
"fact": "The most volcanically active body in the Solar System.",
"diameterKm": "3,643",
"orbitDays": "1.77",
"image": "https://upload.wikimedia.org/wikipedia/commons/thumb/0/07/Io_highest_resolution_natural_color.png/330px-Io_highest_resolution_natural_color.png",
"url": "https://en.wikipedia.org/wiki/Io_(moon)",
"credit": "NASA (public domain)"
},
{
"name": "Europa",
"fact": "An ice shell over a salty ocean that may hold more water than Earth's.",
"diameterKm": "3,122",
"orbitDays": "3.55",
"image": "https://upload.wikimedia.org/wikipedia/commons/thumb/b/b3/Europa_-_Perijove_45_%28cropped%29.png/330px-Europa_-_Perijove_45_%28cropped%29.png",
"url": "https://en.wikipedia.org/wiki/Europa_(moon)",
"credit": "NASA/JPL-Caltech/SwRI/MSSS/Kevin M. Gill (CC BY 2.0)"
},
{
"name": "Ganymede",
"fact": "The largest moon in the Solar System — bigger than Mercury.",
"diameterKm": "5,268",
"orbitDays": "7.15",
"image": "https://upload.wikimedia.org/wikipedia/commons/thumb/2/21/Ganymede_-_Perijove_34_Composite.png/330px-Ganymede_-_Perijove_34_Composite.png",
"url": "https://en.wikipedia.org/wiki/Ganymede_(moon)",
"credit": "NASA/JPL-Caltech/SwRI/MSSS/Kevin M. Gill (CC BY 2.0)"
},
{
"name": "Callisto",
"fact": "One of the most heavily cratered surfaces known.",
"diameterKm": "4,821",
"orbitDays": "16.69",
"image": "https://upload.wikimedia.org/wikipedia/commons/thumb/c/c6/Callisto_VGR2_C2060635_OGB.png/330px-Callisto_VGR2_C2060635_OGB.png",
"url": "https://en.wikipedia.org/wiki/Callisto_(moon)",
"credit": "NASA / zelario12 (CC BY-SA 2.0)"
}
]
}
}
template — expanded for readability
<link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
{% assign p = context.planet %}
<div class="flex items-center gap-4 mb-4">
<img src="{{ p.image }}" alt="{{ p.name }}" class="size-20 rounded-full bg-black shrink-0">
<div>
<h2 class="text-lg font-bold">{{ p.name }}</h2>
<p>{{ p.blurb }}</p>
<a class="link" href="{{ p.url }}">{{ p.name }} on Wikipedia</a>
</div>
</div>
<div class="carousel carousel-center w-full gap-3 pb-2 motion-reduce:scroll-auto focus-visible:outline-2 focus-visible:outline-primary rounded-box"
tabindex="0" role="region" aria-label="Moons — scroll sideways for more">
{% for m in context.moons %}
<div class="carousel-item w-64">
<div class="card bg-base-200 w-full">
<figure class="bg-black"><img src="{{ m.image }}" alt="{{ m.name }}" class="h-36 object-contain"></figure>
<div class="card-body p-4 gap-1">
<h3 class="card-title text-base">{{ m.name }}
<span class="badge badge-sm">{{ forloop.index }} of {{ forloop.length }}</span></h3>
<p>{{ m.fact }}</p>
<p class="text-sm">{{ m.diameterKm }} km across · orbits in {{ m.orbitDays }} days</p>
<a class="link" href="{{ m.url }}">{{ m.name }} on Wikipedia</a>
<p class="text-xs mt-1">Image: {{ m.credit }}</p>
</div>
</div>
</div>
{% endfor %}
</div>
What the user sees, inline in the conversation

The carousel is DaisyUI’s, which scrolls with CSS alone — swipe, trackpad, or focus it and use the arrow keys. The next card peeks in at the edge, so it’s clear there’s more.

A ranked list with photos: top salespeople

Section titled “A ranked list with photos: top salespeople”

A leaderboard a sales skill can show before asking who should take the next big account. Four people have photos; the fifth has none, so the template falls back to initials — computed in the template, not sent in context.

{
"title": "Top salespeople — October",
"summary": "The top five salespeople by sales this month, with quarter and year-to-date totals and progress to quarterly quota.",
"template": "<link href=\"https://cdn.jsdelivr.net/npm/daisyui@5\" rel=\"stylesheet\"><script src=\"https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4\"></script><ol class=\"@container list bg-base-100 rounded-box\">{% for p in context.people %}<li class=\"list-row items-center\"><span class=\"text-2xl font-bold tabular-nums w-6 text-center\">{{ forloop.index }}</span>{% if p.photo %}<div class=\"avatar\"><div class=\"w-14 rounded-full\"><img src=\"{{ p.photo }}\" alt=\"\"></div></div>{% else %}{% assign parts = p.name | split: \" \" %}<div class=\"avatar avatar-placeholder\"><div class=\"w-14 rounded-full bg-neutral text-neutral-content\"><span class=\"text-lg\">{{ parts.first | slice: 0 }}{{ parts.last | slice: 0 }}</span></div></div>{% endif %}<div class=\"list-col-grow\"><div class=\"font-semibold\">{{ p.name }}</div><div class=\"text-sm\">{{ p.region }}</div><progress class=\"progress progress-primary w-full max-w-48\" value=\"{{ p.quotaPct }}\" max=\"100\"aria-label=\"{{ p.name }}: {{ p.quotaPct }}% of quarterly quota\"></progress><div class=\"text-xs\">{{ p.quotaPct }}% of quarterly quota</div></div><dl class=\"@max-md:row-start-2 @max-md:col-start-2 @max-md:col-span-2 @max-md:text-left grid grid-cols-3 gap-x-4 text-right tabular-nums\"><dt class=\"text-xs\">Month</dt><dt class=\"text-xs\">Quarter</dt><dt class=\"text-xs\">Year</dt><dd class=\"font-semibold\">{{ p.month }}</dd><dd>{{ p.quarter }}</dd><dd>{{ p.year }}</dd></dl></li>{% endfor %}</ol>",
"context": {
"people": [
{
"name": "Mei Chen",
"region": "West",
"month": "$184K",
"quarter": "$512K",
"year": "$1.92M",
"quotaPct": 118,
"photo": "https://www.elicitly.ai/examples/salespeople/mei-chen.jpg"
},
{
"name": "Marcus Johnson",
"region": "Central",
"month": "$171K",
"quarter": "$468K",
"year": "$1.74M",
"quotaPct": 109,
"photo": "https://www.elicitly.ai/examples/salespeople/marcus-johnson.jpg"
},
{
"name": "Sofia Ramirez",
"region": "Southeast",
"month": "$158K",
"quarter": "$455K",
"year": "$1.61M",
"quotaPct": 104
},
{
"name": "Arjun Mehta",
"region": "Northeast",
"month": "$149K",
"quarter": "$431K",
"year": "$1.58M",
"quotaPct": 98,
"photo": "https://www.elicitly.ai/examples/salespeople/arjun-mehta.jpg"
},
{
"name": "Grace Okafor",
"region": "EMEA",
"month": "$142K",
"quarter": "$402K",
"year": "$1.49M",
"quotaPct": 95,
"photo": "https://www.elicitly.ai/examples/salespeople/grace-okafor.jpg"
}
]
}
}
template — expanded for readability
<link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<ol class="@container list bg-base-100 rounded-box">
{% for p in context.people %}
<li class="list-row items-center">
<span class="text-2xl font-bold tabular-nums w-6 text-center">{{ forloop.index }}</span>
{% if p.photo %}
<div class="avatar"><div class="w-14 rounded-full"><img src="{{ p.photo }}" alt=""></div></div>
{% else %}
{% assign parts = p.name | split: " " %}
<div class="avatar avatar-placeholder"><div class="w-14 rounded-full bg-neutral text-neutral-content">
<span class="text-lg">{{ parts.first | slice: 0 }}{{ parts.last | slice: 0 }}</span>
</div></div>
{% endif %}
<div class="list-col-grow">
<div class="font-semibold">{{ p.name }}</div>
<div class="text-sm">{{ p.region }}</div>
<progress class="progress progress-primary w-full max-w-48" value="{{ p.quotaPct }}" max="100"
aria-label="{{ p.name }}: {{ p.quotaPct }}% of quarterly quota"></progress>
<div class="text-xs">{{ p.quotaPct }}% of quarterly quota</div>
</div>
<dl class="@max-md:row-start-2 @max-md:col-start-2 @max-md:col-span-2 @max-md:text-left grid grid-cols-3 gap-x-4 text-right tabular-nums">
<dt class="text-xs">Month</dt><dt class="text-xs">Quarter</dt><dt class="text-xs">Year</dt>
<dd class="font-semibold">{{ p.month }}</dd><dd>{{ p.quarter }}</dd><dd>{{ p.year }}</dd>
</dl>
</li>
{% endfor %}
</ol>
What the user sees, inline in the conversation

Top salespeople — October

To try it as written, allow https://www.elicitly.ai, where the sample photos live. In your own skill, photos would come from your CRM or intranet — an origin you allow once (Pro: for the whole organization) — or as data: URIs in context.

Not every host renders MCP Apps. The tool still works there: it returns the text result, with a note that tells the model to present the information itself. For the Jupiter example, a host without MCP Apps (the Claude Code CLI, say) gives the model:

Jupiter and its four Galilean moons — Io, Europa, Ganymede and Callisto — with size, orbit and a link to each on Wikipedia. However, this host did not advertise MCP Apps support, so the user may not see the render — present the key information in your reply as well.

The model already has the data — it wrote the context — so the user still gets an answer, in Markdown, the way chat answers always looked:

Jupiter's four Galilean moons:
| Moon | Diameter | Orbit | Notable for |
| --- | --- | --- | --- |
| Io | 3,643 km | 1.77 days | Most volcanically active body in the Solar System |
| Europa | 3,122 km | 3.55 days | Ice shell over a salty ocean |
| Ganymede | 5,268 km | 7.15 days | Largest moon in the Solar System |
| Callisto | 4,821 km | 16.69 days | Heavily cratered surface |

Same facts, but the pictures and the at-a-glance layout are gone — which is the difference elicit_display makes where hosts support it.

MCP Apps is an extension to MCP, and support varies by host. Verified so far:

  • Claude Desktop renders inline from the local elicitly server and the Claude Desktop extension, follows the host theme, and offers fullscreen. In turns where the agent also runs commands, Claude Desktop groups the steps and shows the call as a collapsed row instead of the inline view.
  • Claude (claude.ai) renders inline from the hosted server and follows the host theme.
  • Claude Code in the Claude desktop app renders inline from the hosted server (connectors there reach it through claude.ai).
  • Claude Code CLI doesn’t advertise MCP Apps, so nothing renders; the model gets the note to present the information itself. It reads structuredContent rather than the text, which is why the notes are in both.
  • ChatGPT renders inline from the hosted server through the standard MCP Apps metadata, follows the host theme (including a light/dark switch while the render stays open), opens fullscreen as a side panel beside the chat, and opens template links in the browser. ChatGPT passes structuredContent to the model as well, so the model also sees { summary, blockedOrigins, appsAdvertised, notes }.

Hosts that don’t support MCP Apps still get the text result — with the note asking the model to present the information itself. See the MCP client matrix for other hosts.

  • Templates — the shared template contract.
  • MCP Apps — the extension elicit_display renders through.