From bopen-tools
Builds MCP tools with interactive HTML interfaces using the official MCP Apps extension. Covers capability negotiation, CSP, app-only tools, and progressive text fallbacks.
How this skill is triggered — by the user, by Claude, or both
Slash command
/bopen-tools:mcp-appsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Build MCP tools that progressively enhance into sandboxed interactive interfaces. Keep every tool useful without UI support.
Build MCP tools that progressively enhance into sandboxed interactive interfaces. Keep every tool useful without UI support.
Check the live packages and clone the matching official examples before implementation:
bun pm view @modelcontextprotocol/ext-apps version
bun pm view @modelcontextprotocol/sdk version
git clone --branch "v$(bun pm view @modelcontextprotocol/ext-apps version)" --depth 1 \
https://github.com/modelcontextprotocol/ext-apps.git /tmp/mcp-ext-apps
The verified July 2026 baseline is Ext Apps 1.7.4 and MCP SDK 1.29.0. Treat those as a tested snapshot, not an evergreen instruction. Use Node 20+ semantics or a compatible Bun runtime.
Prefer the installed official skills:
create-mcp-app for a new server and View.add-app-to-server for an existing MCP server.convert-web-app for a standalone/MCP hybrid.Use this skill for cross-cutting architecture, security, and generative UI integration.
Pair two static server registrations:
ui:// resource through nested _meta.ui.resourceUri.text/html;profile=mcp-app.Pass dynamic data in tool results. Do not generate resource URIs per user, ticket, or request.
const resourceUri = "ui://visual-wayfinder/decision.html";
registerAppTool(server, "open-decision", {
description: "Open the current decision with an optional interactive view.",
inputSchema,
_meta: { ui: { resourceUri } },
}, async (args) => ({
content: [{
type: "text",
text: summarizeDecision(await loadDecision(args)),
}],
structuredContent: {
decision: await loadDecision(args),
},
}));
registerAppResource(server, {
uri: resourceUri,
name: "Decision workbench",
mimeType: RESOURCE_MIME_TYPE,
}, async () => ({
contents: [{
uri: resourceUri,
mimeType: RESOURCE_MIME_TYPE,
text: await Bun.file("dist/decision.html").text(),
_meta: {
ui: {
csp: {
connectDomains: [],
resourceDomains: [],
},
},
},
}],
}));
Follow the current SDK signatures from the checked-out release. The exact resource registration overload and CSP field names are versioned APIs.
Always return both:
content: concise, complete text for the model and non-UI hosts.structuredContent: validated data intended for the View.Keep both payloads bounded. Do not put secrets, credentials, unnecessary PII, or privileged tracker state into either channel. Validate structuredContent again in the View before rendering.
Negotiate io.modelcontextprotocol/ui support. Use the canonical client matrix rather than a remembered host list. When support is absent, continue through the normal text interaction or offer a standalone local/browser interface.
Use:
visibility: ["app"] for safe UI-only actions such as submitting a draft, refreshing a read-only projection, pagination, or filtering.visibility: ["model"] for privileged filesystem, tracker, shell, or administrative operations.Treat the View as untrusted input. Validate authorization, ownership, current revision/nonces, option IDs, numeric bounds, and domain invariants server-side. A UI submission must not bypass the normal domain workflow.
Register all handlers before connecting:
const app = new App({ name: "Decision workbench", version: "1.0.0" });
app.ontoolinput = (input) => renderPending(input);
app.ontoolinputpartial = (input) => renderPartial(input);
app.ontoolresult = (result) => renderResult(result);
app.onhostcontextchanged = (context) => applyHostContext(context);
app.onteardown = async () => {
await flushSmallPendingDraft();
return {};
};
await app.connect(new PostMessageTransport());
Apply host theme, fonts, safe-area insets, container dimensions, and available display modes with fallbacks. Pause expensive animation or polling when the View is not visible.
Use viewUUID when per-view persistence or state recovery requires a stable instance identifier. Do not treat it as universally required for every tool result.
Bundle JavaScript and CSS so browser imports resolve inside the sandbox. A single self-contained HTML file through Vite and vite-plugin-singlefile is the most portable default.
Single-file output is not itself a protocol requirement. Multiple assets can work when the resource metadata declares exact origins and the target host supports them. Prefer single-file delivery unless bundle size or asset reuse provides a concrete reason not to.
Use Bun for local project operations unless the owning project specifies another package manager:
bun add @modelcontextprotocol/ext-apps @modelcontextprotocol/sdk zod
bun add -d typescript vite vite-plugin-singlefile
bun run build
Declare CSP on the resource content returned to the host, following the installed SDK's current types. Start deny-by-default and add exact origins only:
connectDomains for API or socket destinations.resourceDomains for scripts, styles, fonts, images, audio, or video.frameDomains only for required nested frames.Never use wildcard or scheme-wide allowlists such as * or https:. Prefer server-side tools over direct View networking. Declare only required device permissions and handle denial.
Audit every View-initiated tool call. Keep submission tools narrow, idempotent where practical, and protected against stale views.
For JSON Render, invoke json-render-core, json-render-react, and the relevant catalog skill. Add json-render-directives for deterministic formatting/calculation and json-render-devtools during development.
Pin a coherent stable JSON Render release. Use its flat root/elements React spec and validate before rendering.
Treat @json-render/[email protected] as scaffolding. Its helper currently serializes specs through text and uses broad CSP defaults. Wrap or replace its result/resource boundary when production requirements call for structuredContent, exact CSP, or app-only submission tools.
Test all of the following:
basic-host.references/patterns.md — App-only tools, state, results, and interaction patterns.references/security.md — CSP, sandbox, validation, and threat model.references/client-matrix.md — Pointer to the canonical support matrix.references/build-guide.md — Bun-first setup and verification checklist.npx claudepluginhub b-open-io/claude-plugins --plugin bopen-toolsScaffolds MCP Apps with interactive UIs using React, Vue, Svelte, or vanilla JS. Guides tool+resource registration, SDK patterns, and host integration.
Scaffolds an MCP App tool + UI resource pair for interactive, human-in-the-loop workflows. Use when a standard tool's text response is insufficient and a sandboxed HTML UI is needed.
Adds interactive UI widgets (forms, pickers, dashboards, confirm dialogs) to MCP servers for inline rendering in chat surfaces like Claude and ChatGPT.