Ship one MCP tool whose ui:// view renders in at least 2 hosts, as a single-file bundle under 300 KB gzipped, first paint under 500 ms after the tool result, with every data change going through a server tool.
Recommended stack
- @modelcontextprotocol/ext-apps 2.x - registerAppTool/registerAppResource on the server, App class in the view. Official SDK for the MCP Apps extension (spec 2026-01-26, stable), supported by ChatGPT, Claude, VS Code, Goose, and others.
- @modelcontextprotocol/server 2.x - MCP server. ext-apps 2.x is built on the v2 SDK line.
- Vite 8 + vite-plugin-singlefile 2.x - Bundles the view into one HTML file. Hosts load the view in a sandboxed iframe, so everything must be inline.
- ext-apps basic-host example - Local host for testing. Renders the view at localhost:8080 without connecting a real chat client.
Build steps
- Pick a tool whose result is easier to see than read: a chart, a filterable table, a map, or a form to confirm parameters before a write.
- Server: registerAppTool with
_meta: { ui: { resourceUri: "ui://<tool>/app.html" } }, and registerAppResource returning dist/app.html with RESOURCE_MIME_TYPE. Also return a short text summary for hosts without UI support. - View:
new App({ name, version }), setapp.ontoolresultbeforeapp.connect(), render the data, and wire buttons toapp.callServerTool({ name, arguments }). - Bundle with Vite and viteSingleFile; no remote scripts, fonts, or images.
- Match the host theme (useHostStyles in React, or CSS variables) and test light and dark.
- Serve the MCP server over Streamable HTTP (e.g. http://localhost:3001/mcp), then
git clone https://github.com/modelcontextprotocol/ext-apps && cd ext-apps/examples/basic-host && npm install && npm start, open http://localhost:8080, call the tool and click through every action. - Connect to 2 real hosts and confirm the view and the text fallback both behave.
Watch out for
- Setting handlers after connect() misses the initial tool result, leaving a blank view.
- The sandboxed iframe blocks remote scripts, fonts and images unless the resource declares their origins in _meta.ui.csp (resourceDomains / connectDomains); inline everything by default.
- Mutations done only in the view are invisible to the model and skip auth checks. Route every change through a server tool.
- No text fallback means hosts without MCP Apps support show nothing useful.
Definition of done
- basic-host renders the view and a button refreshes data through a server tool.
- The view renders in 2 real hosts.
gzip -c dist/app.html | wc -cis under 300,000.- A host without UI support still returns a useful text answer.