← Field log

MCP Apps are here. Your tools can ship UI into Claude and ChatGPT.

MCP tools used to return text. Now they can return full interactive UIs — dashboards, forms, charts, buttons — that render directly in the conversation. Claude, ChatGPT, VS Code, and Copilot all support it.

This is MCP Apps, the first official extension to the Model Context Protocol. It shipped in January 2026 and it changes what MCP servers can be.

How it works

An MCP server declares a UI resource using the ui:// scheme — a self-contained HTML page. A tool links to it via metadata. When the tool is called, the host renders the HTML in a sandboxed iframe and pipes the tool result into it.

// Server: declare the UI resource
server.registerResource("ui://acme/dashboard", {
  name: "Dashboard",
  mimeType: "text/html;profile=mcp-app",
  content: dashboardHtml,
});

// Server: link a tool to the UI
server.registerTool("get_metrics", {
  description: "Fetch and visualize metrics",
  inputSchema: { type: "object", properties: { period: { type: "string" } } },
  _meta: {
    ui: { resourceUri: "ui://acme/dashboard" }
  },
  handler: async ({ period }) => {
    return await fetchMetrics(period);
  },
});

The host (Claude, ChatGPT, VS Code) fetches the resource, spins up a sandboxed iframe, and sends the tool result into it via postMessage. The iframe renders whatever it wants — a React app, a D3 chart, a form. It's just HTML.

The iframe talks back

This is the part most people miss. The iframe isn't a static display — it's a full bidirectional channel. The UI can call other tools on the server:

// Inside the iframe
import { App } from "@modelcontextprotocol/ext-apps/client";

const app = new App();

// Receive tool results from the host
app.ontoolresult = (data) => {
  renderChart(data.metrics);
};

// Call back to the server
document.getElementById("refresh").onclick = () => {
  app.callServerTool("get_metrics", { period: "7d" });
};

// Even update the conversation context
app.updateModelContext("User is looking at the 7-day metrics dashboard.");

The communication protocol is JSON-RPC 2.0 over postMessage. Every message is auditable. The iframe can't escape its sandbox — no cookie access, no parent page access. But within that sandbox, it can do anything a web app can do.

What this enables

Before MCP Apps, a tool that fetched your analytics data would return a wall of JSON. The LLM would summarize it into text. You'd lose the nuance — the trend lines, the outliers, the ability to drill down.

Now the tool returns the data AND ships a dashboard. You see the actual chart. You click a date range. You hover over a data point. The LLM doesn't have to describe what you can see for yourself.

Figma ships a design viewer. Asana ships a task board. Hex ships a notebook. These aren't screenshots — they're live, interactive applications embedded in your conversation.

The security model

Every MCP App declares its needs upfront:

{
  _meta: {
    ui: {
      csp: {
        connectDomains: ["api.acme.com"],   // allowed fetch targets
        resourceDomains: ["cdn.acme.com"],  // allowed asset sources
      },
      permissions: ["clipboard-write"],     // requested browser APIs
    }
  }
}

The host enforces this via Content Security Policy on the iframe. If a tool claims it only talks to api.acme.com, the iframe literally cannot make requests anywhere else. Templates are declared ahead of time so hosts can prefetch and review before rendering.

This matters because these UIs run inside your AI assistant. If the security model was weak, a malicious MCP server could inject UI that steals your conversation or phishes for credentials. The sandboxing is mandatory, not optional.

What's missing

No standard component library. Every MCP App ships its own HTML/CSS. There's no shared design language, so a Figma widget looks nothing like a Slack widget looks nothing like an Asana widget. The conversation becomes a patchwork.

Google's A2UI takes the opposite approach — declarative JSON with a fixed component catalog. The client renders using its own design system, so everything looks consistent. But you lose the expressiveness of full HTML.

The right answer is probably both: a standard component set for simple cases (cards, tables, metrics) and full HTML escape hatch for rich cases (3D viewers, code editors, maps). Nobody has built this bridge yet.

No offline or native rendering. MCP Apps are web-only. A mobile AI client needs a webview to render them. On iOS that's fine (WKWebView), but it's a heavier lift than rendering a native card from structured JSON. Tool visibility scoping is limited. Tools can declare visibility: ["model"] (LLM can call it) or visibility: ["app"] (only the UI can call it). But there's no "app can call it only after user clicks a button" — the trust model between the iframe and the host is coarse.

What we're building on this

At Daslab we've been running an MCP gateway that exposes workspace tools — GitHub, Slack, Postgres, custom integrations — to any MCP client. Every workspace is a scene with its own credentials, tools, and context. Connect Claude Code to your scene and it can use your integrations without ever seeing an API key.

MCP Apps changes what we can send back. A Postgres query doesn't have to be a JSON blob — it can be a table with sorting and filtering. A GitHub PR doesn't have to be a text summary — it can be a diff viewer. A Stripe dashboard can be a real dashboard.

We're working on a widget SDK that lets developers define asset views once and render them everywhere — as MCP Apps in Claude, as native views in our iOS and desktop apps, and as A2UI JSON for clients that support it. One definition, every surface.

More on that soon. If you want to try connecting Claude Code to a Daslab workspace today, check out daslab.run.


The MCP Apps specification is open source. The ext-apps SDK is on npm. The MCP 2026-07-28 release candidate formalizes Apps as an official extension.