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.
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 <script src=""> tags don't work in srcdoc iframes. You have to load external libraries dynamically:
async function loadLeaflet(): Promise<void> {
if (typeof L !== "undefined") return;
// Load CSS
const cssLink = document.createElement("link");
cssLink.rel = "stylesheet";
cssLink.href = "https://unpkg.com/leaflet@1.9.4/dist/leaflet.css";
document.head.appendChild(cssLink);
// Load JS
return new Promise((resolve, reject) => {
const script = document.createElement("script");
script.src = "https://unpkg.com/leaflet@1.9.4/dist/leaflet.js";
script.onload = () => resolve();
script.onerror = () => reject(new Error("Failed to load Leaflet"));
document.head.appendChild(script);
});
}
This caught me off guard—took a while to figure out why Leaflet wasn't loading.
Try It Yourself
- Clone:
git clone https://github.com/JasonMakes801/iss-tracker-mcp - Build:
bun install && bun run build - Add to Claude Desktop config (
~/Library/Application Support/Claude/claude_desktop_config.json):
{
"mcpServers": {
"iss-tracker": {
"command": "/path/to/bun",
"args": ["/path/to/iss-tracker/dist/index.js", "--stdio"]
}
}
}
- Restart Claude Desktop
- Ask: "Where is the ISS?"
What's Next
Maps are just the start. Dashboards, charts, forms, data visualizations—anything you can build in HTML can now live inside your AI conversation.
What would you build with this?

Top comments (0)