mcpField
M
mcp-visual-output

mcp-visual-output

github.com/yonatangrossDesign & UI29999 installsUpdated today
Repository

About mcp-visual-output

A skill for adding interactive, JSON-spec-driven visual output to new or existing MCP servers. It covers setup, sandbox security, progressive rendering, and dashboard patterns for developers building MCP interfaces.

Key features

  1. 01Set up visual output for new servers
  2. 02Add rendering tools to existing servers
  3. 03Configure iframe sandbox and CSP
  4. 04Stream specs progressively
  5. 05Apply dashboard layout patterns

Use cases

  1. 01Build a dashboard for multiple metrics
  2. 02Render tables with five or more rows
  3. 03Show status across multiple systems

MCP support

Guides developers in exposing JSON-rendered interfaces through MCP tools and resources, using `@json-render/mcp`. It covers stdio and Streamable HTTP transports.

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/yonatangross/orchestkit --skill mcp-visual-output

After installing, type/mcp-visual-outputin Claude Code to confirm it loaded.

Bundled files

References and scripts installed along with SKILL.md.

test-cases.json
references/
component-recipes.md
mcp-integration.md
upstream-mcp.md
rules/
_sections.md
_template.md
dashboard-patterns.md
mcp-app-setup.md
sandbox-csp.md
streaming-output.md

Details

From the project README

Upstream version reference: @json-render/mcp 0.21.0 (2026-09-30).

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

CategoryRuleImpactKey Pattern
Setupmcp-app-setup.mdHIGHcreateMcpApp() and registerJsonRenderTool()
Securitysandbox-csp.mdHIGHCSP declarations, iframe sandboxing
Renderingstreaming-output.mdMEDIUMProgressive rendering via JSON Patch
Patternsdashboard-patterns.mdMEDIUMStat 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

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

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:

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:

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:

{
  "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

DecisionRecommendation
New vs existing servercreateMcpApp() for new; registerJsonRenderTool() to add to existing
CSP policyMinimal -- only declare domains you actually need
StreamingAlways enable progressive rendering; never wait for full spec
Dashboard depthKeep element trees flat (2-3 levels max) for streamability
Component count3-5 component types per catalog covers most dashboards
Visual vs textUse 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

ScenarioUse Visual OutputUse Plain Text
Multiple metrics at a glanceYes -- StatGridNo
Tabular data (5+ rows)Yes -- DataTableNo
Status of multiple systemsYes -- StatusBadge gridNo
Single value answerNoYes
Error messageNoYes
File content / codeNoYes

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

Comments

Markdown supported