← Alle Playbooks
Playbook· build

Build an MCP App: interactive UI in your MCP tool instead of a wall of text

How to give your MCP server a dashboard, a form or a map that renders right inside the chat. With the official ext-apps package, step by step.

Your MCP tool returns data, and the user wants to sort it, filter it, click through it. With plain text that means every interaction is a new prompt. "Show me only the ones from last week." "Sort by revenue." "What is in row 47?" That works, but it is sluggish. Since January 2026 there are MCP Apps for this, the first official MCP extension. Your server can now return a real UI that renders directly in the conversation in a sandboxed iframe. Claude supports it, on the web and on the desktop. In this playbook you build that into your existing MCP server.

Step 1, grasp what MCP Apps are good for and what not

MCP Apps close the gap between what your tool can do and what the user sees. Four scenarios from the official docs that tell you whether you need it: a sales dashboard for filtering by region, a configuration wizard with dependent fields (pick "production" and security options appear), a document review where the user clicks clauses and the model sees the decision in real time, or live monitoring that updates without the tool running again.

If on the other hand your tool only spits out one value or one sentence, leave it at text. UI only pays off once the user explores, manipulates or makes a selection. Do not build a UI because it looks slick. Build it because a text back-and-forth would otherwise take five prompts.

Step 2, check whether your client can do it

MCP Apps need a host that supports the extension. As of launch there are four: Claude (web and desktop, both available immediately), Goose (the reference implementation, available immediately), Visual Studio Code (in Insiders, not in stable), and ChatGPT. The advantage of the open standard: you write the UI once and it runs in all supported clients, without a line of client-specific code.

If your target audience sits on a client that is not there yet, keep the server working without the UI too. A tool that declares a UI resource should still return a sensible text result in case the host does not know the extension.

Step 3, install ext-apps

You need an existing MCP server. If you do not have one yet, build that first (see the playbook below). Then you install the official package:

npm install @modelcontextprotocol/ext-apps

The current version is 1.7.3. The package provides the App class for communication between your UI and the host. On the server side you work with the MCP primitives you already know, tools and resources. The only new thing is that a resource can now deliver HTML and JavaScript instead of just data.

Step 4, create a UI resource under ui://

MCP Apps stand on two pillars. The first is a UI resource: a server-side resource delivered via the ui:// scheme that contains bundled HTML plus JavaScript. That is your actual interface, a small web app.

So create a resource whose URI starts with ui://, for example ui://charts/interactive. The content is a bundled HTML document. Keep the bundle small and self-contained, because the host loads it and renders it in an isolated iframe. No access to the rest of the page, only what you ship with it.

Step 5, link the tool to the resource

The second pillar is a tool with UI metadata. You attach a _meta.ui.resourceUri field to your tool that points at the UI resource from step 4:

{
  name: "visualize_data",
  description: "Visualize data as an interactive chart",
  inputSchema: { /* ... */ },
  _meta: {
    ui: {
      resourceUri: "ui://charts/interactive"
    }
  }
}

If the model now calls visualize_data, the host sees the UI metadata, fetches the resource and renders it in the iframe instead of simply printing text. Make sure resourceUri matches your resource's URI exactly, a typo here is the most common reason nothing renders.

Step 6, initialise the app in the iframe

Now for the UI side. In your bundled HTML you start the App class and connect to the host:

import { App } from "@modelcontextprotocol/ext-apps";

const app = new App();
await app.connect();

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

app.connect() establishes the connection to the host. The ontoolresult handler fires when the tool delivers a result, and you render your interface with it. All communication runs over JSON-RPC via postMessage, so you are not tied to any framework. React, Vue, vanilla JS, all fine as long as it runs in the iframe.

Step 7, call back from the UI to the server

The nice part is the two-way street. From your UI you can call server tools without the user typing a new prompt:

const response = await app.callServerTool({
  name: "fetch_details",
  arguments: { id: "123" },
});

If the user clicks row 47 in your dashboard, your UI calls fetch_details with the ID and shows the details inline. No more "what is in row 47". That is exactly what makes the difference between a text answer and a real app.

Step 8, keep the model in the loop

A UI the model does not notice is only worth half. With updateModelContext you quietly push the model the information about what the user just did:

await app.updateModelContext({
  content: [{ type: "text", text: "User selected option B" }],
});

That keeps the model up to date so it can react to the selection. Apps run inside the client, which is why they can do more than a bare iframe: log events for debugging, open links in the user's browser, send follow-up messages that move the conversation on, or quietly update the model context. Use that sparingly. Every updateModelContext costs tokens and too much noise confuses the model.

Step 9, understand the security model

You are running code from an MCP server in your host, so code you did not write. MCP Apps catch that on four levels, and as a server author you should know what you are working against. First iframe sandboxing: all UI runs in sandboxed iframes with restricted rights. Second pre-declared templates: hosts can inspect the HTML before they render it. Third auditable messages: all UI-to-host communication runs over loggable JSON-RPC. Fourth user consent: hosts can require explicit approval before a UI-initiated tool call goes through.

For you that means: build so your app also works when the user has to dismiss a consent dialog, and hide nothing in the communication, because everything is logged. And keep telling your users to inspect MCP servers thoroughly before connecting them, a UI does not make a dodgy server trustworthy.

Step 10, start from an example instead of from zero

Do not write your first app on a greenfield. The ext-apps repository has working examples that cover almost every standard case: threejs-server for 3D visualisation, map-server for interactive maps, pdf-server for displaying documents, system-monitor-server for real-time dashboards and sheet-music-server for musical notation. Pick the one closest to your use case and build from there.

For testing, the MCPJam tutorials are worth a look, they walk through a real app example with code. And if you are building for Claude.ai and run into implementation problems, there is a dedicated repo for bug reports at github.com/anthropics/claude-ai-mcp. Start small, a single tool with a single UI resource, and only extend once that runs cleanly across all the clients you are targeting.

What next

If you do not have an MCP server yet, start with First MCP server in 90 minutes and come back here afterwards. Once your app is finished and you want to distribute it, continue with Publishing an MCP server. And if you want to learn the whole build topic systematically, level 6 with the lessons from Planning an MCP server is the right place.

Source

  • MCP Apps announcement (official): https://blog.modelcontextprotocol.io/posts/2026-01-26-mcp-apps
  • Package: https://www.npmjs.com/package/@modelcontextprotocol/ext-apps (version 1.7.3)
  • Examples and SDK: ext-apps repository (modelcontextprotocol)
Build an MCP App: interactive UI in your MCP tool instead of a wall of text — StudioMeyer Academy