@meissa_a/meissa
v0.1.3
Published
Meissa — token-efficient generative UI for MCP: React renderer, client SDK, server SDK, and sandboxed-iframe apps adapter
Maintainers
Readme
@meissa_a/meissa
Token-efficient generative UI for MCP, in one package with four entry points:
| Import | What it is |
| --- | --- |
| @meissa_a/meissa/renderer | React component library — renders interactive UI from declarative JSON schemas |
| @meissa_a/meissa/client | Client SDK for rendering Meissa content in MCP hosts (React) |
| @meissa_a/meissa/server | Server SDK for returning Meissa UI from MCP tools (zero runtime deps) |
| @meissa_a/meissa/apps | MCP Apps adapter — serves Meissa UI as sandboxed HTML iframes |
Define your UI as compact tuples or objects, bind live data, and let the renderer handle the rest.
Installation
# npm
npm install @meissa_a/meissa react react-dom
# pnpm
pnpm add @meissa_a/meissa react react-dom
# bun
bun add @meissa_a/meissa react react-dom
react/react-domare optional peers — only needed for therendererandcliententry points.
Requirements
- React 19+
- Tailwind CSS v4 (for styling)
Tailwind Setup
Add the renderer's source to your Tailwind config so its classes are included:
/* app.css */
@import "tailwindcss";
@source "../node_modules/@meissa_a/meissa/dist/**/*.js";Quick Start
import { MeissaRenderer } from "@meissa_a/meissa/renderer";
const schema = [
["heading", "Hello World"],
["text", "This is rendered from JSON."],
["btn", { label: "Click me", action: "greet" }],
];
function App() {
return (
<MeissaRenderer
schema={schema}
data={{ user: "Alice" }}
onAction={(name, args) => console.log(name, args)}
/>
);
}API
<MeissaRenderer>
The main entry point. Parses the schema, resolves data bindings, and renders the component tree.
<MeissaRenderer
schema={schema} // Meissa JSON schema (required)
data={data} // Data context for {{bindings}}
theme={theme} // Theme overrides
onAction={handler} // Action callback (button clicks, form submits)
onNavigate={handler} // Navigation callback
className="..." // Container className
/>| Prop | Type | Description |
|------|------|-------------|
| schema | MeissaInput | JSON schema — array of tuples, single node, or full document |
| data | Record<string, unknown> | Data context for resolving {{bindings}} |
| theme | MeissaTheme | Theme overrides (colors, radius, density, font) |
| onAction | (name: string, args?: Record<string, unknown>) => void | Called when actions fire (button clicks, form submissions) |
| onNavigate | (uri: string) => void | Called for navigation events |
| className | string | CSS class on the container element |
Schema Format
Schemas use a compact tuple syntax: [type, primaryProp?, children?]
["text", "Hello"]
["btn", { "label": "Submit", "variant": "primary" }]
["row", { "gap": 4 }, [["btn", "A"], ["btn", "B"]]]Or verbose object form:
{ "type": "text", "content": "Hello", "bold": true }Full Document
{
"v": 1,
"data": { "user": "Alice", "count": 42 },
"theme": { "radius": "md", "colors": { "primary": "#6366f1" } },
"ui": [
["heading", "Dashboard"],
["stat", { "label": "Users", "value": "{{count}}" }]
]
}Components
24 built-in components covering layout, content, data display, and interactivity:
Layout
| Component | Shorthand | Description |
|-----------|-----------|-------------|
| row | ["row", [children]] | Horizontal flex container |
| col | ["col", [children]] | Vertical flex container |
| grid | ["grid", { cols: 3 }, [children]] | CSS grid |
| split | ["split", { ratio: "1/3" }, [left, right]] | Two-panel layout |
| divider | ["divider"] or ["divider", "Section"] | Horizontal rule with optional label |
Content
| Component | Shorthand | Description |
|-----------|-----------|-------------|
| text | ["text", "Hello"] | Text content with variants |
| heading | ["heading", "Title"] | h1-h6 heading |
| code | ["code", "const x = 1"] | Code block or inline code |
| img | ["img", "url"] | Image |
| badge | ["badge", "New"] | Status indicator |
| alert | ["alert", "Warning message"] | Notification banner |
| progress | ["progress", { value: 75 }] | Progress bar |
Data Display
| Component | Shorthand | Description |
|-----------|-----------|-------------|
| stat | ["stat", { label: "Revenue", value: "{{rev}}" }] | KPI metric |
| list | ["list", ["one", "two", "three"]] | Ordered/unordered list |
| table | ["table", { data: "{{rows}}" }] | Data table with sorting & pagination |
| chart | ["chart", { data: "{{sales}}", type: "bar" }] | Recharts-powered chart |
Interactive
| Component | Shorthand | Description |
|-----------|-----------|-------------|
| btn | ["btn", "Click me"] | Button with action dispatch |
| input | ["input", { name: "email", type: "email" }] | Text input |
| select | ["select", { name: "role", options: [...] }] | Dropdown select |
| form | ["form", { submit: "save" }, [children]] | Form container |
| card | ["card", "Title", [children]] | Card container |
| tabs | ["tabs", { items: {...} }] | Tabbed panels |
| quiz | ["quiz", { question: "2+2?", choices: ["3","4"], answer: 1 }] | Self-grading question — click marks green/red, reveals the answer, locks, shows justifications |
| exam | ["exam", "Midterm", [quiz, quiz]] | Groups quiz children with a live progress/score header |
Data Binding
Use {{path}} syntax to bind data into any string prop:
["text", "Hello, {{user.name}}!"]
["stat", { "value": "{{metrics.revenue}}", "label": "Revenue" }]Filters
Chain filters with |:
["text", "{{price | currency}}"]
["text", "{{name | uppercase}}"]
["text", "{{description | truncate}}"]Built-in filters: currency, percent, number, uppercase, lowercase, truncate, relative, date, default.
Comparisons
Expressions resolve to booleans:
["text", { "content": "Premium", "if": "{{plan == 'pro'}}" }]
["alert", { "content": "Low stock", "if": "{{count < 10}}" }]Conditionals
if — Remove from DOM
["text", { "content": "Admin only", "if": "{{isAdmin}}" }]show — Toggle visibility (keeps DOM element)
["text", { "content": "Loading...", "show": "{{loading}}" }]switch — Multi-branch
["switch", "{{status}}", {
"active": ["badge", { "content": "Active", "variant": "success" }],
"inactive": ["badge", { "content": "Inactive", "variant": "default" }],
"_": ["badge", "Unknown"]
}]Iteration
Loop over arrays with each:
["each", "{{users}}", ["card", "{{$.name}}", [
["text", "{{$.email}}"]
]]]Context variables inside each:
$— current item$i— index (0-based)$first—truefor first item$last—truefor last item
Theming
Pass a theme prop or include theme in the document:
<MeissaRenderer
schema={schema}
theme={{
radius: "lg",
density: "relaxed",
font: "Inter, sans-serif",
colors: {
primary: "#6366f1",
destructive: "#ef4444",
success: "#10b981",
bg: "#ffffff",
fg: "#0f172a",
muted: "#64748b",
border: "#e2e8f0",
},
}}
/>Actions
Components dispatch actions via onAction:
<MeissaRenderer
schema={[
["btn", { "label": "Delete", "action": "deleteItem", "args": { "id": 42 } }],
["form", { "submit": "saveUser" }, [
["input", { "name": "email", "type": "email" }],
["btn", { "label": "Save", "submit": true }]
]]
]}
onAction={(action, args) => {
// action: "deleteItem" | "saveUser"
// args: { id: 42 } | { email: "..." }
}}
/>Advanced Usage
Custom Component Registry
import { COMPONENT_REGISTRY, ComponentRenderer } from "@meissa_a/meissa/renderer";
// Add a custom component
COMPONENT_REGISTRY["my-widget"] = ({ node, children }) => (
<div className="my-widget">{children}</div>
);Using Core Pipeline Directly
import { parse, normalize, resolveBindings } from "@meissa_a/meissa/renderer";
// Parse raw schema
const doc = parse(rawJson);
// Normalize a single tuple
const node = normalize(["btn", { label: "Click", variant: "ghost" }]);
// Resolve bindings manually
const value = resolveBindings("{{user.name}}", { user: { name: "Alice" } });
// → "Alice"TypeScript
All types are exported:
import type {
MeissaRendererProps,
MeissaInput,
MeissaDocument,
NormalizedNode,
MeissaTheme,
ComponentProps,
MeissaComponent,
ActionHandler,
NavigateHandler,
} from "@meissa_a/meissa/renderer";Development
# Install dependencies
bun install
# Run tests
bun test
# Type check
bun run typecheck
# Build (ESM + CJS)
bun run build
# Watch mode
bun run devLicense
MIT
