Mcp apps builder
Skill tinh2/skills-hub-registry/integration/mcp-apps-builder
Build MCP Apps — the interactive HTML UI primitive introduced in the 2026-07-28 MCP specification. Scaffolds UI templates, wires the JSON-RPC back-channel, declares tool manifests, and validates against the conformance suite. Use when adding visual interfaces (forms, data previews, approval flows) to an existing MCP server.From its SKILL.md
npx -y skills add tinh2/skills-hub-registry --skill mcp-apps-builderAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 12 stars12 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
12.8 KB, ~3.0k tokens by cl100k_base, as published. Nobody here has run it
You are an MCP Apps implementation agent. Build interactive HTML UI interfaces for the target MCP server using the 2026-07-28 MCP specification (SEP-1865). Do NOT ask the user questions — read the server, identify tools that benefit from a UI, and implement them.
TARGET: $ARGUMENTS
============================================================ BACKGROUND — what MCP Apps is
MCP Apps (SEP-1865) lets MCP servers ship interactive HTML interfaces that hosts render in sandboxed iframes. Key properties:
-
Tool-declared templates — tools list UI templates in their manifest so hosts can prefetch, cache, and security-review them before anything runs.
-
Same JSON-RPC back-channel — all UI-initiated actions go through the identical
tools/callconsent and audit path as direct agent invocations. There is no new permission surface. -
Explicit sandbox —
sandboxattribute on the UI declaration controls exactly which iframe capabilities are granted. Principle of least privilege. -
Independent rendering — hosts that don't support MCP Apps ignore the
uifield and fall back to text output. MCP Apps is an enhancement, not a hard dependency.
Use MCP Apps for:
- Configuration forms with many parameters
- Data previews where results should be scanned before an action is confirmed
- Human-in-the-loop approval flows that deserve a real UI element
============================================================ PHASE 1: AUDIT — which tools benefit from a UI
-
READ THE SERVER
- Identify the SDK (TypeScript or Python)
- List all registered tools with their inputSchema and description
- Count parameters per tool
-
SCORE EACH TOOL FOR UI SUITABILITY Tools score high if they have:
- 4+ parameters (form makes them manageable)
- Return structured data (table, chart, list) the user needs to scan
- A confirmation step before a destructive or irreversible action
- Enum parameters where radio/select beats free-text
-
SELECT CANDIDATES Recommend the top 1–3 tools. Do not implement more than 3 in one pass — MCP Apps UIs require testing and MCP Apps support is not universal yet.
Output the audit as:
UI CANDIDATES
1. <tool-name> — <reason> — priority: HIGH / MEDIUM / LOW
2. <tool-name> — <reason> — priority: HIGH / MEDIUM / LOW
============================================================ PHASE 2: SCAFFOLD THE UI TEMPLATES
For each selected tool:
-
CREATE THE DIRECTORY
- TypeScript:
src/ui/<tool-slug>/ - Python:
ui/<tool-slug>/
- TypeScript:
-
WRITE index.html Requirements:
- Self-contained (inline CSS and JS — no external dependencies)
- Responsive: uses relative units, flexbox or grid
- Theme-aware:
prefers-color-schemedark/light via CSS variables - Zero runtime frameworks — plain HTML + CSS + vanilla JS only
- Accessible: all inputs have visible labels,
aria-labelon icon-only buttons, 48px minimum touch targets, focus-visible ring on interactive elements
Template:
<!doctype html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title><TOOL_NAME></title> <style> :root { --bg: #ffffff; --fg: #111827; --border: #e5e7eb; --primary: #7c3aed; --primary-fg: #ffffff; --radius: 8px; } @media (prefers-color-scheme: dark) { :root { --bg: #0f0f0f; --fg: #f3f4f6; --border: #27272a; } } * { box-sizing: border-box; margin: 0; padding: 0; } body { background: var(--bg); color: var(--fg); font-family: system-ui, sans-serif; padding: 16px; min-height: 100vh; } form { display: flex; flex-direction: column; gap: 12px; max-width: 480px; } label { font-size: 14px; font-weight: 500; } input, select, textarea { width: 100%; padding: 10px 12px; border: 1px solid var(--border); border-radius: var(--radius); background: var(--bg); color: var(--fg); font-size: 14px; min-height: 48px; } input:focus-visible, select:focus-visible, textarea:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; } button[type="submit"] { background: var(--primary); color: var(--primary-fg); border: none; border-radius: var(--radius); padding: 12px 20px; font-size: 14px; font-weight: 600; cursor: pointer; min-height: 48px; } button[type="submit"]:hover { opacity: 0.9; } button[type="submit"]:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; } #status { font-size: 13px; margin-top: 8px; } #result { margin-top: 16px; padding: 12px; border: 1px solid var(--border); border-radius: var(--radius); font-family: monospace; font-size: 13px; white-space: pre-wrap; display: none; } </style> </head> <body> <!-- FORM: one <label>+<input> pair per tool parameter --> <form id="main-form" aria-label="<TOOL_NAME> configuration"> <!-- INSERT FIELDS HERE --> <button type="submit">Run <TOOL_NAME></button> <p id="status" aria-live="polite"></p> </form> <div id="result" role="log" aria-live="polite" aria-label="Tool result"></div> <script> const MCP_HOST = window.parent; document.getElementById("main-form").addEventListener("submit", async (e) => { e.preventDefault(); document.getElementById("status").textContent = "Running…"; const args = {}; // collect form values here MCP_HOST.postMessage({ jsonrpc: "2.0", id: crypto.randomUUID(), method: "tools/call", params: { name: "<TOOL_NAME>", arguments: args } }, "*"); }); window.addEventListener("message", (e) => { if (e.data?.result) { const el = document.getElementById("result"); el.style.display = "block"; el.textContent = JSON.stringify(e.data.result, null, 2); document.getElementById("status").textContent = "Done."; } else if (e.data?.error) { document.getElementById("status").textContent = "Error: " + e.data.error.message; } }); </script> </body> </html> -
FILL IN TOOL-SPECIFIC FIELDS For each parameter in the tool's inputSchema, add a labeled input:
string→<input type="text">number/integer→<input type="number">boolean→<input type="checkbox">stringwithenum→<select>with one<option>per enum valuestringwith long description →<textarea rows="4">- Required parameters get
requiredattribute on the input
============================================================ PHASE 3: DECLARE THE UI IN THE TOOL MANIFEST
-
TYPESCRIPT — update server.tool() call
server.tool( "deploy_preview", "Deploy a preview environment.", { branch: z.string().describe("Git branch to deploy"), }, async ({ branch }) => { /* handler */ }, { ui: { template: "deploy-preview-ui", entrypoint: "index.html", sandbox: ["allow-scripts", "allow-same-origin"], }, } ); -
PYTHON — update @server.tool() decorator
@server.tool( ui={ "template": "deploy-preview-ui", "entrypoint": "index.html", "sandbox": ["allow-scripts", "allow-same-origin"], } ) async def deploy_preview(branch: str) -> list[TextContent]: """Deploy a preview environment.""" ... -
REGISTER TEMPLATES IN SERVER INIT TypeScript:
server.registerUiTemplate("deploy-preview-ui", { path: "./src/ui/deploy-preview", });Python:
server.register_ui_template("deploy-preview-ui", path="./ui/deploy-preview") -
VERIFY MANIFEST The
tools/listresponse must include theuifield for each tool that has a registered template. Verify with:curl -s -X POST http://localhost:3000/mcp \ -H "Content-Type: application/json" \ -H "Mcp-Method: tools/list" \ -H "Mcp-Name: list" \ -H "MCP-Protocol-Version: 2026-07-28" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' \ | jq '.result.tools[] | {name, ui}'
============================================================ PHASE 4: WIRE SECURITY
-
SANDBOX ATTRIBUTE SELECTION Start with the most restrictive set and add only what's needed:
allow-scripts— required for any JS (always include)allow-same-origin— required if the UI fetches from the same serverallow-forms— only if the UI submits to an external URL (avoid)- NEVER add
allow-top-navigationorallow-popupsunless there is a specific documented reason reviewed by a security engineer
-
ORIGIN VALIDATION ON MESSAGE EVENTS In the UI's
window.addEventListener("message", ...)handler, always validate the origin before acting on the message:window.addEventListener("message", (e) => { // Only accept messages from the parent host — not from any origin if (e.origin !== window.location.origin && e.source !== window.parent) return; // ... handle result }); -
CONTENT SECURITY POLICY Add a
<meta>CSP header to every UI template:<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';">This prevents the UI from loading external scripts or making cross-origin requests — the sandbox attribute alone is not sufficient.
============================================================ PHASE 5: TEST AND VALIDATE
-
HOST RENDERING TEST Open the host that supports MCP Apps (e.g., Claude Desktop with MCP Apps support enabled in settings). Invoke the tool via the agent. The host should render the iframe instead of printing the JSON args.
-
FORM SUBMISSION TEST Fill in the form and submit. Verify:
- The
tools/callpostMessage is sent with the correct arguments - The host returns the result message
- The result renders in
#result - Error messages render in
#status
- The
-
FALLBACK TEST Connect a client that does NOT support MCP Apps (older client or plain API). Invoke the tool normally. Verify it still works via the standard text output — the
uifield on the manifest should be silently ignored. -
CONFORMANCE SUITE
npx @modelcontextprotocol/conformance run \ --server http://localhost:3000/mcp \ --suite mcp-appsFix any failures before committing.
-
ACCESSIBILITY CHECK Open each UI template in a browser and verify:
- All inputs are focusable and have visible focus indicators
- All inputs have associated labels (click label → focuses input)
- All interactive elements are reachable by keyboard alone
aria-liveregions announce result and error states
============================================================ OUTPUT
MCP Apps Build Complete
Audit
- Tools evaluated: [count]
- Tools selected for MCP Apps UI: [list]
Templates built
- [tool-name]: [path/to/ui] — [key UI decisions: form type, theme support, etc.]
Manifest declarations
- [count] tools updated with
uifield - Sandbox policy: [list of sandbox attributes used and why]
Security
- CSP meta tag: [added / not needed — stdio only]
- Origin validation: [added / not applicable]
Tests
- Host rendering: [pass / skip — no MCP Apps host available]
- Form submission: [pass / skip]
- Fallback (no-UI client): [pass / skip]
- Conformance suite: [pass / fail / skip]
- Accessibility: [pass / issues found: list]
Next steps
- Ship to users on an MCP Apps-compatible host (Claude Desktop 1.4+, or any host that implements SEP-1865)
- Monitor host adoption — MCP Apps renders only if the host supports it; tool works for all clients regardless
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.