GHL × json-render Spike
Proof-of-concept: replace hand-coded HTML MCP apps with AI-generated UIs using Vercel's json-render library.
What This Is
We have 11 GHL MCP apps built as self-contained HTML files (~400-540 lines each, ~5,000 lines total). Each is hand-coded with bespoke markup. This spike tests whether we can:
- Define a component catalog (Zod schemas) that constrains what AI can generate
- Build React components matching the existing visual quality
- Have AI produce JSON trees that the Renderer turns into polished UIs
- Serve via MCP Apps SDK (
@modelcontextprotocol/ext-apps) for rendering in AI chat
How It Works
User Prompt → AI + Catalog (guardrailed) → JSON Tree → React Renderer → Polished UI
┌──────────────┐ ┌───────────────┐ ┌──────────────┐ ┌──────────────┐
│ "Show me the │────▶│ Claude + GHL │────▶│ JSON Tree │────▶│ React with │
│ pipeline" │ │ Catalog │ │ (validated) │ │ Tailwind UI │
└──────────────┘ └───────────────┘ └──────────────┘ └──────────────┘
Key insight: One HTML app bundle serves ALL views. The JSON tree in structuredContent determines what renders.
Quick Start
cd ghl-json-render-spike
npm install
npm run dev
# Open http://localhost:3000
Demo Pages
| Page | URL | Components Used |
|---|---|---|
| Landing | / |
Overview + links to demos |
| Contact Grid | /contact-grid |
PageHeader · SearchBar · FilterChips · DataTable |
| Pipeline Board | /pipeline |
PageHeader (gradient) · KanbanBoard |
| Campaign Stats | /campaign |
PageHeader · StatsGrid · MetricCard · ProgressBar · Card |
| Invoice Preview | /invoice |
DetailHeader · SplitLayout · InfoBlock · LineItemsTable · KeyValueList · ActionBar |
| Playground | /playground |
Paste any JSON tree → see it render live |
Project Structure
src/
├── catalog/
│ └── ghl-catalog.ts # Component catalog (Zod schemas + action defs)
├── components/ # React implementations of each catalog component
│ ├── registry.tsx # Maps type names → React components
│ ├── PageHeader.tsx
│ ├── DataTable.tsx
│ ├── KanbanBoard.tsx
│ ├── MetricCard.tsx
│ ├── LineItemsTable.tsx
│ └── ... (20 components total)
├── examples/ # JSON trees showing what AI would generate
│ ├── contact-grid.json
│ ├── pipeline-board.json
│ ├── campaign-stats.json
│ └── invoice-preview.json
├── lib/
│ └── GHLRenderer.tsx # Wraps DataProvider + ActionProvider + Renderer
├── mcp/
│ ├── generate-ui.ts # System prompts + catalog → prompt generation
│ └── mcp-tool-handler.ts # Full MCP Apps SDK integration example
└── app/ # Next.js pages (demo harness)
Component Catalog (20 Components)
Layout
- PageHeader — title, subtitle, status badge, meta stats, gradient variant
- Card — container with optional header, padding variants
- StatsGrid — responsive grid of metric cards
- SplitLayout — two-column layout (50/50, 33/67, 67/33)
- Section — titled section wrapper
Data Display
- DataTable — sortable table with column formats, row selection, pagination
- KanbanBoard — columns with draggable cards (pipeline view)
- MetricCard — big number + label + trend indicator
- StatusBadge — colored badge by status variant
- Timeline — chronological event list
- ProgressBar — percentage bar with benchmark markers
Detail Views
- DetailHeader — entity name, ID, status badge
- KeyValueList — label/value pairs (totals, metadata)
- LineItemsTable — invoice-style table with quantities and totals
- InfoBlock — labeled block of info (From/To on invoices)
Interactive
- SearchBar — search input with focus ring
- FilterChips — toggleable filter tags
- TabGroup — tab navigation
Actions
- ActionButton — primary/secondary/danger/ghost variants
- ActionBar — row of action buttons
Action Definitions (12 Actions)
view_contact, edit_contact, delete_contact, move_opportunity, send_invoice, mark_paid, void_invoice, download_pdf, pause_campaign, resume_campaign, export_data, refresh_data
MCP Apps Integration
The spike includes a complete example of how this integrates with the official MCP Apps SDK:
registerAppTool → returns structuredContent with UITree
registerAppResource → serves single-file HTML bundle
HTML app receives UITree via ontoolresult → feeds to json-render Renderer
Key pattern: One registerAppResource call serves one HTML bundle. ALL GHL views render through it. The structuredContent.uiTree JSON tree determines the UI.
See src/mcp/mcp-tool-handler.ts for the full server example using:
registerAppToolfrom@modelcontextprotocol/ext-apps/serverregisterAppResourcewithRESOURCE_MIME_TYPEui://scheme resource URIsstructuredContentfor passing data to the UI- Action tools (no UI) called from the app via
app.callServerTool()
Comparison: Old vs New
Old Approach (hand-coded HTML)
- 11 separate HTML files, each 400-540 lines
- ~5,000 lines of bespoke HTML/CSS/JS
- Each app has duplicated styles, markup patterns
- Changes to design require editing all 11 files
- No validation — errors surface at runtime
New Approach (json-render)
- 1 component catalog: ~270 lines (Zod schemas)
- 20 React components: ~800 lines total
- 4 JSON examples: ~400 lines total (what AI generates)
- ~1,470 lines total for equivalent coverage
- Changes to design: update one component, all views update
- Catalog-enforced validation — AI can only use defined components
Line Count Reduction
| Old | New | Savings | |
|---|---|---|---|
| Per app | ~470 lines | ~100 lines JSON | ~79% |
| Total (4 apps) | ~1,880 lines | ~400 lines JSON + ~1,070 shared | ~22% fewer total, 79% less per-app work |
| Adding a new app | ~470 lines from scratch | ~100 lines JSON (AI-generated) | AI does it |
The real win: adding new views costs ~0 developer effort — Claude generates the JSON tree from the catalog.
Next Steps
- Build the single-file HTML app — Vite + vite-plugin-singlefile bundle containing React, json-render, and all GHL components
- Wire up to real MCP server — Replace the Next.js demo with
registerAppResourceserving the bundle - Add streaming — Use
useUIStreamfrom json-render for progressive rendering as Claude generates - Expand the catalog — Add calendar, workflow, timeline components for remaining 7 GHL apps
- AI generation testing — Test Claude generating JSON trees from the catalog prompt with real GHL data
- Action integration — Wire
app.callServerTool()to actual GHL API endpoints