diff --git a/README.md b/README.md index 0ba5bc6..14149f9 100644 --- a/README.md +++ b/README.md @@ -42,14 +42,60 @@ make dev # Start all services You can also use `pnpm` directly (`pnpm dev`, `pnpm dev:app`, `pnpm dev:agent`, etc.). +## MCP Server (Self-Hosted) + +The repo includes a standalone [Model Context Protocol](https://modelcontextprotocol.io) server that exposes the design system, skill instructions, and an HTML document assembler to any MCP-compatible client — including Claude Desktop, Claude Code, and Cursor. + +### What it provides + +- **`assemble_document` tool** — wraps HTML fragments with the full design system CSS and bridge JS, returning an iframe-ready document +- **Skill resources** — browse and read skill instruction documents (`skills://list`, `skills://{name}`) +- **Prompt templates** — pre-composed prompts for widgets, SVG diagrams, and advanced visualizations + +### Claude Desktop (stdio) + +Add to your Claude Desktop config (`claude_desktop_config.json`): + +```json +{ + "mcpServers": { + "open-generative-ui": { + "command": "node", + "args": ["dist/stdio.js"], + "cwd": "/path/to/apps/mcp" + } + } +} +``` + +### Claude Code / HTTP clients + +```bash +# Start the HTTP server +cd apps/mcp && pnpm dev +``` + +Add to `.mcp.json`: + +```json +{ + "openGenerativeUI": { + "url": "http://localhost:3100/mcp" + } +} +``` + +See [apps/mcp/README.md](apps/mcp/README.md) for full configuration, Docker deployment, and API reference. + ## Architecture -Turborepo monorepo with two apps: +Turborepo monorepo with three packages: ``` apps/ ├── app/ Next.js 16 frontend (CopilotKit v2, React 19, Tailwind 4) -└── agent/ LangGraph Python agent (GPT-5.4, CopilotKit middleware) +├── agent/ LangGraph Python agent (GPT-5.4, CopilotKit middleware) +└── mcp/ Standalone MCP server (design system + skills + document assembler) ``` ### How It Works diff --git a/apps/mcp/.dockerignore b/apps/mcp/.dockerignore new file mode 100644 index 0000000..7c53328 --- /dev/null +++ b/apps/mcp/.dockerignore @@ -0,0 +1,9 @@ +node_modules/ +npm-debug.log +.git +.gitignore +README.md +.env.example +.DS_Store +dist/ +.turbo/ diff --git a/apps/mcp/.env.example b/apps/mcp/.env.example new file mode 100644 index 0000000..a7ffead --- /dev/null +++ b/apps/mcp/.env.example @@ -0,0 +1,12 @@ +# Server Configuration +MCP_PORT=3100 +NODE_ENV=development + +# CORS (comma-separated origins, default: * for development) +ALLOWED_ORIGINS=* + +# Skills directory (default: ./skills relative to package root) +SKILLS_DIR=./skills + +# Logging +LOG_LEVEL=info diff --git a/apps/mcp/.gitignore b/apps/mcp/.gitignore new file mode 100644 index 0000000..049d6c8 --- /dev/null +++ b/apps/mcp/.gitignore @@ -0,0 +1,11 @@ +node_modules/ +dist/ +.env +.env.local +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.DS_Store +.turbo/ +.venv/ diff --git a/apps/mcp/Dockerfile b/apps/mcp/Dockerfile new file mode 100644 index 0000000..60042e5 --- /dev/null +++ b/apps/mcp/Dockerfile @@ -0,0 +1,53 @@ +# Multi-stage build for production +FROM node:20-alpine AS builder + +WORKDIR /app + +# Install build dependencies +RUN apk add --no-cache python3 make g++ + +# Copy package files +COPY package.json pnpm-lock.yaml ./ + +# Install pnpm +RUN npm install -g pnpm@9 + +# Install dependencies +RUN pnpm install --frozen-lockfile + +# Copy source +COPY tsconfig.json ./ +COPY src ./src +COPY skills ./skills + +# Build +RUN pnpm build + +# Production image +FROM node:20-alpine + +WORKDIR /app + +# Install dumb-init for proper signal handling +RUN apk add --no-cache dumb-init + +# Copy built application from builder +COPY --from=builder /app/dist ./dist +COPY --from=builder /app/skills ./skills +COPY --from=builder /app/node_modules ./node_modules +COPY package.json ./ + +# Non-root user +RUN addgroup -g 1001 -S nodejs && \ + adduser -S nodejs -u 1001 + +USER nodejs + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:${MCP_PORT:-3100}/health || exit 1 + +EXPOSE 3100 + +ENTRYPOINT ["/sbin/dumb-init", "--"] +CMD ["node", "dist/index.js"] diff --git a/apps/mcp/README.md b/apps/mcp/README.md new file mode 100644 index 0000000..c5d0089 --- /dev/null +++ b/apps/mcp/README.md @@ -0,0 +1,297 @@ +# OpenGenerativeUI MCP Server + +A standalone, independently deployable [Model Context Protocol](https://modelcontextprotocol.io) server that exposes OpenGenerativeUI's design system, skills, and document renderer to any MCP-compatible client. + +## Features + +- **Design System Tool** — `assemble_document` wraps HTML fragments with OpenGenerativeUI's complete CSS design system and bridge JavaScript +- **Skill Resources** — Browse and read skill instruction documents via `skills://list` and `skills://{name}` +- **Prompt Templates** — Pre-composed prompts for common visualization tasks: `create_widget`, `create_svg_diagram`, `create_visualization` +- **Standalone** — No dependencies on other packages; can be deployed independently +- **Configurable** — Environment variables for port, CORS origins, skills directory, and logging + +## Quick Start + +### Prerequisites +- Node.js >= 18 +- pnpm or npm + +### Installation + +```bash +# Navigate to the MCP package +cd apps/mcp + +# Install dependencies +pnpm install + +# Start the development server +pnpm dev +# → MCP server running on http://localhost:3100/mcp + +# Health check +curl http://localhost:3100/health +# → {"status":"ok"} +``` + +### Build for Production + +```bash +pnpm build +pnpm start +``` + +## Configuration + +Create a `.env` file in the package root (copy from `.env.example`): + +```bash +# Server port (default: 3100) +MCP_PORT=3100 + +# CORS origins, comma-separated (default: * for development) +ALLOWED_ORIGINS=* + +# Skills directory path (default: ./skills) +SKILLS_DIR=./skills + +# Log level (default: info) +LOG_LEVEL=info +``` + +## Usage + +### Claude Desktop (stdio) + +Claude Desktop uses stdio transport. Build first, then add to your `claude_desktop_config.json`: + +```bash +pnpm build +``` + +```json +{ + "mcpServers": { + "open-generative-ui": { + "command": "node", + "args": ["dist/stdio.js"], + "cwd": "/absolute/path/to/apps/mcp" + } + } +} +``` + +For development, you can use `tsx` directly: + +```json +{ + "mcpServers": { + "open-generative-ui": { + "command": "npx", + "args": ["tsx", "src/stdio.ts"], + "cwd": "/absolute/path/to/apps/mcp" + } + } +} +``` + +### Claude Code (HTTP) + +Add to `.mcp.json`: + +```json +{ + "openGenerativeUI": { + "url": "http://localhost:3100/mcp" + } +} +``` + +Then start the HTTP server with `pnpm dev`. + +### Any MCP Client + +- **stdio**: Run `node dist/stdio.js` (or `pnpm start:stdio`) +- **HTTP**: Connect to `http://localhost:3100/mcp` (start with `pnpm dev` or `pnpm start`) + +## API Reference + +### Tool: `assemble_document` + +Wraps an HTML fragment with the complete OpenGenerativeUI design system. + +**Input:** +```typescript +{ + title: string; // Short title, e.g., "Binary Search Visualization" + description: string; // One-sentence explanation + html: string; // Self-contained HTML fragment (inline + + + +
+ + + +
+ + +``` + +### Math Visualizations +For plotting functions, showing geometric relationships, or exploring equations. + +**Pattern: Function Plotter with SVG** +```html + + + + + + x + y + + + + +
+ + +
+ + +``` + +### Sortable / Filterable Data Tables +```html + + + + + + + + + + + + + + + +
NameValueStatus
+ + +``` + +--- + +## Part 4: Chart.js — Advanced Patterns + +### Dark Mode Awareness +```javascript +const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches; +const textColor = isDark ? '#c2c0b6' : '#3d3d3a'; +const gridColor = isDark ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.06)'; +const tooltipBg = isDark ? '#2C2C2A' : '#fff'; +``` + +Canvas cannot read CSS variables — always detect dark mode and use +hardcoded hex values. + +### Wrapper Pattern (Critical for Sizing) +```html +
+ +
+``` +- Height goes on the wrapper div ONLY, never on canvas. +- Always set `responsive: true, maintainAspectRatio: false`. +- For horizontal bar charts: height = (bars × 40) + 80 pixels. + +### Custom Legend (Always Use This) +Disable Chart.js default legend and build HTML: +```javascript +plugins: { legend: { display: false } } +``` +```html +
+ + Series A — 65% + + + Series B — 35% + +
+``` + +### Dashboard Layout +Metric cards on top → chart below → sendPrompt for drill-down: +```html + +
+ +
+ + +
+ +
+``` + +### Chart Type Selection Guide +| Data pattern | Chart type | +|----------------------------|---------------------| +| Trend over time | Line | +| Category comparison | Vertical bar | +| Ranking (few items) | Horizontal bar | +| Part of whole | Doughnut | +| Distribution | Histogram (bar) | +| Correlation (2 variables) | Scatter | +| Multi-variable comparison | Radar | +| Range / uncertainty | Line with fill area | + +--- + +## Part 5: Generative Art and Illustration + +For when the user asks for something creative, decorative, or aesthetic. + +### When to Use +- "Draw me a sunset" / "Create a pattern" +- Decorative headers or visual breaks +- Mood illustrations for creative writing +- Abstract visualizations of data or music + +### Rules (Different from Diagrams) +- Fill the canvas — art should feel rich, not sparse +- Bold colors are encouraged. You can use custom hex freely. +- Layered overlapping shapes create depth +- Organic forms with `` curves, ``, `` +- Texture via repetition (hatching, dots, parallel lines) +- Geometric patterns with `` +- NO gradients, shadows, blur, or glow (still flat aesthetic) + +### Pattern: Geometric Art +```svg + + + + + + + + + + + + + +``` + +### Pattern: Radial Symmetry +```svg + + + + + + + + + + + + +``` + +### Pattern: Landscape with Layered Shapes +For physical scenes, use ALL hardcoded hex (no theme classes): +```svg + + + + + + + + + + + +``` + +--- + +## Part 6: Advanced Patterns + +### Tabbed / Multi-View Interfaces +Since content streams top-down, don't use `display: none` during streaming. +Instead, render all content stacked, then use post-stream JS to create tabs: + +```html +
+ + + +
+ +
+
+
+ + +``` + +### sendPrompt() — Chat-Driven Interactivity +A global function that sends a message as if the user typed it. +Use it when the user's next action benefits from AI thinking: + +```html + + +``` + +**Use for**: drill-downs, follow-up questions, "explain this part". +**Don't use for**: filtering, sorting, toggling — handle those in JS. +Append ` ↗` to button text when it triggers sendPrompt. + +### Responsive Grid Pattern +```css +display: grid; +grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); +gap: 12px; +``` +Use `minmax(0, 1fr)` if children have large min-content that could overflow. + +### CSS Animations (Subtle and Purposeful) +```css +/* Only animate transform and opacity for performance */ +@keyframes fadeSlideIn { + from { opacity: 0; transform: translateY(8px); } + to { opacity: 1; transform: translateY(0); } +} + +/* Always respect user preferences */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + } +} + +/* Flowing particles / convection currents */ +@keyframes flow { to { stroke-dashoffset: -20; } } +.flowing { + stroke-dasharray: 5 5; + animation: flow 1.6s linear infinite; +} + +/* Pulsing for active elements */ +@keyframes pulse { + 0%, 100% { opacity: 0.3; } + 50% { opacity: 0.7; } +} +``` + +--- + +## Part 7: External Libraries (CDN Allowlist) + +Only these CDN origins work (CSP-enforced): +- `cdnjs.cloudflare.com` +- `esm.sh` +- `cdn.jsdelivr.net` +- `unpkg.com` + +### Useful Libraries + +**Chart.js** (data visualization): +```html + +``` + +**Three.js** (3D graphics): +```html + +``` + +**D3.js** (advanced data viz, force layouts, geographic maps): +```html + +``` + +**Mermaid** (ERDs, sequence diagrams, class diagrams): +```html + +``` + +**Tone.js** (audio synthesis): +```html + +``` + +--- + +## Part 8: Quality Checklist + +Before producing any visual, run through this: + +### Functional +- [ ] Does it work without JavaScript during streaming? (Content visible) +- [ ] Do all interactive controls have event handlers? +- [ ] Are all displayed numbers rounded properly? +- [ ] Does the canvas/SVG fit within the container width? + +### Visual +- [ ] Dark mode test: would every element be readable on near-black? +- [ ] No hardcoded text colors in HTML (use CSS variables) +- [ ] No gradients, shadows, blur, or glow +- [ ] Borders are 0.5px (except 2px for featured item accent) +- [ ] Font weights are only 400 or 500 +- [ ] All text is sentence case + +### Content +- [ ] Explanatory text is in the response, not inside the widget +- [ ] No titles or headings embedded in the HTML output +- [ ] Visual is self-explanatory without reading the narration +- [ ] Narration adds value beyond what the visual shows +- [ ] Offered a clear "go deeper" path + +### Accessibility +- [ ] `@media (prefers-reduced-motion: reduce)` for all animations +- [ ] Text contrast is sufficient (dark text on light fills, vice versa) +- [ ] Interactive elements are large enough to click (min 44px touch target) +- [ ] No information conveyed by color alone + +--- + +## Part 9: Decision Matrix — Picking the Right Visual + +| User asks about... | Output type | Technology | +|-----------------------------|--------------------------|---------------------| +| How X works (physical) | Illustrative diagram | SVG | +| How X works (abstract) | Interactive explainer | HTML + inline SVG | +| Process / steps | Flowchart | SVG | +| Architecture / containment | Structural diagram | SVG | +| Database schema / ERD | Relationship diagram | Mermaid | +| Trends over time | Line chart | Chart.js | +| Category comparison | Bar chart | Chart.js | +| Part of whole | Doughnut chart | Chart.js | +| KPIs / metrics | Dashboard | HTML metric cards | +| Design a UI | Mockup | HTML | +| Choose between options | Comparison cards | HTML grid | +| Cyclic process | Step-through | HTML stepper | +| Physics / math | Simulation | Canvas + JS | +| Function / equation | Plotter | SVG + JS | +| Data exploration | Sortable table | HTML + JS | +| Creative / decorative | Art / illustration | SVG | +| 3D visualization | 3D scene | Three.js | +| Music / audio | Synthesizer | Tone.js | +| Network / graph | Force layout | D3.js | +| Quick factual answer | Plain text | None | +| Code solution | Code block | None | +| Emotional support | Warm text | None | diff --git a/apps/mcp/skills/master-agent-playbook.txt b/apps/mcp/skills/master-agent-playbook.txt new file mode 100644 index 0000000..e27ff25 --- /dev/null +++ b/apps/mcp/skills/master-agent-playbook.txt @@ -0,0 +1,432 @@ +# Master Agent Playbook: Making AI Responses Extraordinary + +This playbook teaches an AI coding agent how to go beyond plain text and deliver +responses that are visual, interactive, and deeply educational. It covers the +philosophy, decision-making, and technical skills needed. + +--- + +## Part 1: The Core Philosophy + +### Think Like a Teacher, Not a Search Engine + +Bad: "A load path is the route that forces take through a structure to the ground." +Good: [draws an interactive building cross-section with loads flowing downward] + +The principle: **Show, don't just tell.** Before writing any response, ask: +- Would a diagram make this click faster than a paragraph? +- Would an interactive widget let the user explore the concept themselves? +- Would a worked example teach better than a definition? + +### The Response Decision Tree + +``` +User asks a question + │ + ├─ Is it a quick factual answer? → Answer in 1-2 sentences. + │ + ├─ Is it conceptual / "how does X work"? + │ ├─ Is it spatial or visual? → SVG illustrative diagram + │ ├─ Is it a process/flow? → SVG flowchart or HTML stepper + │ ├─ Is it data-driven? → Interactive chart (Chart.js / Recharts) + │ └─ Is it abstract but explorable? → Interactive HTML widget with controls + │ + ├─ Is it "build me X"? → Working code artifact, fully functional + │ + ├─ Is it a comparison? → Side-by-side table or comparative visual + │ + └─ Is it emotional/personal? → Warm text response. No visuals needed. +``` + +### The 3-Layer Response Pattern + +Great responses layer information: + +1. **Hook** (1-2 sentences): Validate the question, set context. +2. **Visual** (diagram/widget): The core explanation, rendered visually. +3. **Narration** (2-4 paragraphs): Walk through the visual, add nuance, + connect to what the user already knows. Offer to go deeper. + +Never dump a visual without narration. Never narrate without visuals +when visuals would help. + +--- + +## Part 2: Skill — Interactive HTML Widgets + +For concepts that benefit from user exploration. More powerful than +static SVGs — users can manipulate parameters and see results. + +### When to Use +- The concept has a variable the user could tweak (temperature, rate, count) +- The system has states the user could toggle (on/off, mode A/B) +- The explanation benefits from stepping through stages +- Data exploration or filtering is involved + +### Template: Interactive Widget with Controls + +```html + + + + + + + + + +
+ +
+ + +``` + +### Template: Step-Through Explainer + +For cyclic or staged processes (event loops, biological cycles, pipelines). + +```html + + +
+ +
+ +
+ +
+ + Step 1 of 4 +
+ + +``` + +### CSS Animation Patterns (for live diagrams) + +```css +/* Flowing particles along a path */ +@keyframes flow { + to { stroke-dashoffset: -20; } +} +.flowing { + stroke-dasharray: 5 5; + animation: flow 1.6s linear infinite; +} + +/* Pulsing glow for active elements */ +@keyframes pulse { + 0%, 100% { opacity: 0.3; } + 50% { opacity: 0.7; } +} +.pulsing { animation: pulse 2s ease-in-out infinite; } + +/* Flickering (for flames, sparks) */ +@keyframes flicker { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.8; } +} + +/* Always respect reduced motion preferences */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + } +} +``` + +--- + +## Part 3: Skill — Data Visualization + +### When to Use +- Comparing quantities +- Showing trends over time +- Displaying distributions or proportions +- Making data explorable + +### Approach: Inline SVG Charts (No Dependencies) + +For simple charts, hand-draw in SVG. No library needed. + +```svg + + + + + + + + + + Q1 + $42k + + +``` + +### Approach: Chart.js (For Complex/Interactive Charts) + +When you need tooltips, responsive legends, animations: + +```html + + + +``` + +--- + +## Part 4: Skill — Mermaid Diagrams + +For relationship diagrams (ERDs, class diagrams, sequence diagrams) where +precise layout math isn't worth doing by hand. + +```html +
+ +``` + +Use Mermaid for: ERDs, class diagrams, sequence diagrams, Gantt charts. +Use hand-drawn SVG for: everything else (flowcharts, architecture, +illustrative diagrams) — you get much better control. + +--- + +## Part 5: Skill — Explanatory Writing Between Visuals + +### Narration Patterns + +**The Walk-Through**: Point at parts of the visual and explain them. +> "Starting at the top, the roof deck collects distributed loads across +> its surface. These get channeled into the rafters below, which act +> like one-way bridges..." + +**The "Why It Matters"**: Connect the visual to real consequences. +> "This is why lower columns are always larger — they're carrying the +> accumulated weight of every floor above." + +**The "Common Mistake"**: Anticipate misconceptions. +> "One thing that trips people up: removing a single column doesn't just +> lose that member — it breaks the entire load chain." + +**The "Go Deeper" Offer**: End with expansion paths. +> "Want me to show how lateral loads (wind, seismic) take a completely +> different path?" + +### Tone Rules +- Warm and direct. Not academic, not dumbed-down. +- Use "you" and "we" freely. +- Analogies and metaphors are powerful. Use them. +- Short paragraphs (2-4 sentences). No walls of text. +- Bold key terms on first introduction, then don't re-bold. +- Never use bullet points for explanations. Prose only. +- Ask at most one question per response. + +--- + +## Part 6: Skill — Knowing What NOT to Visualize + +Not everything needs a diagram. Skip visuals when: + +- The answer is a single fact or number +- The user is venting or emotional (empathy, not charts) +- The topic is purely textual (writing, editing, drafting) +- A code snippet is the answer (just show the code) +- The user explicitly asked for brief/concise + +### The "Would They Screenshot This?" Test + +If the user would likely screenshot or save the visual to reference +later, it was worth making. If not, just use text. + +--- + +## Part 7: Putting It All Together + +### Example Response Structure (Complex Technical Question) + +``` +[1-2 sentence hook validating the question] + +[Visual: SVG diagram or interactive widget] + +[Walk-through narration: 3-4 paragraphs explaining the visual, + pointing at specific parts, noting key insights] + +[One "go deeper" offer with 2-3 specific directions] +``` + +### Example Response Structure (Simple Question with Visual Aid) + +``` +[Direct answer in 1-2 sentences] + +[Small supporting visual if it adds value] + +[One additional insight or context sentence] +``` + +### Quality Checklist Before Responding + +- [ ] Did I pick the right format? (text vs SVG vs interactive vs chart) +- [ ] Is the visual self-explanatory even without the narration? +- [ ] Does the narration add value beyond what the visual shows? +- [ ] Are colors meaningful, not decorative? +- [ ] Does it work in dark mode? +- [ ] Is the response concise? (Cut anything that doesn't teach) +- [ ] Did I offer a clear next step? + +--- + +## Appendix: Quick Reference + +| Concept Type | Best Format | +|-------------------------|------------------------------| +| How X works (physical) | Illustrative SVG diagram | +| How X works (abstract) | Interactive HTML + SVG | +| Process / workflow | SVG flowchart | +| Architecture | SVG structural diagram | +| Data relationships | Mermaid ERD | +| Trends / comparisons | Chart.js or SVG bar chart | +| Cyclic process | HTML step-through widget | +| System states | Interactive widget + toggles | +| Quick answer | Plain text | +| Code solution | Code block / artifact | +| Emotional support | Warm text only | diff --git a/apps/mcp/skills/svg-diagram-skill.txt b/apps/mcp/skills/svg-diagram-skill.txt new file mode 100644 index 0000000..2a28385 --- /dev/null +++ b/apps/mcp/skills/svg-diagram-skill.txt @@ -0,0 +1,245 @@ +# SVG Diagram Generation Skill + +You can generate rich, inline SVG diagrams to visually explain concepts. Use this skill whenever a visual would help the user understand a system, process, architecture, or mechanism better than text alone. + +--- + +## When to Use + +- Explaining how something works (load paths, circuits, pipelines, algorithms) +- Showing architecture or structure (system diagrams, component layouts) +- Illustrating processes or flows (flowcharts, data flow, decision trees) +- Building intuition for abstract concepts (attention mechanisms, gradient descent, recursion) + +## SVG Setup + +Always use this template: + +```svg + + + + + + + + +``` + +- **Width is always 680px** via viewBox. Set `width="100%"` so it scales responsively. +- **H (height)** = bottom-most element's y + height + 40px padding. Don't guess — compute it. +- **Safe content area**: x=40 to x=640, y=40 to y=(H-40). +- **No wrapping divs**, no ``, ``, ``, or DOCTYPE. +- **Background is transparent** — the host provides the background. + +--- + +## Core Design Rules + +### Typography +- **Two sizes only**: 14px for titles/labels, 12px for subtitles/descriptions. +- **Two weights only**: 400 (regular), 500 (medium/bold). Never use 600 or 700. +- **Font**: Use `font-family="system-ui, -apple-system, sans-serif"` or inherit from host. +- **Always set** `text-anchor="middle"` and `dominant-baseline="central"` for centered text in boxes. +- **Sentence case always**. Never Title Case or ALL CAPS. + +### Text Width Estimation +At 14px, each character ≈ 8px wide. At 12px, each character ≈ 7px wide. +- "Load Balancer" (13 chars) at 14px ≈ 104px → needs rect ≈ 140px wide (with padding). +- Always compute: `rect_width = max(title_chars × 8, subtitle_chars × 7) + 48px padding`. + +### Colors (Light/Dark Mode Safe) +Use these semantic color sets that work in both modes: + +``` +Teal: fill="#E1F5EE" stroke="#0F6E56" text="#085041" (dark: fill="#085041" stroke="#5DCAA5" text="#9FE1CB") +Purple: fill="#EEEDFE" stroke="#534AB7" text="#3C3489" (dark: fill="#3C3489" stroke="#AFA9EC" text="#CECBF6") +Coral: fill="#FAECE7" stroke="#993C1D" text="#712B13" (dark: fill="#712B13" stroke="#F0997B" text="#F5C4B3") +Amber: fill="#FAEEDA" stroke="#854F0B" text="#633806" (dark: fill="#633806" stroke="#EF9F27" text="#FAC775") +Blue: fill="#E6F1FB" stroke="#185FA5" text="#0C447C" (dark: fill="#0C447C" stroke="#85B7EB" text="#B5D4F4") +Gray: fill="#F1EFE8" stroke="#5F5E5A" text="#444441" (dark: fill="#444441" stroke="#B4B2A9" text="#D3D1C7") +Red: fill="#FCEBEB" stroke="#A32D2D" text="#791F1F" (dark: fill="#791F1F" stroke="#F09595" text="#F7C1C1") +Green: fill="#EAF3DE" stroke="#3B6D11" text="#27500A" (dark: fill="#27500A" stroke="#97C459" text="#C0DD97") +Pink: fill="#FBEAF0" stroke="#993556" text="#72243E" (dark: fill="#72243E" stroke="#ED93B1" text="#F4C0D1") +``` + +**Color meaning, not sequence**: Don't rainbow-cycle. Use 2-3 colors per diagram. Map colors to categories or physical properties (warm = heat/energy, cool = calm/cold, gray = structural/neutral). + +If you're rendering inside a system that supports CSS variables, prefer: +- `var(--color-text-primary)` for primary text +- `var(--color-text-secondary)` for muted text +- `var(--color-border-tertiary)` for light borders + +### Shapes & Layout +- **Stroke width**: 0.5px for borders, 1.5px for arrows/connectors. +- **Corner radius**: `rx="4"` for subtle rounding, `rx="8"` for emphasized. `rx="20"` for large containers. +- **Spacing**: 60px minimum between boxes, 24px padding inside boxes, 12px text-to-edge clearance. +- **Single-line box**: 44px tall. **Two-line box**: 56px tall. +- **Max 4-5 nodes per row** at 680px width. If more, split into multiple diagrams. +- **All connectors need `fill="none"`** — SVG defaults fill to black, which turns paths into black blobs. + +--- + +## Component Patterns + +### Single-Line Node +```svg + + + Node title + +``` + +### Two-Line Node +```svg + + + Title + Short subtitle + +``` + +### Arrow Connector +```svg + +``` + +### Dashed Flow Indicator +```svg + +``` + +### Leader Line with Label (for annotations) +```svg + + +Annotation text +``` + +### Large Container (for structural diagrams) +```svg + +Container name +``` + +--- + +## Diagram Types & When to Use Each + +### 1. Flowchart +**When**: Sequential processes, decision trees, pipelines. +**Layout**: Top-to-bottom or left-to-right. Single direction only. +**Rules**: +- Arrows must never cross unrelated boxes. Route around with L-bends if needed. +- Keep all same-type boxes the same height. +- Max 4-5 nodes per diagram. Break complex flows into multiple diagrams. + +### 2. Structural Diagram +**When**: Containment matters — things inside other things (architecture, org charts, system components). +**Layout**: Nested rectangles. Outer = container, inner = regions. +**Rules**: +- Max 2-3 nesting levels. +- 20px minimum padding inside every container. +- Use different color ramps for parent vs child to show hierarchy. + +### 3. Illustrative Diagram +**When**: Building intuition. "How does X actually work?" +**Layout**: Freeform — follows the subject's natural geometry. +**Rules**: +- Shapes can be freeform (paths, ellipses, polygons), not just rects. +- Color encodes intensity, not category (warm = active, cool = dormant). +- Overlap shapes for depth, but never let strokes cross text. +- Labels go in margins with leader lines pointing to the relevant part. + +--- + +## Critical Checks Before Finalizing + +1. **ViewBox height**: Find your lowest element (max y + height). Set H = that + 40px. +2. **No content past x=640 or below y=(H-40)**. +3. **Text fits in boxes**: `(char_count × 8) + 48 < rect_width` for 14px text. +4. **No arrows through boxes**: Trace every line's path — if it crosses a rect, reroute. +5. **All `` connectors have `fill="none"`**. +6. **All text has appropriate fill color** — never rely on inheritance (SVG defaults to black). +7. **Colors work in dark mode**: If using hardcoded colors, provide both light and dark variants. If using CSS variables, you're fine. + +--- + +## Multi-Diagram Approach + +For complex topics, use multiple smaller SVGs instead of one dense one: +- Each SVG should have 3-5 nodes max. +- Write explanatory text between diagrams. +- First diagram = overview, subsequent = zoom into subsections. +- Never promise diagrams you don't deliver. + +--- + +## Example: Simple 3-Step Flow + +```svg + + + + + + + + + + User request + HTTP POST /api/data + + + + + + + Server processing + Validate and transform + + + + + + + Database write + INSERT into table + +``` + +--- + +## Tips for Great Diagrams + +- **Less is more**: A clean 4-node diagram teaches better than a cramped 12-node one. +- **Color = meaning**: Warm colors for active/hot/important, cool for passive/cold/secondary, gray for structural. +- **Streaming effect**: Since SVGs render top-to-bottom as tokens arrive, structure your elements top-down for a natural build-up animation. +- **Annotations on the side**: Put explanatory labels in the right margin (x > 560) with leader lines pointing to the relevant element. +- **Consistent heights**: All boxes of the same type should be the same height. +- **Whitespace is your friend**: Don't fill every pixel. Breathing room makes diagrams readable. diff --git a/apps/mcp/src/index.ts b/apps/mcp/src/index.ts new file mode 100644 index 0000000..8e74307 --- /dev/null +++ b/apps/mcp/src/index.ts @@ -0,0 +1,59 @@ +import { serve } from "@hono/node-server"; +import { Hono } from "hono"; +import { cors } from "hono/cors"; +import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js"; +import { createMcpServer } from "./server.js"; + +const PORT = Number(process.env.MCP_PORT) || 3100; +const ALLOWED_ORIGINS = process.env.ALLOWED_ORIGINS?.split(",") || ["*"]; + +const server = createMcpServer(); +const sessions = new Map(); + +const app = new Hono(); + +app.use( + "*", + cors({ + origin: + ALLOWED_ORIGINS.length === 1 && ALLOWED_ORIGINS[0] === "*" + ? "*" + : ALLOWED_ORIGINS, + allowMethods: ["GET", "POST", "DELETE", "OPTIONS"], + allowHeaders: [ + "Content-Type", + "mcp-session-id", + "Last-Event-ID", + "mcp-protocol-version", + ], + exposeHeaders: ["mcp-session-id", "mcp-protocol-version"], + }) +); + +app.get("/health", (c) => c.json({ status: "ok" })); + +app.all("/mcp", async (c) => { + const sessionId = c.req.header("mcp-session-id"); + + if (sessionId && sessions.has(sessionId)) { + return sessions.get(sessionId)!.handleRequest(c.req.raw); + } + + const transport = new WebStandardStreamableHTTPServerTransport(); + await server.connect(transport); + + const response = await transport.handleRequest(c.req.raw); + + const newSessionId = response.headers.get("mcp-session-id"); + if (newSessionId) { + sessions.set(newSessionId, transport); + transport.onclose = () => { + sessions.delete(newSessionId); + }; + } + + return response; +}); + +serve({ fetch: app.fetch, port: PORT }); +console.log(`MCP server running on http://localhost:${PORT}/mcp`); diff --git a/apps/mcp/src/renderer.ts b/apps/mcp/src/renderer.ts new file mode 100644 index 0000000..8ae8238 --- /dev/null +++ b/apps/mcp/src/renderer.ts @@ -0,0 +1,351 @@ +// OpenGenerativeUI Design System CSS and Bridge JS +// Forked from apps/app/src/components/generative-ui/widget-renderer.tsx +// WARNING: Keep in sync with the source widget-renderer.tsx when the design system changes. + +const THEME_CSS = ` +:root { + --color-background-primary: #ffffff; + --color-background-secondary: #f7f6f3; + --color-background-tertiary: #efeee9; + --color-background-info: #E6F1FB; + --color-background-danger: #FCEBEB; + --color-background-success: #EAF3DE; + --color-background-warning: #FAEEDA; + + --color-text-primary: #1a1a1a; + --color-text-secondary: #73726c; + --color-text-tertiary: #9c9a92; + --color-text-info: #185FA5; + --color-text-danger: #A32D2D; + --color-text-success: #3B6D11; + --color-text-warning: #854F0B; + + --color-border-primary: rgba(0, 0, 0, 0.4); + --color-border-secondary: rgba(0, 0, 0, 0.3); + --color-border-tertiary: rgba(0, 0, 0, 0.15); + --color-border-info: #185FA5; + --color-border-danger: #A32D2D; + --color-border-success: #3B6D11; + --color-border-warning: #854F0B; + + --font-sans: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; + --font-serif: Georgia, "Times New Roman", serif; + --font-mono: "SF Mono", "Fira Code", "Fira Mono", monospace; + + --border-radius-md: 8px; + --border-radius-lg: 12px; + --border-radius-xl: 16px; + + --p: var(--color-text-primary); + --s: var(--color-text-secondary); + --t: var(--color-text-tertiary); + --bg2: var(--color-background-secondary); + --b: var(--color-border-tertiary); +} + +@media (prefers-color-scheme: dark) { + :root { + --color-background-primary: #1a1a18; + --color-background-secondary: #2c2c2a; + --color-background-tertiary: #222220; + --color-background-info: #0C447C; + --color-background-danger: #501313; + --color-background-success: #173404; + --color-background-warning: #412402; + + --color-text-primary: #e8e6de; + --color-text-secondary: #9c9a92; + --color-text-tertiary: #73726c; + --color-text-info: #85B7EB; + --color-text-danger: #F09595; + --color-text-success: #97C459; + --color-text-warning: #EF9F27; + + --color-border-primary: rgba(255, 255, 255, 0.4); + --color-border-secondary: rgba(255, 255, 255, 0.3); + --color-border-tertiary: rgba(255, 255, 255, 0.15); + --color-border-info: #85B7EB; + --color-border-danger: #F09595; + --color-border-success: #97C459; + --color-border-warning: #EF9F27; + } +} +`; + +const SVG_CLASSES_CSS = ` +svg text.t { font: 400 14px var(--font-sans); fill: var(--p); } +svg text.ts { font: 400 12px var(--font-sans); fill: var(--s); } +svg text.th { font: 500 14px var(--font-sans); fill: var(--p); } + +svg .box > rect, svg .box > circle, svg .box > ellipse { fill: var(--bg2); stroke: var(--b); } +svg .node { cursor: pointer; } +svg .node:hover { opacity: 0.8; } +svg .arr { stroke: var(--s); stroke-width: 1.5; fill: none; } +svg .leader { stroke: var(--t); stroke-width: 0.5; stroke-dasharray: 4 4; fill: none; } + +/* Purple */ +svg .c-purple > rect, svg .c-purple > circle, svg .c-purple > ellipse, +svg rect.c-purple, svg circle.c-purple, svg ellipse.c-purple { fill: #EEEDFE; stroke: #534AB7; } +svg .c-purple text.th, svg .c-purple text.t { fill: #3C3489; } +svg .c-purple text.ts { fill: #534AB7; } + +/* Teal */ +svg .c-teal > rect, svg .c-teal > circle, svg .c-teal > ellipse, +svg rect.c-teal, svg circle.c-teal, svg ellipse.c-teal { fill: #E1F5EE; stroke: #0F6E56; } +svg .c-teal text.th, svg .c-teal text.t { fill: #085041; } +svg .c-teal text.ts { fill: #0F6E56; } + +/* Coral */ +svg .c-coral > rect, svg .c-coral > circle, svg .c-coral > ellipse, +svg rect.c-coral, svg circle.c-coral, svg ellipse.c-coral { fill: #FAECE7; stroke: #993C1D; } +svg .c-coral text.th, svg .c-coral text.t { fill: #712B13; } +svg .c-coral text.ts { fill: #993C1D; } + +/* Pink */ +svg .c-pink > rect, svg .c-pink > circle, svg .c-pink > ellipse, +svg rect.c-pink, svg circle.c-pink, svg ellipse.c-pink { fill: #FBEAF0; stroke: #993556; } +svg .c-pink text.th, svg .c-pink text.t { fill: #72243E; } +svg .c-pink text.ts { fill: #993556; } + +/* Gray */ +svg .c-gray > rect, svg .c-gray > circle, svg .c-gray > ellipse, +svg rect.c-gray, svg circle.c-gray, svg ellipse.c-gray { fill: #F1EFE8; stroke: #5F5E5A; } +svg .c-gray text.th, svg .c-gray text.t { fill: #444441; } +svg .c-gray text.ts { fill: #5F5E5A; } + +/* Blue */ +svg .c-blue > rect, svg .c-blue > circle, svg .c-blue > ellipse, +svg rect.c-blue, svg circle.c-blue, svg ellipse.c-blue { fill: #E6F1FB; stroke: #185FA5; } +svg .c-blue text.th, svg .c-blue text.t { fill: #0C447C; } +svg .c-blue text.ts { fill: #185FA5; } + +/* Green */ +svg .c-green > rect, svg .c-green > circle, svg .c-green > ellipse, +svg rect.c-green, svg circle.c-green, svg ellipse.c-green { fill: #EAF3DE; stroke: #3B6D11; } +svg .c-green text.th, svg .c-green text.t { fill: #27500A; } +svg .c-green text.ts { fill: #3B6D11; } + +/* Amber */ +svg .c-amber > rect, svg .c-amber > circle, svg .c-amber > ellipse, +svg rect.c-amber, svg circle.c-amber, svg ellipse.c-amber { fill: #FAEEDA; stroke: #854F0B; } +svg .c-amber text.th, svg .c-amber text.t { fill: #633806; } +svg .c-amber text.ts { fill: #854F0B; } + +/* Red */ +svg .c-red > rect, svg .c-red > circle, svg .c-red > ellipse, +svg rect.c-red, svg circle.c-red, svg ellipse.c-red { fill: #FCEBEB; stroke: #A32D2D; } +svg .c-red text.th, svg .c-red text.t { fill: #791F1F; } +svg .c-red text.ts { fill: #A32D2D; } + +@media (prefers-color-scheme: dark) { + svg text.t { fill: #e8e6de; } + svg text.ts { fill: #9c9a92; } + svg text.th { fill: #e8e6de; } + + svg .c-purple > rect, svg .c-purple > circle, svg .c-purple > ellipse, + svg rect.c-purple, svg circle.c-purple, svg ellipse.c-purple { fill: #3C3489; stroke: #AFA9EC; } + svg .c-purple text.th, svg .c-purple text.t { fill: #CECBF6; } + svg .c-purple text.ts { fill: #AFA9EC; } + + svg .c-teal > rect, svg .c-teal > circle, svg .c-teal > ellipse, + svg rect.c-teal, svg circle.c-teal, svg ellipse.c-teal { fill: #085041; stroke: #5DCAA5; } + svg .c-teal text.th, svg .c-teal text.t { fill: #9FE1CB; } + svg .c-teal text.ts { fill: #5DCAA5; } + + svg .c-coral > rect, svg .c-coral > circle, svg .c-coral > ellipse, + svg rect.c-coral, svg circle.c-coral, svg ellipse.c-coral { fill: #712B13; stroke: #F0997B; } + svg .c-coral text.th, svg .c-coral text.t { fill: #F5C4B3; } + svg .c-coral text.ts { fill: #F0997B; } + + svg .c-pink > rect, svg .c-pink > circle, svg .c-pink > ellipse, + svg rect.c-pink, svg circle.c-pink, svg ellipse.c-pink { fill: #72243E; stroke: #ED93B1; } + svg .c-pink text.th, svg .c-pink text.t { fill: #F4C0D1; } + svg .c-pink text.ts { fill: #ED93B1; } + + svg .c-gray > rect, svg .c-gray > circle, svg .c-gray > ellipse, + svg rect.c-gray, svg circle.c-gray, svg ellipse.c-gray { fill: #444441; stroke: #B4B2A9; } + svg .c-gray text.th, svg .c-gray text.t { fill: #D3D1C7; } + svg .c-gray text.ts { fill: #B4B2A9; } + + svg .c-blue > rect, svg .c-blue > circle, svg .c-blue > ellipse, + svg rect.c-blue, svg circle.c-blue, svg ellipse.c-blue { fill: #0C447C; stroke: #85B7EB; } + svg .c-blue text.th, svg .c-blue text.t { fill: #B5D4F4; } + svg .c-blue text.ts { fill: #85B7EB; } + + svg .c-green > rect, svg .c-green > circle, svg .c-green > ellipse, + svg rect.c-green, svg circle.c-green, svg ellipse.c-green { fill: #27500A; stroke: #97C459; } + svg .c-green text.th, svg .c-green text.t { fill: #C0DD97; } + svg .c-green text.ts { fill: #97C459; } + + svg .c-amber > rect, svg .c-amber > circle, svg .c-amber > ellipse, + svg rect.c-amber, svg circle.c-amber, svg ellipse.c-amber { fill: #633806; stroke: #EF9F27; } + svg .c-amber text.th, svg .c-amber text.t { fill: #FAC775; } + svg .c-amber text.ts { fill: #EF9F27; } + + svg .c-red > rect, svg .c-red > circle, svg .c-red > ellipse, + svg rect.c-red, svg circle.c-red, svg ellipse.c-red { fill: #791F1F; stroke: #F09595; } + svg .c-red text.th, svg .c-red text.t { fill: #F7C1C1; } + svg .c-red text.ts { fill: #F09595; } +} +`; + +const FORM_STYLES_CSS = ` +* { box-sizing: border-box; margin: 0; } +html { background: transparent; } +body { + font-family: var(--font-sans); + font-size: 16px; + line-height: 1.7; + color: var(--color-text-primary); + background: transparent; + -webkit-font-smoothing: antialiased; +} +button { + font-family: inherit; + font-size: 14px; + padding: 6px 16px; + border: 0.5px solid var(--color-border-secondary); + border-radius: var(--border-radius-md); + background: transparent; + color: var(--color-text-primary); + cursor: pointer; + transition: background 0.15s, transform 0.1s; +} +button:hover { background: var(--color-background-secondary); } +button:active { transform: scale(0.98); } +input[type="text"], +input[type="number"], +input[type="email"], +input[type="search"], +textarea, +select { + font-family: inherit; + font-size: 14px; + padding: 6px 12px; + height: 36px; + border: 0.5px solid var(--color-border-tertiary); + border-radius: var(--border-radius-md); + background: var(--color-background-primary); + color: var(--color-text-primary); + transition: border-color 0.15s; +} +input:hover, textarea:hover, select:hover { border-color: var(--color-border-secondary); } +input:focus, textarea:focus, select:focus { + outline: none; + border-color: var(--color-border-primary); + box-shadow: 0 0 0 3px rgba(0, 0, 0, 0.06); +} +textarea { height: auto; min-height: 80px; resize: vertical; } +input::placeholder, textarea::placeholder { color: var(--color-text-tertiary); } +input[type="range"] { + -webkit-appearance: none; + appearance: none; + height: 4px; + background: var(--color-border-tertiary); + border-radius: 2px; + border: none; + outline: none; +} +input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + width: 18px; height: 18px; + border-radius: 50%; + background: var(--color-background-primary); + border: 0.5px solid var(--color-border-secondary); + cursor: pointer; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} +input[type="range"]::-moz-range-thumb { + width: 18px; height: 18px; + border-radius: 50%; + background: var(--color-background-primary); + border: 0.5px solid var(--color-border-secondary); + cursor: pointer; +} +input[type="checkbox"], input[type="radio"] { + width: 16px; height: 16px; + accent-color: var(--color-text-info); +} +a { color: var(--color-text-info); text-decoration: none; } +a:hover { text-decoration: underline; } +#content > * { + animation: fadeSlideIn 0.4s ease-out both; +} +#content > *:nth-child(1) { animation-delay: 0s; } +#content > *:nth-child(2) { animation-delay: 0.06s; } +#content > *:nth-child(3) { animation-delay: 0.12s; } +#content > *:nth-child(4) { animation-delay: 0.18s; } +#content > *:nth-child(5) { animation-delay: 0.24s; } +#content > *:nth-child(n+6) { animation-delay: 0.3s; } +@keyframes fadeSlideIn { + from { opacity: 0; transform: translateY(8px); } + to { opacity: 1; transform: translateY(0); } +} +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + } +} +`; + +const BRIDGE_JS = ` +window.sendPrompt = function(text) { + window.parent.postMessage({ type: 'send-prompt', text: text }, '*'); +}; +window.openLink = function(url) { + window.parent.postMessage({ type: 'open-link', url: url }, '*'); +}; +document.addEventListener('click', function(e) { + var a = e.target.closest('a[href]'); + if (a && a.href.startsWith('http')) { + e.preventDefault(); + window.parent.postMessage({ type: 'open-link', url: a.href }, '*'); + } +}); +function reportHeight() { + var content = document.getElementById('content'); + var h = content ? content.offsetHeight : document.documentElement.scrollHeight; + window.parent.postMessage({ type: 'widget-resize', height: h }, '*'); +} +var ro = new ResizeObserver(reportHeight); +ro.observe(document.getElementById('content') || document.body); +window.addEventListener('load', reportHeight); +var _resizeInterval = setInterval(reportHeight, 200); +setTimeout(function() { clearInterval(_resizeInterval); }, 15000); +`; + +export function assembleDocument(html: string): string { + return ` + + + + + + + + +
+ ${html} +
+ + +`; +} diff --git a/apps/mcp/src/server.ts b/apps/mcp/src/server.ts new file mode 100644 index 0000000..b7328ae --- /dev/null +++ b/apps/mcp/src/server.ts @@ -0,0 +1,110 @@ +import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"; +import * as z from "zod"; +import { listSkills, loadSkill } from "./skills.js"; +import { assembleDocument } from "./renderer.js"; + +export function createMcpServer(): McpServer { + const server = new McpServer({ + name: "open-generative-ui", + version: "0.1.0", + }); + + // Resources: list and get skills + server.registerResource( + "skills-list", + "skills://list", + { description: "JSON array of available skill names", mimeType: "application/json" }, + async () => ({ + contents: [ + { + uri: "skills://list", + mimeType: "application/json", + text: JSON.stringify(listSkills()), + }, + ], + }) + ); + + server.registerResource( + "skill", + new ResourceTemplate("skills://{name}", { + list: async () => ({ + resources: listSkills().map((name) => ({ + uri: `skills://${name}`, + name, + mimeType: "text/plain", + })), + }), + }), + { description: "Full text of a skill instruction document", mimeType: "text/plain" }, + async (uri, { name }) => ({ + contents: [ + { + uri: uri.href, + mimeType: "text/plain", + text: loadSkill(name as string), + }, + ], + }) + ); + + // Prompts: pre-composed skill prompts + server.registerPrompt( + "create_widget", + { description: "Instructions for creating interactive HTML widgets" }, + async () => ({ + messages: [ + { + role: "user", + content: { type: "text", text: loadSkill("master-agent-playbook") }, + }, + ], + }) + ); + + server.registerPrompt( + "create_svg_diagram", + { description: "Instructions for creating inline SVG diagrams" }, + async () => ({ + messages: [ + { + role: "user", + content: { type: "text", text: loadSkill("svg-diagram-skill") }, + }, + ], + }) + ); + + server.registerPrompt( + "create_visualization", + { description: "Advanced visualization instructions" }, + async () => ({ + messages: [ + { + role: "user", + content: { type: "text", text: loadSkill("agent-skills-vol2") }, + }, + ], + }) + ); + + // Tool: assemble complete HTML document with design system + server.registerTool( + "assemble_document", + { + description: + "Wraps HTML with OpenGenerativeUI theme CSS, SVG classes, form styles, and bridge JS. " + + "Returns a complete iframe-ready HTML document.", + inputSchema: { + title: z.string().describe("Short title for the visualization"), + description: z.string().describe("One-sentence explanation of what this shows"), + html: z.string().describe("Self-contained HTML fragment with inline