2026-01-26, through @modelcontextprotocol/ext-apps) and keeps a text path for every other host.
The app template
render_widget is registered with _meta.ui.resourceUri pointing at ui://widgentic/app.html. That resource is served with mime type text/html;profile=mcp-app and is a self-contained document: the widgentic base stylesheet plus a small inline bridge implementing the iframe side of the protocol — the ui/initialize handshake, the ui/notifications/initialized notification, ping and ui/resource-teardown responders, and ui/notifications/size-changed reports. It references no external resources, contains no imports and declares no CSP domains, so the strictest sandbox a host offers is enough.
The host mounts it in a sandboxed iframe; buildAppTemplate() from @widgentic/mcp produces the same document for anyone running their own server.
What each render sends
Every successfulrender_widget result, whatever format was requested, carries structuredContent:
treeis the render tree andhtmlits serialization: two projections of one render, never divergent.cssholds the theme declarations plus the registered styles of the kind; for agroup, every item kind’s styles exactly once.payloadis the validated payload, carrying the resolvedthemeas a top-level field when one was given.diagnosticsappears only when hint analysis found something;load(orloadsfor a group) appears only when the kind declares a load binding and the caller holds theexecutescope.
ui/notifications/tool-result and mounts the tree natively: DOM built with createElement and createTextNode, tag and attribute names held to the serializer’s allowlists, on* attributes skipped. When a later result has the same shape, the existing DOM is patched in place and the root element object survives. Sibling lists whose elements all carry a distinct key — table rows and tree nodes with record ids, template each lists with a key path — are paired by key, so a result that reorders them moves the existing elements and a branch the reader opened stays open; any other list is paired by position. The html fragment is injected into the root only when tree is absent.
Two more bridge behaviours:
- Streaming preview. On
ui/notifications/tool-input-partial, built-in kinds (and groups of built-ins) are previewed in the frame from the partial input, marked in progress and patched as more arrives. A stored widget, alone or inside a group, shows a skeleton naming the kind; when the host proxies tool calls (serverTools), the frame asks the server for a preview through the app-onlypreview_widgettool and shows the answer instead. The frame acts on a widget name only once it has fully arrived, so it never asks about a half-streamed name or mistakes one for a built-in; until then it shows an unnamed placeholder. Agents are asked to writewidgetbeforedataso previews can start early. One request is in flight at a time, the latest input goes next, and any failure keeps the skeleton. Previews never show an image’s URL: wherever the result will draw an image, a box of the same shape holds its place, and a URL still arriving is left out until it is complete. The frame never guesses a render. The tool result replaces the preview, images included. - Host context. The host’s theme is applied as
data-themeandcolor-scheme, its style variables flow into the--wg-*tokens, and safe-area insets become body padding. Unbridged tokens follow the dark preset when the host theme is dark; an explicit widgenticthemestill wins. Anchor clicks never navigate the frame: the template asks the host to openhttp(s),mailtoandtellinks throughui/open-link.
Formats
format picks which content blocks travel; the render itself does not change.
An unrecognized value returns
INVALID_TYPE at path: "format".
Hosts without Apps support
A client that does not advertise the Apps capability (extensions["io.modelcontextprotocol/ui"]) gets the same tools with the template simply unmounted: the HTML text block and payload block, or a page document to open in a browser. Claude Code is the everyday case: its tool results are text and it does not mount MCP Apps UI by design. Hosts that understand the widgentic mime type can mount the payload block natively with extractWidgetPayload and @widgentic/core; see Render in your host.
Model-context slimming
In an Apps host the person already sees the widget, so the model does not need the markup. The default-format result replaces the full-HTML text block with a one-line confirmation naming the kind, stating that the visual is already displayed and instructing the model not to restate the data. The payload block andstructuredContent are unchanged, and the Hint notes: and Action notes: tails still reach the model. The signal is the client’s UI capability: negotiated once by a 2025-era session, or carried in every 2026-07-28 request’s _meta. WIDGENTIC_ASSUME_UI=1 covers calls that reveal neither, such as stateless 2025-era HTTP. Explicit formats are never slimmed.
Script-free, network-free content
Widget content contains no scripts and no external references. That is by construction — the render tree cannot emit raw HTML, templates rejectscript, iframe, style, link and the other active-content tags, and theme values cannot contain url(.
The one external reference a widget can legitimately carry is an image source, and Apps sandboxes block external img-src while permitting data:. The server therefore inlines images on the iframe-facing surfaces — structuredContent.html, structuredContent.tree and the ui://widgentic/page/<kind> resource — turning each https source whose guarded fetch succeeds into a data: URI. The fetch refuses private, loopback, link-local and metadata addresses, requires an image/* content type, caps each image at 1 MiB and about 4 seconds, and fetches at most 24 sources per render in priority order: card heroes first, then thumbnails, avatars and template images, then tree icons. Fetched sources are admitted while the bytes they substitute fit 3 MiB per render, where a source used by 200 nodes is charged 200 times; a source that does not fit keeps its URL, and a smaller one after it may still be inlined. A failure or an overflow leaves the original URL and its alt text. The model-facing HTML text block and page output keep the original URLs. Domains declared as resourceDomains (the operator’s, then the person’s from Settings) go out as canonical origins in _meta.ui.csp.resourceDomains, and hosts that honor them load images from them directly. Sources on them rank after every undeclared source, because inlining is the only route the undeclared ones have, so a declared image is inlined only with the room left over and otherwise keeps its URL. The stderr note counts declared leftovers separately. WIDGENTIC_INLINE_IMAGES=0 disables inlining. Per-host behaviour is in the Host matrix.