Ingram Cloud

Documentation

MCP Apps

MCP Apps

Attach an interactive UI to an agent and an mcp deployment serves it to any host that supports the MCP Apps extension (SEP-1865) — Claude, ChatGPT, Copilot. The host renders your HTML inline in a sandboxed iframe instead of plain text; a host that doesn't support it ignores the UI and shows the reply.

You write standard MCP Apps HTML (the @modelcontextprotocol/ext-apps App class, the ui:// scheme, the text/html;profile=mcp-app MIME) — Ingram Cloud is a conforming MCP Apps server. Templates are tenant-authored and version-pinned; Ingram never generates UI at runtime.

The pieces

  • A template is an HTML bundle attached to an agent under a name. Its bytes live in blob storage; the agent carries the reference.
  • Publishing an agent version freezes its templates into the immutable snapshot, so a rendered panel is reproducible per version.
  • A smith running that agent (over its mcp deployment) is what the host talks to; the panel it renders is byte-identical for every caller.

Author a template

Upload the bundle as multipart — the HTML file part plus a JSON metadata sidecar. Uploads land on the agent's draft; publish a version to freeze it.

# Authorization: tenant-admin token (server-side only)
curl -X POST https://api.cloud.ingram.tech/v1/agents/agt_…/ui \
  -H "Authorization: Bearer $IC_TOKEN" \
  -H "IC-Api-Version: 2026-05-01" \
  -F 'file=@dashboard.html;type=text/html' \
  -F 'metadata={"name":"dashboard","csp":{"connectDomains":["https://api.example.com"]}}'
# → 200 { "name": "dashboard", "content_hash": "…", "csp": { … } }

metadata fields:

  • name — unique per agent ([a-z0-9_-]); re-uploading the same name replaces it.
  • csp — the MCP Apps _meta.ui.csp object (connectDomains, resourceDomains, frameDomains, baseUriDomains), the origins the sandboxed iframe may reach. Omit it and the deny-by-default sandbox allows only a self-contained bundle.
  • permissions — the _meta.ui.permissions map the template requests (e.g. { "camera": {} }).
  • prefers_border_meta.ui.prefersBorder: whether the host draws its own card around the panel. Omit it to take the host's default (Claude: borderless on web, bordered on mobile); set false for a panel that blends into the conversation.
  • domain_meta.ui.domain: a stable sandbox origin, needed only when the panel runs its own OAuth flow or your API allowlists it by Origin. The format is the host's; for Claude it is the first 32 hex characters of the SHA-256 of your deployment's MCP URL followed by .claudemcpcontent.com.
  • tool — present only for a typed app-tool (below).

Every _meta.ui field is echoed to the host verbatim on both resources/list and resources/read, so a host can review the sandbox before it fetches the bundle.

List, fetch, and remove templates on the draft:

# Authorization: tenant-admin token (server-side only)
curl https://api.cloud.ingram.tech/v1/agents/agt_…/ui \
  -H "Authorization: Bearer $IC_TOKEN" -H "IC-Api-Version: 2026-05-01"
# → { "data": [ { "name": "dashboard", … } ] }

# The bundle's bytes, tagged with a strong ETag of its content_hash. Send it
# back as If-None-Match and an unchanged bundle answers 304:
curl https://api.cloud.ingram.tech/v1/agents/agt_…/ui/dashboard/content \
  -H "Authorization: Bearer $IC_TOKEN" -H "IC-Api-Version: 2026-05-01"

curl -X DELETE https://api.cloud.ingram.tech/v1/agents/agt_…/ui/dashboard \
  -H "Authorization: Bearer $IC_TOKEN" -H "IC-Api-Version: 2026-05-01"

The internal blob key is never returned. These endpoints ride the same runs:read / runs:write scopes and tenant-admin gate as the rest of /v1/agents — a smith-scoped token can't touch them.

Then publish so the template is live for the deployment:

# Authorization: tenant-admin token (server-side only)
curl -X POST https://api.cloud.ingram.tech/v1/agents/agt_…/versions \
  -H "Authorization: Bearer $IC_TOKEN" -H "IC-Api-Version: 2026-05-01" \
  -H "Content-Type: application/json" -d '{ "note": "with dashboard UI" }'

How it renders

Deploy the agent as an mcp server and connect a host. A host renders the template its tool descriptor links (_meta.ui.resourceUri on tools/list), so which panel shows is decided by which tool the host calls:

  • Bound to ask — name a template ask and every ask reply renders inside it.
  • Typed app-tools — give a template a tool ({ description, input_schema, instruction, mutating, visibility }). The deployment surfaces one MCP tool per app-tool, each linked to its own template; the host's model routes to the right one, and the smith runs the template's scoped instruction with the host-supplied arguments. A mutating tool carries the standard destructiveHint annotation, so the host asks the user before a write; reads run freely.

The panel receives the run's result over the standard MCP Apps channel: content is the smith's text reply, structuredContent the JSON to render. The smith fills structuredContent by calling the built-in render_app tool (enabled once the agent has a template) with { template, data }data lands in the panel when template is the one the called tool links. Without a render_app call the panel gets { "text": … }, the reply. Keep the payload lean: Claude spills results over ~150k characters to its sandbox filesystem and the panel never hydrates; paginate, and let the panel fetch more.

Tools the panel calls

A rendered panel can call the deployment's tools itself through the host's tools/call proxy — a refresh, the next page, a write. Each call runs a smith turn; a write that needs approval pauses the run on the standard approval channel. Set tool.visibility to ["app"] for a tool only the panel should call: the host keeps it out of its model's tool list, and the descriptor says so in _meta.ui.visibility.

Hosts

Any host in the MCP Apps client matrix renders the same deployment; the two you meet most:

  • Claude — web, desktop, iOS and Android (not Claude Code). Add the deployment as a custom connector; users approve the panel once ("Always allow"). frameDomains is restricted pending Claude's security review; mobile grants no camera, microphone or location. Follow Claude's design guidelines for style tokens and display modes.
  • ChatGPT — implements the same standard: _meta.ui.resourceUri, the profile MIME, csp, prefersBorder, domain, and the ui/* bridge. Add the deployment as a developer-mode connector and the panel renders as-is; the openai/* aliases and window.openai are ChatGPT's compatibility layer for older apps, not something you author against. Capabilities only ChatGPT offers (checkout, file upload, modals) have no standard expression yet and are not exposed.

In your own chat

Templates also render in your own app. An app-embedded chat riding @ingram-cloud/ai-sdk can render the same bundle: render_app arrives as a standard tool call carrying { template, data } — there, any template it names renders — and …/ui/{name}/content serves the canonical bytes. One template covers MCP hosts, your own app, and text degradation, with no second copy to drift.

Degradation

Only MCP hosts render ui:// panels. On a chat channel (Slack, Telegram, email) or any host without the extension, the smith's text reply stands on its own — so write a reply that reads well without the UI.

In the console

An agent's Deploy tab lists its apps under MCP Apps, uploads new bundles, sets connect origins, the border preference and app-only tools, and previews a template. The preview is an MCP Apps host: it answers the bundle's ui/initialize, then delivers tool-input and a sample tool-result, so a bundle built on @modelcontextprotocol/ext-apps renders exactly as it connects in Claude or ChatGPT. permissions and domain are set through the API or Pulumi.