loading…
Search for a command to run...
loading…
TypeScript declarative UI component library for MCP apps. Wire-compatible with Python prefab-ui.
TypeScript declarative UI component library for MCP apps. Wire-compatible with Python prefab-ui.
CI tests @maxhealth.tech/prefab prefab-protocol TypeScript License: MIT
TypeScript declarative UI component library for MCP apps. Wire-compatible with PrefectHQ's Python prefab-ui — same $prefab v0.2 wire protocol.
Write MCP servers in TypeScript/Bun and generate the same wire format that Python servers produce. Render the output in any web app with the included vanilla DOM renderer. Full circle: server-side DSL → JSON → browser UI.
Note: This library is a superset of the Python
prefab-ui(v0.19.1). Core components and the wire protocol are identical. Chart formatting features (xAxisFormat,tooltipXFormat,tooltipXKey, per-seriestooltipFormat, dual Y-axis) are TS-only extensions — the Python lib does not yet emit them. The renderer handles both payloads seamlessly.
rx() expressions, SetState/ToggleState/AppendState actionsdisplay(), display_form(), CallTool, SendMessage built inapp() factory with dual-protocol handshake, host theme, lifecycle hooksautoTable(), autoChart(), autoForm(), autoMetrics() and moreThe renderer is vanilla DOM — no framework dependency. Drop it into any web app:
ref div<script> tagAny app that connects to MCP servers can render $prefab tool output as rich interactive UI — tables, charts, forms, badges — with zero custom code.
npm install @maxhealth.tech/prefab
# or
bun add @maxhealth.tech/prefab
import { display, autoTable, H1, Column } from '@maxhealth.tech/prefab'
async function listUsers(args: any) {
const users = await db.query('SELECT * FROM users')
return display(
Column({ children: [
H1('Users'),
autoTable(users),
]}),
{ title: 'User List' }
)
}
The auto-mount bundle handles the full lifecycle — bridge handshake,
tool-result rendering, and DOM mounting — with a single <script> tag:
<div id="root"></div>
<script src="https://cdn.jsdelivr.net/npm/@maxhealth.tech/prefab/dist/renderer.auto.min.js"></script>
Works in VS Code, Claude Desktop, ChatGPT, and any MCP Apps host.
For manual control, use renderer.min.js instead:
<script src="https://cdn.jsdelivr.net/npm/@maxhealth.tech/prefab/dist/renderer.min.js"></script>
<script>
const ui = await prefab.app();
ui.onToolInput((args) => {
// Render wire-format JSON received from the MCP host
ui.mount('#root', args);
});
</script>
Column, Row, Grid, GridItem, Container, Div, Span, Dashboard, DashboardItem, Pages, Page, Detail, MasterDetail
Heading, H1–H4, Text, P, Lead, Large, Small, Muted, BlockQuote, Label, Link, Code, Markdown, Kbd
Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter
DataTable, col, Badge, Dot, Metric, Ring, Progress, Separator, Loader, Icon
Table, TableHead, TableBody, TableFooter, TableRow, TableHeader, TableCell, TableCaption, ExpandableRow
Form, Input, Textarea, Button, ButtonGroup, Select, SelectOption, SelectGroup, SelectLabel, SelectSeparator, Checkbox, Switch, Slider, Radio, RadioGroup, Combobox, ComboboxOption, ComboboxGroup, ComboboxLabel, ComboboxSeparator, Calendar, DatePicker, Field, FieldTitle, FieldDescription, FieldContent, FieldError, ChoiceCard
Tabs, Tab, Accordion, AccordionItem, Dialog, Popover, Tooltip, HoverCard, Carousel
BarChart, LineChart, AreaChart, PieChart, RadarChart, ScatterChart, Sparkline, RadialChart, Histogram
TS-only extension — these props are not yet supported by the Python
prefab-uilibrary. The renderer gracefully ignores them if absent, so Python-generated charts still render fine.
Charts use the same pipe system as {{ value | pipe }} expressions — all formatting is declarative JSON:
| Prop | Level | Effect |
|---|---|---|
xAxisFormat |
chart | Pipe applied to x-axis tick labels |
tooltipXFormat |
chart | Pipe applied to tooltip category label |
tooltipXKey |
chart | Read tooltip label from a different data key |
tooltipFormat |
per-series | Pipe applied to that series' value in tooltip |
// Same timestamp field, two presentations:
LineChart({
data: timeseries,
xAxis: 'timestamp',
xAxisFormat: 'date', // axis: "4/25/2026"
tooltipXFormat: 'datetime', // tooltip: "4/25/2026, 2:30:00 PM"
series: [
{ dataKey: 'revenue', label: 'Revenue', tooltipFormat: 'currency' },
{ dataKey: 'growth', label: 'Growth', tooltipFormat: 'percent' },
],
})
All built-in pipes work: upper, lower, truncate, currency, percent, compact, date, time, datetime, number, round, plus custom wire pipes.
Image, Audio, Video, Embed, Svg, DropZone, Mermaid
Alert, AlertTitle, AlertDescription
ForEach, If, Elif, Else, Define, Use, Slot
Use rx() to create reactive expressions that update when state changes:
import { rx, STATE } from '@maxhealth.tech/prefab'
// Simple state reference
Text(rx('count')) // → "{{ count }}"
// Arithmetic
Text(rx('count').add(1)) // → "{{ count + 1 }}"
// Dot-path access
Text(rx('user').dot('name')) // → "{{ user.name }}"
// Direct template string
Text('Hello, {{ user.name }}!') // interpolated at render time
// Ternary
Badge(rx('status').eq('active').then('Online', 'Offline'))
// Pipes (filters)
Text(rx('amount').currency()) // → "{{ amount | currency }}"
Text(rx('items').length()) // → "{{ items | length }}"
Text(rx('name').upper().truncate(20)) // → "{{ name | upper | truncate:20 }}"
// STATE proxy (single-level shorthand: STATE.key → rx('key'))
Text(STATE.count) // → "{{ count }}"
Built-in pipes: upper, lower, capitalize, truncate, currency, number, percent, date, time, datetime, length, default, json, keys, values, first, last, find, dot, join, abs, round, compact, pluralize, selectattr, rejectattr
Type-safe reactive primitives for master-detail patterns:
import {
signal, collection, DataTable, col, Detail, MasterDetail,
Heading, Text, Badge, PrefabApp,
} from '@maxhealth.tech/prefab'
const patients = collection('patients', data, { key: 'id' })
const sel = signal('selectedPatientId', patients.firstKey())
const ref = patients.by(sel)
const app = new PrefabApp({
title: 'Patient Browser',
view: MasterDetail({
masterWidth: '350px',
children: [
DataTable({
from: patients,
selected: sel,
columns: [
col({ key: 'name', header: 'Name', format: 'humanName' }),
col('gender'),
],
}),
Detail({
of: ref,
empty: Text('Select a patient'),
children: [
Heading(ref.dot('name')),
Badge(ref.dot('gender')),
],
}),
],
}),
// state auto-collected from signal() and collection() — no manual wiring
})
signal(key, initial) — named reactive scalar, auto-registers statecollection(key, rows, { key }) — named keyed array with O(1) lookupref.dot(field) — typed property access (Ref<T[K]>)ref.formatted(field, pipe) — dot + pipe shorthand for codegenPrefabApp gathers state from signal/collection factoriesExtend the pipe system for domain-specific formatting:
import { registerPipe } from '@maxhealth.tech/prefab'
registerPipe('humanName', (names) => {
const hn = (names as { family: string; given: string[] }[])[0]
return hn ? `${hn.family}, ${hn.given.join(' ')}` : ''
})
registerPipe('quantity', (v, unit) => `${v} ${unit ?? ''}`)
// Now usable in expressions: {{ patient.name | humanName }}
// And in col descriptors: col({ key: 'name', format: 'humanName' })
Built-in pipes always take precedence. Re-registration warns and overwrites (HMR-friendly).
Actions are triggered by user interactions (onClick, onChange, onSubmit) or lifecycle events (onMount):
import { SetState, ToggleState, CallTool, ShowToast, OpenLink, rx } from '@maxhealth.tech/prefab'
// Client-side state mutation
Button('Increment', { onClick: new SetState('count', rx('count').add(1)) })
// Toggle boolean
Button('Toggle', { onClick: new ToggleState('expanded') })
// MCP tool call
Button('Refresh', { onClick: new CallTool('get_data', { arguments: { id: '{{ selectedId }}' } }) })
// Toast notification
Button('Save', { onClick: new ShowToast('Saved!', { variant: 'success' }) })
Client actions: SetState, ToggleState, AppendState, PopState, ShowToast, CloseOverlay, OpenLink, SetInterval, Fetch, OpenFilePicker, CallHandler
MCP actions: CallTool, SendMessage, UpdateContext, RequestDisplayMode
Generate complete UIs from raw data — no manual component wiring:
import { autoTable, autoChart, autoForm, autoMetrics } from '@maxhealth.tech/prefab'
// Table from array of objects
autoTable(users, { title: 'Users', search: true })
// Chart from data + series definitions
autoChart(
salesData,
[{ dataKey: 'revenue', label: 'Revenue', color: '#3b82f6' }],
{ title: 'Revenue', chartType: 'bar', xAxis: 'month' },
)
// Form that submits to an MCP tool
autoForm(
[
{ name: 'email', type: 'email', required: true },
{ name: 'name', label: 'Full Name', required: true },
],
'create_user',
{ title: 'New User', submitLabel: 'Create' },
)
// KPI metric cards
autoMetrics([
{ label: 'Revenue', value: '$42K', delta: '+12%', trend: 'up', trendSentiment: 'positive' },
{ label: 'Users', value: '3,420', delta: '+5%', trend: 'up', trendSentiment: 'positive' },
])
Auto-renderers: autoDetail, autoTable, autoChart, autoForm, autoComparison, autoMetrics, autoTimeline, autoProgress
Return UIs from MCP tool handlers:
import { display, display_form, display_update, display_error } from '@maxhealth.tech/prefab'
import { Column, H1 } from '@maxhealth.tech/prefab'
// Full UI
return display(Column({ children: [H1('Dashboard'), autoMetrics(kpis)] }), { title: 'Dashboard' })
// Form that submits back to a tool (fields, toolName, options)
return display_form(
[
{ name: 'name', type: 'text', required: true },
{ name: 'email', type: 'email' },
],
'update_user',
{ title: 'Edit User' },
)
// Partial state update (no full re-render)
return display_update({ count: 42, status: 'complete' })
// Error display
return display_error('User not found', { code: 404 })
Two bundles, zero external dependencies:
| Bundle | Size | Use case |
|---|---|---|
renderer.auto.min.js |
~80KB | Recommended. Self-boots bridge, mounts $prefab into #root automatically |
renderer.min.js |
~80KB | Library only — defines window.prefab, you wire the bridge yourself |
<div id="root"></div>
<script src="renderer.auto.min.js"></script>
Races both bridge protocols (prefab:* and ui/* JSON-RPC) in parallel.
First host to respond wins. Buffers tool results that arrive before the handler is wired.
<script src="renderer.min.js"></script>
<script>
// Mount from wire format data
const app = PrefabRenderer.mount(document.getElementById('root'), wireData);
</script>
For MCP Apps running in iframes:
const ui = await prefab.app();
// Receive tool input from host
ui.onToolInput((args) => {
ui.render('#root', buildUI(args));
});
// Call tools on the host
const result = await ui.callTool('get_data', { query: 'active users' });
// Request display mode change
ui.requestMode('fullscreen');
// Access host context
console.log(ui.host); // { name, version, ... }
console.log(ui.capabilities); // { toast, clipboard, ... }
console.log(ui.theme); // host CSS variables
All UIs serialize to the $prefab wire format (JSON):
{
"$prefab": { "version": "0.2" },
"view": {
"type": "Column",
"children": [
{ "type": "H1", "content": "Hello" },
{ "type": "Text", "content": "{{ message }}" }
]
},
"state": {
"message": "Welcome to prefab"
},
"theme": {
"light": { "primary": "#3b82f6" },
"dark": { "primary": "#60a5fa" }
}
}
| Field | Type | Description |
|---|---|---|
$prefab |
{ version: string } |
Format identifier and version |
view |
ComponentJSON |
Root component tree |
state |
Record<string, unknown> |
Initial reactive state |
theme |
{ light?, dark? } |
CSS custom property overrides |
defs |
Record<string, ComponentJSON> |
Reusable component templates |
keyBindings |
Record<string, ActionJSON> |
Keyboard shortcut → action mappings |
{
"type": "Button",
"content": "Click me",
"variant": "default",
"onClick": {
"action": "setState",
"key": "count",
"value": "{{ count + 1 }}"
}
}
import { ... } from '@maxhealth.tech/prefab' // Everything
import { ... } from '@maxhealth.tech/prefab/actions' // Actions only
import { ... } from '@maxhealth.tech/prefab/rx' // Rx expressions only
import { ... } from '@maxhealth.tech/prefab/charts' // Chart components only
import { ... } from '@maxhealth.tech/prefab/auto' // Auto-renderers
import { ... } from '@maxhealth.tech/prefab/mcp' // MCP display helpers
import { ... } from '@maxhealth.tech/prefab/renderer' // Browser renderer
import '@maxhealth.tech/prefab/prefab.css' // Default stylesheet
bun install # Install dependencies
bun test # Run tests (996 passing)
bun run build # TypeScript compile + IIFE bundle
bun run lint # ESLint
bun run typecheck # Type check without emitting
MIT
Add this to claude_desktop_config.json and restart Claude Desktop.
{
"mcpServers": {
"prefab": {
"command": "npx",
"args": [
"-y",
"@maxhealth.tech/prefab"
]
}
}
}pro tip
Just installed Prefab? Say to Claude: "remember why I installed Prefaband what I want to try" — it'll save into your Vault.
how this works →