Visual UIs Are Now Possible in MCP Servers

MCP servers can now render interactive UIs directly in Claude Desktop’s chat window. Not just text responses—actual HTML with JavaScript, maps, charts, anything.

ISS Tracker demo

What Changed

The @modelcontextprotocol/ext-apps library lets MCP tools return visual UIs. When you call a tool, instead of just getting text back, you get an interactive iframe rendered inline in the conversation.

This means your AI assistant can show you things, not just tell you about them.

Resources:

How It Works

The architecture has two parts: a server that fetches data and declares the UI, and a client-side app that renders it.

Server Side

Register a tool with UI metadata pointing to an HTML resource:

import { registerAppTool, registerAppResource } from "@modelcontextprotocol/ext-apps/server";

const resourceUri = "ui://iss-tracker/mcp-app.html";

// Register the UI resource (bundled HTML)
registerAppResource(server, resourceUri, "text/html", () => APP_HTML);

// Register the tool with UI metadata
registerAppTool(server, "where_is_iss", {
  description: "Show ISS location on a live map",
  uiResourceUri: resourceUri,
  csp: {
    connectDomains: ["https://*.openstreetmap.org", "https://unpkg.com"],
    resourceDomains: ["https://*.openstreetmap.org", "https://unpkg.com"],
  },
  execute: async () => {
    const [iss, path, geo] = await Promise.all([
      fetch("https://api.wheretheiss.at/v1/satellites/25544").then(r => r.json()),
      fetch(`https://api.wheretheiss.at/v1/satellites/25544/positions?timestamps=${timestamps}`).then(r => r.json()),
      fetch("http://ip-api.com/json/").then(r => r.json()),
    ]);
    return { iss, path, user: { latitude: geo.lat, longitude: geo.lon, city: geo.city } };
  },
});

The csp field is important—it declares which external domains your UI needs to access. Without this, Leaflet tiles and scripts would be blocked.

Client Side

The UI receives tool results and renders them:

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

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

app.ontoolresult = (result) => {
  const data = result.structuredContent;
  // Update your UI with the data
  updateMap(data.iss, data.user);
};

app.connect();

Key Gotcha: Dynamic Script Loading

Static

Previous Post

Let’s Read Continuous Discovery Habits Together (February 2026)

Next Post

MasterControl AI-Powered SOP Analyzer

Related Posts