StackOne

Build Your First MCP App

Interactive UIs Inside AI Chat Windows

MCP Apps Tutorial

MCP servers return text.

Great for fetching data and answering questions.

But what happens when users need to interact with something?

MCP Apps

The first official extension to MCP

Servers can now return interactive HTML UIs
that render directly inside the chat window.

Announced January 26, 2026

Four Key Concepts

ui:// Resources

New URI scheme for UI

Tool-UI Linkage

_meta.ui.resourceUri

Sandboxed Iframes

Isolated execution

Bidirectional Comms

App ↔ Host ↔ Server

How It Works

User asks
→
LLM picks tool
→
Host fetches UI
→
Renders in iframe

The UI can call server tools directly, bypassing the LLM for fast iteration.

Why not just build a web app?

What We're Building

API Explorer

An interactive HTTP client that renders inside the chat.
Ask the agent to test an endpoint, get a full request builder.

Project Structure

mcp-app/
├── server.ts          # MCP server + tools
├── mcp-app.html       # HTML entry point
├── src/
│   └── mcp-app.ts     # UI logic
├── vite.config.ts     # Bundles to single file
└── package.json

Step 1: Define the Resource URI

const resourceUri = "ui://api-explorer/mcp-app.html";

The ui:// scheme tells hosts this is a renderable UI.

Step 2: Register Tool with UI

registerAppTool(server, "send-request", {
  title: "Send API Request",
  description: "Send an HTTP request...",
  inputSchema: requestInputSchema,
  _meta: { ui: { resourceUri } },
}, handleRequest);

_meta.ui.resourceUri links the tool to the UI.

Step 3: App-Only Tool

registerAppTool(server, "inspect-response", {
  ...
  _meta: {
    ui: {
      resourceUri,
      visibility: ["app"]
    }
  },
}, handleRequest);

Hidden from LLM. Only the UI can call it.

Step 4: Connect to Host

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

const app = new App({
  name: "API Explorer",
  version: "1.0.0"
});

app.connect();

Two lines to establish the postMessage channel.

Step 5: Receive Tool Results

app.ontoolresult = (params) => {
  const data = JSON.parse(params.content[0].text);
  displayResponse(data);
  prefillForm(data);
};

Host pushes tool results to your app automatically.

Step 6: Call Tools from UI

const result = await app.callServerTool({
  name: "inspect-response",
  arguments: { method, url, headers, body },
});

Direct UI → Host → Server → Host → UI. No LLM in the loop.

Build & Run

npm run build   # Vite bundles to single HTML
npm run serve   # Start server on :3001

Test with basic-host or add to Claude as a custom connector.

Where It Runs

What's Next?

Extend This App

  • Auth header presets
  • Saved collections
  • OpenAPI import
  • Response diffing

Build Your Own

  • Data explorers
  • Config wizards
  • Real-time dashboards
  • Media viewers

Resources

🚀

Go Build Something

MCP Apps are ready. Your turn.