--- name: mcp-visual-output compatibility: "Claude Code 2.1.220+" description: "Interactive MCP visual output via @json-render/mcp: upgrade plain JSON tool responses to dashboards rendered in sandboxed iframes inside MCP clients like Claude, Cursor, and ChatGPT. Use when a tool result would read better as a stat grid, data table, or status badge than as text. For the server itself (transport, auth, tool handlers, security) reach for ork:mcp-patterns." tags: [mcp, json-render, visual-output, dashboard, iframe, sandbox, interactive-ui, genui] version: 1.1.0 author: OrchestKit user-invocable: false disable-model-invocation: false complexity: medium context: inherit persuasion-type: guidance metadata: category: mcp upstream-package: "@json-render/mcp" upstream-version-tested: "0.19.0" --- # MCP Visual Output Upgrade plain MCP tool responses to interactive dashboards rendered inside AI conversations. Built on `@json-render/mcp`, which bridges the json-render spec system with MCP's tool/resource model -- the AI generates a typed JSON spec, and a sandboxed iframe renders it as an interactive UI. > **Building an MCP server from scratch?** Use `ork:mcp-patterns` for server setup, transport, and security. This skill focuses on the **visual output layer** after your server is running. > > **Need the full component catalog?** See `ork:json-render-catalog` for all available components, props, and composition patterns. ## Decision Tree -- Which File to Read ``` What are you doing? | +-- Setting up visual output for the first time | +-- New MCP server -----------> rules/mcp-app-setup.md | +-- Existing MCP server ------> rules/mcp-app-setup.md (registerJsonRenderTool section) | +-- Configuring security / sandbox | +-- CSP declarations ----------> rules/sandbox-csp.md | +-- Iframe permissions --------> rules/sandbox-csp.md | +-- Rendering strategy | +-- Progressive streaming -----> rules/streaming-output.md | +-- Dashboard layouts ----------> rules/dashboard-patterns.md | +-- API reference | +-- Server-side API -----------> references/mcp-integration.md | +-- Component recipes ----------> references/component-recipes.md ``` ## Quick Reference | Category | Rule | Impact | Key Pattern | |----------|------|--------|-------------| | **Setup** | `mcp-app-setup.md` | HIGH | createMcpApp() and registerJsonRenderTool() | | **Security** | `sandbox-csp.md` | HIGH | CSP declarations, iframe sandboxing | | **Rendering** | `streaming-output.md` | MEDIUM | Progressive rendering via JSON Patch | | **Patterns** | `dashboard-patterns.md` | MEDIUM | Stat grids, status badges, data tables | **Total: 4 rules across 3 categories** ## How It Works 1. **Define a catalog** -- typed component schemas using `defineCatalog()` + Zod 2. **Register with MCP** -- `createMcpApp()` for new servers or `registerJsonRenderTool()` for existing ones 3. **AI generates specs** -- the model produces a JSON spec conforming to the catalog 4. **Iframe renders it** -- a bundled React app inside a sandboxed iframe renders the spec with `useJsonRenderApp()` + `` The AI never writes HTML or CSS. It produces a structured JSON spec that references catalog components by type. The iframe app renders those components using a pre-built registry. ## Quick Start -- New MCP Server ```typescript import { createMcpApp } from '@json-render/mcp' import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' import { buildAppHtml } from '@json-render/mcp/app' import { catalog } from './catalog' // Generate the iframe HTML from the bundled JS/CSS (docs-prescribed generator). const bundledHtml = buildAppHtml({ entry: './app.tsx' }) // 1. Create the MCP app (async; returns an McpServer, no .start()/.close()). // name + version are required; tool config nests under `tool` // (default tool name is 'render-ui'). There is no top-level `csp`. const server = await createMcpApp({ name: 'my-app', version: '1.0.0', catalog, // component schemas the AI can use html: bundledHtml, // pre-built iframe app (single HTML file) tool: { name: 'render-dashboard', description: 'Render an interactive dashboard from a json-render spec', }, }) // 2. Connect a transport -- stdio, Streamable HTTP, or any MCP transport await server.connect(new StdioServerTransport()) ``` ## Quick Start -- Enhance Existing Server with Visual Output ```typescript import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' import { registerJsonRenderTool, registerJsonRenderResource } from '@json-render/mcp' import { buildAppHtml } from '@json-render/mcp/app' import { catalog } from './catalog' const server = new McpServer({ name: 'my-server', version: '1.0.0' }) // Generate the iframe HTML from the bundled JS/CSS (docs-prescribed generator). const bundledHtml = buildAppHtml({ entry: './app.tsx' }) const resourceUri = 'ui://my-server/dashboard' // Register the render tool (lets the model return specs). // name, title, description, and resourceUri are all required. registerJsonRenderTool(server, { catalog, name: 'render-dashboard', title: 'Render Dashboard', description: 'Render an interactive dashboard from a json-render spec', resourceUri, }) // Serve the bundled HTML iframe app as a resource (new in 0.15). // resourceUri must match the tool's resourceUri. registerJsonRenderResource(server, { resourceUri, html: bundledHtml }) ``` `registerJsonRenderResource()` was added in 0.15 to separate **tool registration** from **UI resource serving** — useful when the host caches the bundled HTML (clients: Claude, ChatGPT, Cursor, VS Code Copilot, Goose, Postman). Transports: stdio **and** Streamable HTTP (Express) both supported. This skill is verified against `@json-render/mcp` **0.19.0**. ## Client-Side Iframe App The iframe app receives specs from the MCP host and renders them: ```typescript import { useJsonRenderApp } from '@json-render/mcp/app' import { Renderer } from '@json-render/react' import { registry } from './registry' function App() { const { spec, loading } = useJsonRenderApp() if (loading) return return } ``` ## Catalog Definition Catalogs define what components the AI can use. Each component has typed props via Zod: ```typescript import { defineCatalog } from '@json-render/core' import { schema } from '@json-render/react/schema' import { z } from 'zod' export const dashboardCatalog = defineCatalog(schema, { components: { StatGrid: { props: z.object({ items: z.array(z.object({ label: z.string(), value: z.string(), trend: z.enum(['up', 'down', 'flat']).optional(), color: z.enum(['green', 'red', 'yellow', 'blue']).optional(), })), }), children: false, }, StatusBadge: { props: z.object({ label: z.string(), status: z.enum(['success', 'warning', 'error', 'info', 'pending']), }), children: false, }, DataTable: { props: z.object({ columns: z.array(z.object({ key: z.string(), label: z.string() })), rows: z.array(z.record(z.string())), }), children: false, }, }, }) ``` ## Example: Eval Results Dashboard The AI generates a spec like this -- flat element map, no nesting beyond 2 levels: ```json { "root": "dashboard", "elements": { "dashboard": { "type": "Card", "props": { "title": "Eval Results -- v7.21.1" }, "children": ["stats", "table"] }, "stats": { "type": "StatGrid", "props": { "items": [ { "label": "Skills Evaluated", "value": "94", "trend": "flat" }, { "label": "Pass Rate", "value": "97.8%", "trend": "up", "color": "green" }, { "label": "Avg Score", "value": "8.2/10", "trend": "up" } ] } }, "table": { "type": "DataTable", "props": { "columns": [ { "key": "skill", "label": "Skill" }, { "key": "score", "label": "Score" }, { "key": "status", "label": "Status" } ], "rows": [ { "skill": "implement", "score": "9.1", "status": "pass" }, { "skill": "verify", "score": "8.7", "status": "pass" } ] } } } } ``` ## Key Decisions | Decision | Recommendation | |----------|----------------| | New vs existing server | `createMcpApp()` for new; `registerJsonRenderTool()` to add to existing | | CSP policy | Minimal -- only declare domains you actually need | | Streaming | Always enable progressive rendering; never wait for full spec | | Dashboard depth | Keep element trees flat (2-3 levels max) for streamability | | Component count | 3-5 component types per catalog covers most dashboards | | Visual vs text | Use visual output for multi-metric views; plain text for single values | > **CC 2.1.113** fixed MCP concurrent-call timeout handling — hanging tool calls now error cleanly instead of blocking the queue. Parallel tool invocation from dashboards is safer; no workarounds needed. ## When to Use Visual Output vs Plain Text | Scenario | Use Visual Output | Use Plain Text | |----------|------------------|----------------| | Multiple metrics at a glance | Yes -- StatGrid | No | | Tabular data (5+ rows) | Yes -- DataTable | No | | Status of multiple systems | Yes -- StatusBadge grid | No | | Single value answer | No | Yes | | Error message | No | Yes | | File content / code | No | Yes | ## Common Mistakes 1. Returning raw HTML strings from MCP tools instead of json-render specs (breaks type safety, no streaming) 2. Deeply nested component trees that cannot stream progressively (keep flat) 3. Using `script-src 'unsafe-inline'` in CSP declarations (security risk, unnecessary) 4. Waiting for the full spec before rendering (defeats progressive rendering) 5. Defining 20+ component types in a single catalog (increases prompt token cost) 6. Missing `html` bundle in `createMcpApp()` config (iframe has nothing to render) ## Related Skills - `ork:mcp-patterns` -- MCP server building, transport, security - `ork:json-render-catalog` -- Full component catalog and composition patterns - `ork:multi-surface-render` -- Rendering across Claude, Cursor, ChatGPT, web - `ork:ai-ui-generation` -- GenUI patterns for AI-generated interfaces