build-mcp-app

Build MCP servers that serve interactive UI widgets rendered inline in chat.

36.8k|4.1k|Updated Nov 20, 2025
One-click install
npx skills add https://github.com/anthropics/claude-plugins-official --skill build-mcp-app-anthropics
Or copy as Structured Prompt for Agent▼
Please help me install this Agent Skill.
Skill: build-mcp-app
Source: https://github.com/anthropics/claude-plugins-official/tree/main/plugins/mcp-server-dev/skills/build-mcp-app
Command: npx skills add https://github.com/anthropics/claude-plugins-official --skill build-mcp-app-anthropics

SYSTEM DOCUMENTATION & REQUIREMENTS

💡 This Skill requires @modelcontextprotocol/sdk, @modelcontextprotocol/ext-apps, zod, express, and includes references (resource) components.

What problem does it solve? Plain MCP tools return only text or JSON, which falls short when a user must pick from a large list, confirm a destructive action, or view spatial data like charts and maps. This Skill guides you through adding interactive UI widgets to an MCP server so tools can render forms, pickers, dashboards, and confirmation dialogs directly inside the conversation in Claude and other compatible hosts. ## Core Features & Use Cases - Widget-enabled tools: Register tools with _meta.ui.resourceUri and serve HTML resources via registerAppResource using the @modelcontextprotocol/ext-apps package. - Bidirectional widget runtime: Use the App class inside the sandboxed iframe to receive tool results, send messages, call server tools, open links, and adapt to host theme and display mode. - Production guidance: Reference docs cover iframe CSP sandbox constraints, payload size budgeting, abuse protection with rate limiting, and the connector-directory submission checklist. - Use Case: Build a contact picker tool where Claude calls pick_contact, the host renders an interactive searchable list in an iframe, and the user's selection is injected back into the conversation as a message. ## Quick Start Ask the AI to use the build-mcp-app skill to add an interactive picker widget to your MCP server tool.

Frequently Asked Questions about build-mcp-app

High-intent search queries and answers about installing and using this skill.

FAQPage Schema
How do I add an interactive UI widget to an MCP server tool?▼

Register the tool with registerAppTool and declare a UI resource via _meta.ui.resourceUri, then register that resource separately with registerAppResource serving HTML under the text/html;profile=mcp-app MIME type. The tool handler returns plain JSON, which the host pipes into the iframe widget via the ontoolresult event.

When should I use a widget instead of elicitation in MCP?▼

Use elicitation for simple confirmations, short enum picks, and flat forms since it is spec-native with zero UI code. Use a widget for large searchable lists, visual previews, charts or maps, and live-updating progress, which elicitation cannot handle.

Why does my MCP app widget render as a blank rectangle?▼

Blank widgets are almost always CSP violations, most commonly importing ext-apps from a CDN like esm.sh, whose transitive fetches the iframe blocks. Inline the ext-apps/app-with-deps bundle into the HTML at server startup and check the iframe's own devtools console for errors.

Can MCP app widgets open external links or load remote images?▼

The iframe sandbox blocks window.open and target=_blank anchors, so outbound navigation must go through app.openLink. Remote images are blocked by CSP img-src, so fetch them server-side and inline them as data: URLs in the tool result payload.

What are the requirements for submitting an MCP app to the Claude connector directory?▼

The server must use OAuth or authless authentication since static bearer tokens block listing, and every tool needs annotations like title and readOnlyHint. Widgets must stay under 500px inline height, support both themes, and you must supply 3-5 PNG screenshots of the app response.

Why does my widget show a JSON parse error on large tool results?▼

Hosts cap tool-result text at roughly 150,000 characters and substitute a file-pointer string when exceeded, so JSON.parse fails in ontoolresult. Cap payloads around 130KB by pruning unreferenced columns, truncating rows, and moving heavy assets to a separate tool the widget calls via callServerTool.