Answers · MCP security
Our MCP server is adding an MCP App, an interactive HTML view the host renders in the chat. What does the host's sandbox actually protect, what can the app still do, and how do we lock it down so a bug or a hostile input in the view can't call our tools or leak data?
Last updated
TL;DR: An MCP App (the MCP Apps extension, SEP-1865, stable spec 2026-01-26) is a ui:// resource of type text/html;profile=mcp-app that a tool links through _meta.ui.resourceUri. Hosts must render it in a sandboxed iframe, talk to it only through postMessage JSON-RPC, and build its Content Security Policy from what the resource declares in _meta.ui.csp; with nothing declared, the spec's default allows no network connections (connect-src 'none'), no nested frames and no undeclared origins. That protects the host page and the user's cookies from your view. It doesn't protect your server from your view: the app can ask the host to call your tools, send messages into the chat, update the model's context and open links, and the spec only says hosts MAY ask the user first. So the lock-down is mostly yours: declare the fewest origins and permissions, mark UI-only tools with visibility ["app"] and still authorize them on the server, treat tool input and results shown in the view as untrusted, keep secrets out of the HTML (hosts may prefetch and cache it), and keep the template stable so hosts that hash or review it don't see surprises.
What should I check, step by step?
1. Declare only the origins the view needs in _meta.ui.csp: connectDomains for fetch, XHR and WebSocket, resourceDomains for scripts, styles, images and fonts. Leave frameDomains and baseUriDomains out unless you need them; omitted, the host uses frame-src 'none' and base-uri 'self'.
- Prefer bundling scripts and styles into the HTML over loading them from a CDN, so there's no third-party origin in resourceDomains that could change what runs.
- Request camera, microphone or geolocation in _meta.ui.permissions only if the view truly needs them; hosts map them onto the iframe's allow attribute and may still refuse.
- Mark tools that only the view should call with _meta.ui.visibility ["app"] so the host keeps them out of the model's tool list, and mark tools the view must never call with ["model"].
- Authorize every tools/call on the server as if it came from anyone: visibility is enforced by the host, not by your server, and a host that doesn't support MCP Apps simply treats the tool as a normal tool.
- Treat tool input and tool results the host pushes into the view as untrusted data: render them as text, never through innerHTML or eval, since they can carry content from the model or from third parties.
- Don't put API keys, tokens or user data in the ui:// HTML. Hosts may prefetch and cache the resource, and any client that can call resources/read can fetch it.
- Don't have the view send ui/message or ui/update-model-context text the user didn't write or see; content that steers the model is exactly what a reviewer will flag.
- Keep the template stable between releases and version it on purpose, and scan its text for hidden markup before publishing, because hosts may hash, review or allowlist templates.
What does the MCP Apps iframe sandbox and CSP protect?
The spec requires every view to run in a sandboxed iframe, with all communication through postMessage, so the view can't read the host page's DOM, cookies or local storage, navigate the parent page or run script in the host's context. A web host must also put the view behind a sandbox proxy on a different origin from the host, and the proxy loads your HTML with a CSP built from your declared domains and with object-src 'none'.
- Hosts must block connections to domains you didn't declare, may restrict the policy further but must not allow undeclared domains; they should warn users when a view needs external domains and should log the CSP they applied. If you need a stable origin so your API can allowlist the view for CORS, the spec's _meta.ui.domain field exists for that, and its format is up to each host.
What can an MCP App view still do inside the host, and do hosts ask the user first?
Through the host, a view can request tools/call on its own server, send a message into the conversation (ui/message), replace the context the model sees on its next turn (ui/update-model-context), ask the host to open a link (ui/open-link) and ask for a different display mode. The spec says hosts should add ui/message content to the conversation and may request user consent, should pass model-context updates to the model and may show them to the user. The project's announcement says hosts can require explicit approval for UI-initiated tool calls; it isn't a requirement.
- The spec's own threat model lists a malicious server delivering harmful HTML, a compromised view trying to escape the sandbox, unauthorized tool execution, exfiltration of host data, and phishing or social engineering, and notes that a view can still show misleading content and burn CPU and memory. A real example of the gap between host and view: one of the critical MCP Inspector advisories published on October 21, 2026 let a remote MCP App steal the Inspector proxy's token (fixed in 1.0.3).
Does MCP Apps tool visibility (app-only tools) protect your server's tools?
_meta.ui.visibility defaults to ["model", "app"]. With ["app"], the host must leave the tool out of the model's tool list, only the view from the same server connection may call it, and cross-server calls to app-only tools are always blocked. The host must reject a view's call to a tool whose visibility doesn't include "app".
- All of that happens in the host. Your server receives an ordinary tools/call and can't tell whether a person clicked a button, the model called the tool or a different client sent it. So check the caller's authorization and the arguments on every call, require a confirmation step such as elicitation for anything that writes, spends or deletes, and log each call with its origin as far as you know it.
How do I ask Rightshold about this (free, no key)?
Add the MCP server https://www.rightshold.com/mcp to Claude, Cursor, ChatGPT, Grok or VS Code, then ask: “Before we publish our MCP App, scan the text of our ui:// HTML template and the linked tool's description for content a reviewer can't see.”
{
"tool": "scan_text",
"arguments": {
"text": "<the ui:// resource HTML and the tool description>"
}
}- Free, no key, up to 100,000 characters per call; scan a large bundle in parts. It reports exact positions of zero-width and bidirectional characters, hidden markup such as HTML comments, display:none and zero-size text, and base64 blobs (decoded one level), and labels instruction-like language as a heuristic, never a certainty.
- It doesn't run or analyze your JavaScript, check your CSP or test the host's sandbox, and a bundled app may legitimately contain comments or base64 fonts, so read each finding rather than treating it as a verdict.
Rightshold scans text and records evidence about MCP servers. It isn't an MCP host, doesn't render or sandbox MCP Apps and isn't affiliated with the MCP project or any host named here. Requirements are quoted from the MCP Apps specification (2026-01-26) and the project's documentation as checked on November 10, 2026; host behavior varies by client.
What are the sources?
- MCP Apps specification (SEP-1865, 2026-01-26)
- Model Context Protocol: MCP Apps overview
- MCP blog: MCP Apps, bringing UI capabilities to MCP clients (January 26, 2026)
- MCP Apps docs: CSP and CORS
- MCP Apps API: McpUiResourceCsp
- MDN: Content Security Policy
Related
- Which MCP Inspector version is safe to run?
- How to ask the user to confirm an action from an MCP server with elicitation
- How to check your MCP tool descriptions for hidden text before publishing
- How to keep MCP as a thin adapter and recheck authorization on every tool call
Not legal advice. JSON · Markdown · All answers
Get a free API key in one call
No card and no email needed. 100 free credits; the license and policy checks stay free without a key.
curl -s -X POST https://www.rightshold.com/v1/accounts/signup -H "Content-Type: application/json" -d '{"accept_terms":true}'
export RIGHTSHOLD_API_KEY=rs_... # the api_key from the reply (shown once)
curl -s https://www.rightshold.com/v1/licenses/check -H "Authorization: Bearer $RIGHTSHOLD_API_KEY" \
-H "Content-Type: application/json" -d '{"license":"MIT","intended_use":{"commercial_use":true}}'
