---
title: MCP Apps support
description: How ggui implements the io.modelcontextprotocol/ui capability so hosts like Claude Desktop, claude.ai, Goose, and VS Code Copilot render generative UIs inline.
---

[MCP Apps](https://modelcontextprotocol.io/extensions/apps/overview) 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`](/oss-quickstart/) 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](/clients/claude-desktop/). For the underlying transport, see [WebSocket protocol](/api/websocket-protocol/).

## What MCP Apps adds

Without MCP Apps, an MCP tool that produces UI has to choose between:

1. Returning structured data and hoping the host formats it (no interactivity), or
2. 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

When the server boots with `mcpApps` enabled, three things happen:

1. **`io.modelcontextprotocol/ui` is advertised** in the server's `initialize` capabilities (under `experimental`). MCP-Apps-aware hosts read this and switch on inline rendering.
2. **`ui://ggui/render` is served** as a resource via `resources/read` — a minimal HTML shell that loads [`@ggui-ai/iframe-runtime`](https://www.npmjs.com/package/@ggui-ai/iframe-runtime) and opens the WebSocket channel.
3. **Every `ggui_render` tool result carries the `_meta["ai.ggui/render"]` slice** — `sessionId`, `appId`, `runtimeUrl`, `wsUrl`, a short-TTL `wsToken`, and `expiresAt` (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`), a `pollingUrl` fallback for WS-blocked environments, and `lastSequence` to seed replay cursors. The iframe consumes the slice, opens the WebSocket, and trades the `wsToken` for a longer-lived `sessionToken` for reconnects.

## Capability declaration

On `initialize`, ggui returns:

```json
{
  "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

Every UI-producing tool (today: `ggui_render`) declares a meta-resource on the result so the host knows where to load the UI from:

```json
{
  "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`

Reading the resource returns a small HTML document — paper-themed, full-bleed, no chrome — whose only job is:

1. Receive the `ai.ggui/render` slice from the host (via `postMessage`).
2. Dynamically load `runtimeUrl` (the iframe-runtime bundle).
3. 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`](https://www.npmjs.com/package/@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

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](/protocol/bootstrap-handshake/).

## Self-hosted: enabling MCP Apps in your own server

```typescript
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 ship `ws://localhost:…` to internet-accessible servers — clients must be able to reach it.
- **`runtime: true`** (default when `mcpApps` is on) — mounts the iframe-runtime bundle at `/_ggui/iframe-runtime.js`. Pass `runtime: { 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

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](/clients/claude-desktop/))                                  |
| 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

- MCP Apps protocol: <https://modelcontextprotocol.io/extensions/apps/overview>
- ggui server factory: [`@ggui-ai/mcp-server`](https://www.npmjs.com/package/@ggui-ai/mcp-server)
- Iframe runtime: [`@ggui-ai/iframe-runtime`](https://www.npmjs.com/package/@ggui-ai/iframe-runtime)
- Wire envelopes: [Envelopes](/protocol/envelopes/)
- Glossary terms: [gadget, tool, blueprint](/glossary/)