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.
Scanned 9/2/2026
Install to Claude Code
npx -y skills add yonatangross/orchestkit --skill mcp-visual-output --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Mcp Visual Output?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/yonatangross-mcp-visual-output-orchestkit)More formats (shields.io, HTML) on the badges page.
---
name: mcp-visual-output
compatibility: "Claude Code 2.1.251+"
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.20.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()` + `<Renderer />`
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.20.0**, which shipped zero code change: every published file is byte-identical to 0.19.0 apart from the version and its pinned `@json-render/core` dependency. New capability in that release therefore belongs to core and react, not to this package. What reaches an MCP-rendered iframe indirectly is named slots and nested repeats, plus a fix so named `onSuccess`/`onError` chained actions actually receive their configured `params`.
## 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 <Skeleton />
return <Renderer spec={spec} registry={registry} />
}
```
## 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
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!