Building an MCP App: declarations, sandbox and text fallbacks Under the 2026-07-28 Model Context Protocol specification, an MCP App is built from three declarations — a ui:// HTML resource served as text/html;profile=mcp-app, a tool whose _meta.ui.resourceUri points at that resource, and the domains the view may reach — with the host enforcing the sandbox, according to a builder's account published in October 2026. The author reports that web hosts run the view in two iframes on a different origin from the host under a Content Security Policy that can be tightened but never loosened, and that tools MUST return meaningful content even when a UI is available because some hosts will never draw the view. The advice is to ship one self-contained HTML file, have the view call the server's tools rather than its APIs, and test every tool in a text-only host. Building an MCP App: declarations, sandbox and text fallbacks An MCP App is an interactive view that a Model Context Protocol MCP server puts into the conversation. Building one turned out to be mostly declarations: a page, a tool that points at it, and the few domains the page may reach. The host does the enforcing. What I’d hold onto is the plain part: a tool with a view still has to answer in text, because some hosts will never draw it. This is part 25 of my series on what an MCP server does under the 2026-07-28 specification. Part 24 https://pournasserian.com/writing/mcp-2026-apps showed how an app’s user interface UI reaches the screen, and this one is the builder’s side. The facts are as I read them in October 2026. In brief 1. The server declares and the host enforces. It declares a ui:// HyperText Markup Language HTML resource served as text/html;profile=mcp-app , a tool whose meta.ui.resourceUri points at it, and the domains its view may reach. 2. visibility decides who may call a tool, so a tool marked "app" serves the view’s buttons without ever reaching the model’s tool list. 3. Tools MUST return meaningful content even when a UI is available, and servers SHOULD give text-only hosts a fallback. 4. Web hosts run the view in two iframes, on a different origin from the host, under a Content Security Policy CSP that can be tightened but never loosened. 5. My advice: ship one self-contained HTML file, have the view call your tools rather than your application programming interfaces APIs , and test every tool in a text-only host. What the server declares The UI resource An MCP App starts with a resource https://pournasserian.com/writing/mcp-2026-resources whose Uniform Resource Identifier URI names a page. The MCP Apps specification https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/2026-01-26/apps.mdx sets four rules for it: | Rule | Value | |---|---| | URI | MUST use the ui:// scheme, which is reserved for MCP Apps | | Media type | MUST be text/html;profile=mcp-app | | Content | text a string or blob base64 , and it MUST be a valid HTML5 document | | Listing | Servers MAY leave UI-only resources out of resources/list , since hosts find them through tool metadata | The resource’s own meta.ui says what the page needs from its sandbox: | Field | What it’s for | |---|---| | csp.connectDomains | Origins for fetch, XMLHttpRequest and WebSocket connect-src | | csp.resourceDomains | Origins for scripts, styles, images, fonts and media; wildcard subdomains are allowed | | csp.frameDomains | Origins for nested iframes frame-src , 'none' if omitted | | csp.baseUriDomains | Allowed base URIs base-uri , 'self' if omitted | | permissions | Requests camera , microphone , geolocation or clipboardWrite ; hosts MAY honour them, so use feature detection | | domain | Requests a dedicated sandbox origin, useful for OAuth callbacks, cross-origin resource sharing CORS and API-key allowlists; the format depends on the host | | prefersBorder | Asks for true or against false a visible border and background; set it explicitly, since defaults vary | The tool link A tool points at its page with meta.ui.resourceUri , and meta.ui.visibility says who may call it: { "name": "get weather", "description": "Get current weather for a location", "inputSchema": { "type": "object", "properties": { "location": { "type": "string" } } }, " meta": { "ui": { "resourceUri": "ui://weather-server/dashboard", "visibility": "model", "app" } } } | visibility | Effect | |---|---| | "model", "app" , the default | The agent sees it and can call it, and so can the app | | "app" | Hidden from the agent: hosts MUST NOT include it in the model’s tool list, but the app can call it on the same server connection. Use it for refresh buttons and form submits | | "model" | Hosts MUST reject a tools/call for it from the app | The older flat key, meta "ui/resourceUri" , is deprecated. Use meta.ui.resourceUri . Results for two audiences A UI-enabled tool’s result has two readers, as part 8 https://pournasserian.com/writing/mcp-2026-tool-definitions set out: | Field | Who it’s for | |---|---| | content | The model, and text-only hosts | | structuredContent | The view; under the MCP Apps convention it isn’t added to the model’s context | | meta | Neither: metadata such as timestamps and versions | Tools MUST return meaningful content even when a UI is available, and servers SHOULD provide text-only fallback behaviour for every UI-enabled tool. Keeping structuredContent from the model is only a convention: Claude Code and its Agent software development kit SDK https://code.claude.com/docs/en/agent-sdk/custom-tools pass it to the model in place of the text blocks of content , and ChatGPT lets the model see it. My advice, as in part 8: write each half to stand on its own. In Python The official Python SDK, mcp 2.3.0, has MCP Apps built in. First the page, declaring the one origin its script comes from: python from mcp.server import MCPServer from mcp.server.apps import Apps, ResourceCsp, client supports apps from mcp.server.mcpserver import Context from mcp.types import CallToolResult, TextContent SALES = {"north": 1200, "south": 950} UI = "ui://sales/dashboard.html" apps = Apps apps.add html resource UI, """< doctype html