| name | build-mcp-app |
| description | This skill should be used when the user wants an MCP App, interactive UI resource, inline widget, picker, dashboard, chart, form, or iframe-based MCP user interface. Use after the server's deployment and protocol era are known. |
| version | 0.2.0 |
Build an MCP App
Provenance: Forked from anthropics/claude-plugins-official/plugins/mcp-server-dev under Apache-2.0. Significant changes: reorganized around the 2026-07-28 core/App transport split and verified C#/Python App servers. See ../THIRD-PARTY-NOTICES.md.
An MCP App is a UI resource (ui://…, text/html;profile=mcp-app) that a tool associates through _meta.ui.resourceUri. The server returns structured data; the host fetches the resource and renders it in a sandboxed iframe.
The unavoidable era split
The browser bridge and the MCP transport are independent:
- A modern server can use C#
ModelContextProtocol 2.0.0 or Python mcp 2.0.0, including stateless HTTP, server/discover, and per-request _meta.
- The browser bundle remains
@modelcontextprotocol/ext-apps@1.7.5. Its peer dependency is legacy @modelcontextprotocol/sdk@^1.29.0; do not make that package the server transport for a 2026-07-28 application.
- TypeScript SDK v2 has no Apps support. Track upstream migration PRs #710, #719, and #720; do not invent a v2 Apps API.
The Apps extension identifier is io.modelcontextprotocol/ui, not …/apps.
Decide whether an iframe is warranted
Use plain text or MRTR for a confirmation, a short enum, or a flat form. Build a widget only when the user needs a searchable picker, visual preview, chart/map/diff, or a focused interactive workspace.
Keep the widget single-purpose. It cannot access host DOM, cookies, or storage; it cannot open a popup directly; and its network access is constrained by its resource CSP. Use host-mediated APIs such as callServerTool and openLink.
Modern extension negotiation
A modern Apps-capable request declares Apps in per-request _meta:
{
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {
"extensions": {
"io.modelcontextprotocol/ui": {
"mimeTypes": ["text/html;profile=mcp-app"]
}
}
}
}
}
A server that supports Apps includes the same extension ID in server/discover result capabilities:
{ "extensions": { "io.modelcontextprotocol/ui": {} } }
A UI-enhanced tool must still have useful text behavior for a client that does not declare Apps.
Server-side Apps support
C# ModelContextProtocol 2.0.0
Apps APIs are experimental and emit MCPEXP003. Enable the extension after tool registration:
builder.Services.AddMcpServer()
.WithTools<ContactTools>()
.WithMcpApps();
[McpServerTool]
[McpAppUi(ResourceUri = "ui://contacts/picker.html")]
public static string PickContact(string? filter = null) => "Use the text fallback when UI is unavailable.";
[McpMeta("ui", JsonValue = """{\"resourceUri\":\"ui://contacts/picker.html\"}""")] is the raw-metadata alternative when an attribute-specific shape is insufficient. Do not apply both for the same ui key. Register the matching HTML resource with McpApps.HtmlMimeType.
Python mcp 2.0.0
from mcp.server.apps import Apps
from mcp.server.mcpserver import MCPServer
apps = Apps()
@apps.tool(resource_uri="ui://contacts/picker.html", description="Pick a contact visually or return text.")
def pick_contact(filter: str | None = None) -> str:
return "Contact picker data goes here."
apps.add_html_resource("ui://contacts/picker.html", picker_html)
mcp = MCPServer("contacts", extensions=[apps])
Apps stamps tool _meta.ui, registers the UI resource, and advertises io.modelcontextprotocol/ui. It is an extension of the modern server—not the legacy ext-apps/server transport helpers.
Browser bundle
Build and inline @modelcontextprotocol/ext-apps@1.7.5's app-with-deps bundle into the resource HTML. Do not import it from a CDN: the sandbox CSP blocks transitive fetches. The bundle's postMessage dialect is separate from the core transport, which is why it can serve a browser UI from a C#/Python modern server.
Inside the iframe, connect only after registering handlers:
<script type="module">
const { App } = globalThis.ExtApps;
const app = new App({ name: "ContactPicker", version: "1.0.0" }, {});
app.ontoolresult = ({ content }) => render(JSON.parse(content[0].text));
await app.connect();
</script>
Use app.sendMessage() for an explicit user decision, app.updateModelContext() for non-chat state, app.callServerTool() for server work, and app.openLink() for outbound navigation. See references/apps-sdk-messages.md for the full bridge surface.
Resource and tool metadata
| Location | Metadata | Purpose |
|---|
| Tool | _meta.ui.resourceUri | Binds a tool to its ui:// HTML resource |
| Tool | _meta.ui.visibility | Limits a helper tool to model and/or app use |
| Resource | _meta.ui.csp | Allows exact connection, image, frame, and base-uri origins |
| Resource | _meta.ui.prefersBorder | Requests host border treatment |
Treat all widget input and tool output as untrusted. Do not render user-controlled strings through innerHTML; use DOM APIs or safe escaping.
Testing
Test three seams separately:
- Modern server:
server/discover, per-request _meta, and the tool/resource calls work through the selected C#/Python SDK.
- Browser bundle: the inline bundle connects, receives tool results, observes host theme/resize changes, and cannot escape CSP.
- Host: add the server to a real Apps-capable host and exercise the actual iframe sandbox.
A modern raw probe needs MCP-Protocol-Version, matching _meta, and extension capability—not an initialize message:
{"jsonrpc":"2.0","id":1,"method":"server/discover","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{"extensions":{"io.modelcontextprotocol/ui":{"mimeTypes":["text/html;profile=mcp-app"]}}}}}}
References
references/apps-sdk-messages.md — iframe-to-host bridge
references/iframe-sandbox.md — CSP, bundle inlining, and sandbox limits
references/widget-templates.md — focused UI patterns
references/payload-budgeting.md — result-size discipline
references/abuse-protection.md — untrusted content and action safety
references/directory-checklist.md — connector-directory submission