MCP Apps support
read as.md MCP Apps is the protocol extension that lets MCP servers ship interactive UI alongside structured data, and lets MCP hosts render those UIs inline in the chat surface. The OSS @ggui-ai/mcp-server implements the wire format on both sides — as will the hosted ggui server (mcp.ggui.ai, coming soon) — so generative UIs render directly in chat instead of forcing a “click this link” detour to a browser tab.
This page documents the protocol pieces ggui implements. For end-user setup, see Connect Claude Desktop. For the underlying transport, see WebSocket protocol.
What MCP Apps adds
Section titled “What MCP Apps adds”Without MCP Apps, an MCP tool that produces UI has to choose between:
- Returning structured data and hoping the host formats it (no interactivity), or
- Returning a URL the user clicks out to (interactive, but chat and UI live in separate windows).
MCP Apps adds a third option: declare a UI resource alongside the tool result, the host sandboxes it in an iframe inside the chat, and a WebSocket channel carries data both ways — host to UI for live updates, UI to server for actions.
What ggui ships
Section titled “What ggui ships”When the server boots with mcpApps enabled, three things happen:
io.modelcontextprotocol/uiis advertised in the server’sinitializecapabilities (underexperimental). MCP-Apps-aware hosts read this and switch on inline rendering.ui://ggui/renderis served as a resource viaresources/read— a minimal HTML shell that loads@ggui-ai/iframe-runtimeand opens the WebSocket channel.- Every
ggui_rendertool result carries the_meta["ai.ggui/render"]slice —sessionId,appId,runtimeUrl,wsUrl, a short-TTLwsToken, andexpiresAt(ISO 8601 string) as top-level fields, alongside capability + render-state fields, plus theme fields (themeId,themeMode,theme— a validated--ggui-*CSS-variable overlay the iframe applies at:root), apollingUrlfallback for WS-blocked environments, andlastSequenceto seed replay cursors. The iframe consumes the slice, opens the WebSocket, and trades thewsTokenfor a longer-livedsessionTokenfor reconnects.
Capability declaration
Section titled “Capability declaration”On initialize, ggui returns:
{ "capabilities": { "tools": { "listChanged": true }, "resources": { "subscribe": false, "listChanged": false }, "experimental": { "io.modelcontextprotocol/ui": {} } }}Hosts that recognize io.modelcontextprotocol/ui flip into inline-render mode. Hosts that don’t simply ignore the capability and skip inline rendering — the render is still delivered as a resource (ui://ggui/render/<id> on _meta.ui.resourceUri), but without MCP-Apps support there is nothing to mount it. There is no agent-returned URL to open instead.
Tool result shape
Section titled “Tool result shape”Every UI-producing tool (today: ggui_render) declares a meta-resource on the result so the host knows where to load the UI from:
{ "content": [{ "type": "text", "text": "Created render render_abc123" }], "_meta": { "ui": { "resourceUri": "ui://ggui/render/render_abc123" }, "ai.ggui/render": { "sessionId": "render_abc123", "appId": "app_abc", "runtimeUrl": "https://your-server.example.com/_ggui/iframe-runtime.js", "wsUrl": "wss://your-server.example.com/ws", "wsToken": "btkn_…", "expiresAt": "2099-01-01T00:00:00.000Z" } }}The tool declaration (returned by tools/list) is what carries _meta.ui.visibility: ["model"] — the MCP Apps signal that this tool ships a renderable UI surface. Each per-call result stamps _meta.ui.resourceUri (the per-render URI) plus the _meta["ai.ggui/render"] slice. The host fetches ui://ggui/render (or the per-render form ui://ggui/render/<sessionId>) once, sandboxes it in an iframe, and forwards the _meta["ai.ggui/render"] slice to it.
The shell at ui://ggui/render
Section titled “The shell at ui://ggui/render”Reading the resource returns a small HTML document — paper-themed, full-bleed, no chrome — whose only job is:
- Receive the
ai.ggui/renderslice from the host (viapostMessage). - Dynamically load
runtimeUrl(the iframe-runtime bundle). - Hand the slice to the runtime, which opens the WebSocket and starts rendering.
The shell is intentionally minimal. The actual rendering work — component resolution, contract validation, action dispatch, gadget loading — lives in @ggui-ai/iframe-runtime, which the shell loads on demand. This keeps the shell payload tiny and lets the runtime version-bump independently of host caches.
Bootstrap token exchange
Section titled “Bootstrap token exchange”The wsToken is short-lived (default 180s) and reusable within its TTL, so a transient WebSocket drop reconnects without a fresh handshake. The iframe trades it for a longer-lived sessionToken (default 4h) on the first wsToken-authed subscribe frame, then uses the session token (sessionToken) for reconnects. After the wsToken expires, the iframe swaps the envelope via ggui_runtime_refresh_ws_token (within the refresh window) or re-bootstraps. Consequences:
- Hosts can cache the resource document, but the bootstrap is per-call — every render mints a fresh token.
- An iframe that loses connection reconnects with its session token (
sessionToken) without re-fetching the resource or re-running OAuth. - A leaked bootstrap token is useless after the TTL expires.
The bootstrap is HMAC-signed with a server-side wsTokenSecret. Multi-pod deployments MUST share a deterministic secret (typically from a secrets manager) so any pod accepts any other pod’s tokens. The handshake details are documented in Bootstrap handshake.
Self-hosted: enabling MCP Apps in your own server
Section titled “Self-hosted: enabling MCP Apps in your own server”import { createGguiServer } from "@ggui-ai/mcp-server";
const server = createGguiServer({ // ... renderChannel: true, // required — MCP Apps needs the WS channel mcpApps: { wsUrl: "wss://your-server.example.com/ws", }, runtime: true, // serve the iframe-runtime bundle wsTokenSecret: process.env.WS_TOKEN_SECRET, // required for multi-pod});For local dev, wsUrl: "ws://127.0.0.1:6781/ws" is the conventional loopback URL. Hosted ggui (coming soon) will use wss://mcp.ggui.ai/ws.
What each option does:
renderChannel: true— mounts the live-channel WebSocket at/ws. MCP Apps requires it; the iframe has nowhere to connect without one.mcpApps.wsUrl— the publicly-reachable WebSocket URL written onto every bootstrap. Don’t shipws://localhost:…to internet-accessible servers — clients must be able to reach it.runtime: true(default whenmcpAppsis on) — mounts the iframe-runtime bundle at/_ggui/iframe-runtime.js. Passruntime: { url: "https://your-cdn/…" }to point at an externally-hosted bundle.wsTokenSecret— HMAC secret. If omitted, the server mints a random secret at boot — fine for single-process dev, wrong for multi-pod (pods would reject each other’s tokens).
mcpApps requires renderChannel: true; the factory throws at construction if you enable one without the other.
Compatibility matrix
Section titled “Compatibility matrix”Host capabilities below describe what each MCP host supports when connected to your self-hosted server (a hosted ggui connector is coming soon):
| Host | OAuth | MCP Apps | Notes |
|---|---|---|---|
| Claude Desktop | Yes | Yes | Inline rendering, full UX. (install) |
| claude.ai (web) | Yes | Yes | Same as Desktop. |
| Goose | Yes | Yes | Inline rendering in TUI mode varies by terminal. |
| VS Code Copilot | Yes | Yes | UI renders in a side panel. |
| Cursor | Yes | Partial | OAuth works; MCP Apps support depends on version. |
| Generic MCP runtime | No | No | Static Authorization: Bearer …; no inline render — resolve the resourceUri resource yourself. |
If your host doesn’t yet implement MCP Apps, the underlying render still works — you just lose inline rendering. Each render is delivered as an MCP-Apps resource (ui://ggui/render/<id> on _meta.ui.resourceUri); resolve it with resources/read. There is no render-viewer URL the agent receives.
Reference
Section titled “Reference”- MCP Apps protocol: https://modelcontextprotocol.io/extensions/apps/overview
- ggui server factory:
@ggui-ai/mcp-server - Iframe runtime:
@ggui-ai/iframe-runtime - Wire envelopes: Envelopes
- Glossary terms: gadget, tool, blueprint