Compare commits

...
Author SHA1 Message Date
Chris Tate b2020fad6f cleanup 2026-03-09 02:06:22 -05:00
Chris Tate 5b51204960 vue 2026-03-09 01:34:12 -05:00
Chris Tate 3418d42a43 svelte/vue clients for mcp 2026-03-09 01:18:31 -05:00
Chris Tate 4606c01085 skills page (#188) 2026-03-06 20:39:07 -06:00
Chris Tate c1a700d719 cleaner skills (#187) 2026-03-06 20:22:56 -06:00
github-actions[bot] 6f15faaae0 chore: version packages (#186)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-03-06 20:02:39 -06:00
Chris Tate 63c339b4bb prepare 0.12.0 (#185) 2026-03-06 19:58:08 -06:00
Chris Tate 1cc87310c9 mcp (#184)
* mcp

* fix lint
2026-03-06 19:41:27 -06:00
Chris Tate 5b929dffa6 fix(svelte): correct JSDoc comment and add missing zod peer dependency (#183)
Fixes #182
2026-03-06 19:16:17 -06:00
Simon H 512f7fe5c5 feat: Svelte-renderer (#168)
* create svelte-renderer

* helpers

* reactive API

* turn into provider components

* fixes

* examples

* docs

* correct component props for catalog, harmonize defineRegistry API with other APis

* shuffle things around

* createRenderer

* on

* boundary/warning

* functions

* fix

* fix types
2026-03-06 17:31:06 -06:00
339 changed files with 16159 additions and 516 deletions
+3 -1
View File
@@ -17,7 +17,9 @@
"@json-render/jotai",
"@json-render/vue",
"@json-render/xstate",
"@json-render/image"
"@json-render/image",
"@json-render/mcp",
"@json-render/svelte"
]
],
"linked": [],
+16
View File
@@ -0,0 +1,16 @@
{
"mcpServers": {
"json-render-react": {
"command": "npx",
"args": ["tsx", "examples/mcp-react/server.ts", "--stdio"]
},
"json-render-vue": {
"command": "npx",
"args": ["tsx", "examples/mcp-vue/server.ts", "--stdio"]
},
"json-render-svelte": {
"command": "npx",
"args": ["tsx", "examples/mcp-svelte/server.ts", "--stdio"]
}
}
}
+2
View File
@@ -4,6 +4,7 @@
node_modules
.pnp
.pnp.js
.pnpm-store/
# Local env files
.env*
@@ -27,6 +28,7 @@ out/
build
dist
*.tsbuildinfo
.svelte-kit/
# Debug
+9
View File
@@ -0,0 +1,9 @@
{
"servers": {
"json-render": {
"type": "stdio",
"command": "npx",
"args": ["tsx", "examples/mcp-react/server.ts", "--stdio"]
}
}
}
+1 -1
View File
@@ -98,7 +98,7 @@ When asked to prepare a release (e.g. "prepare v0.12.0"):
- An API reference page at `apps/web/app/(main)/docs/api/<name>/page.mdx`
- An entry in `apps/web/lib/page-titles.ts` and `apps/web/lib/docs-navigation.ts`
- An entry in the docs-chat system prompt (`apps/web/app/api/docs-chat/route.ts`)
- A skill at `skills/json-render-<name>/SKILL.md`
- A skill at `skills/<name>/SKILL.md`
- A `packages/<name>/README.md`
6. **Run `pnpm type-check`** after all changes to verify nothing is broken
+26 -2
View File
@@ -19,6 +19,8 @@ npm install @json-render/core @json-render/react-pdf
npm install @json-render/core @json-render/react-email @react-email/components @react-email/render
# or for Vue
npm install @json-render/core @json-render/vue
# or for Svelte
npm install @json-render/core @json-render/svelte
```
## Why json-render?
@@ -28,7 +30,7 @@ json-render is a **Generative UI** framework: AI generates interfaces from natur
- **Guardrailed** - AI can only use components in your catalog
- **Predictable** - JSON output matches your schema, every time
- **Fast** - Stream and render progressively as the model responds
- **Cross-Platform** - React, Vue (web), React Native (mobile) from the same catalog
- **Cross-Platform** - React, Vue, Svelte (web), React Native (mobile) from the same catalog
- **Batteries Included** - 36 pre-built shadcn/ui components ready to use
## Quick Start
@@ -116,16 +118,19 @@ function Dashboard({ spec }) {
| `@json-render/core` | Schemas, catalogs, AI prompts, dynamic props, SpecStream utilities |
| `@json-render/react` | React renderer, contexts, hooks |
| `@json-render/vue` | Vue 3 renderer, composables, providers |
| `@json-render/svelte` | Svelte 5 renderer with runes-based reactivity |
| `@json-render/shadcn` | 36 pre-built shadcn/ui components (Radix UI + Tailwind CSS) |
| `@json-render/react-native` | React Native renderer with standard mobile components |
| `@json-render/remotion` | Remotion video renderer, timeline schema |
| `@json-render/react-pdf` | React PDF renderer for generating PDF documents from specs |
| `@json-render/react-email` | React Email renderer for HTML/plain-text emails from specs |
| `@json-render/image` | Image renderer for SVG/PNG output (OG images, social cards) via Satori |
| `@json-render/codegen` | Utilities for generating code from json-render UI trees |
| `@json-render/redux` | Redux / Redux Toolkit adapter for `StateStore` |
| `@json-render/zustand` | Zustand adapter for `StateStore` |
| `@json-render/jotai` | Jotai adapter for `StateStore` |
| `@json-render/xstate` | XState Store (atom) adapter for `StateStore` |
| `@json-render/mcp` | MCP Apps integration for Claude, ChatGPT, Cursor, VS Code |
## Renderers
@@ -177,6 +182,23 @@ const { registry } = defineRegistry(catalog, {
// <Renderer :spec="spec" :registry="registry" />
```
### Svelte (UI)
```typescript
import { defineRegistry, Renderer } from "@json-render/svelte";
import { schema } from "@json-render/svelte/schema";
const { registry } = defineRegistry(catalog, {
components: {
Card: ({ props, children }) => /* Svelte 5 snippet */,
Button: ({ props, emit }) => /* Svelte 5 snippet */,
},
});
// In your Svelte component:
// <Renderer spec={spec} registry={registry} />
```
### shadcn/ui (Web)
```tsx
@@ -462,8 +484,10 @@ pnpm dev
- http://react-email-demo.json-render.localhost:1355 - React Email Example
- http://remotion-demo.json-render.localhost:1355 - Remotion Video Example
- Chat Example: run `pnpm dev` in `examples/chat`
- Svelte Example: run `pnpm dev` in `examples/svelte` or `examples/svelte-chat`
- Vue Example: run `pnpm dev` in `examples/vue`
- Vite Renderers (React + Vue): run `pnpm dev` in `examples/vite-renderers`
- MCP Vue Example: run `pnpm build && pnpm start:stdio` in `examples/mcp-vue`
- Vite Renderers (React + Vue + Svelte): run `pnpm dev` in `examples/vite-renderers`
- React Native example: run `npx expo start` in `examples/react-native`
## How It Works
+9
View File
@@ -1,5 +1,14 @@
# web
## 0.1.5
### Patch Changes
- Updated dependencies [63c339b]
- @json-render/core@0.12.0
- @json-render/codegen@0.12.0
- @json-render/react@0.12.0
## 0.1.4
### Patch Changes
+104
View File
@@ -0,0 +1,104 @@
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/api/jotai")
# @json-render/jotai
Jotai adapter for json-render's `StateStore` interface.
## Installation
```bash
npm install @json-render/jotai @json-render/core @json-render/react jotai
```
## jotaiStateStore
Create a `StateStore` backed by a Jotai atom.
```typescript
import { jotaiStateStore } from "@json-render/jotai";
```
### Options
<table>
<thead>
<tr>
<th>Option</th>
<th>Type</th>
<th>Required</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>atom</code></td>
<td><code>{'WritableAtom<StateModel, [StateModel], void>'}</code></td>
<td>Yes</td>
<td>A writable atom holding the state model.</td>
</tr>
<tr>
<td><code>store</code></td>
<td>Jotai <code>Store</code></td>
<td>No</td>
<td>The Jotai store instance. Defaults to a new store created internally. Pass your own to share state with <code>{'<Provider>'}</code>.</td>
</tr>
</tbody>
</table>
### Example
```typescript
import { atom } from "jotai";
import { jotaiStateStore } from "@json-render/jotai";
import { StateProvider } from "@json-render/react";
const uiAtom = atom<Record<string, unknown>>({ count: 0 });
const store = jotaiStateStore({ atom: uiAtom });
```
```tsx
<StateProvider store={store}>
{/* json-render reads/writes go through Jotai */}
</StateProvider>
```
### Shared Jotai Store
If your app already uses a Jotai `<Provider>` with a custom store, pass it so both json-render and your components share the same state:
```typescript
import { atom, createStore } from "jotai";
import { Provider as JotaiProvider } from "jotai/react";
import { jotaiStateStore } from "@json-render/jotai";
import { StateProvider } from "@json-render/react";
const jStore = createStore();
const uiAtom = atom<Record<string, unknown>>({ count: 0 });
const store = jotaiStateStore({ atom: uiAtom, store: jStore });
```
```tsx
<JotaiProvider store={jStore}>
<StateProvider store={store}>
{/* Both json-render and useAtom() see the same state */}
</StateProvider>
</JotaiProvider>
```
## Re-exports
<table>
<thead>
<tr>
<th>Export</th>
<th>Source</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>StateStore</code></td>
<td><code>@json-render/core</code></td>
</tr>
</tbody>
</table>
+306
View File
@@ -0,0 +1,306 @@
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/api/mcp")
# @json-render/mcp
MCP Apps integration for json-render. Serve json-render UIs as interactive [MCP Apps](https://modelcontextprotocol.io/docs/extensions/apps) inside Claude, ChatGPT, Cursor, VS Code, and other MCP-capable clients.
## Install
```bash
npm install @json-render/mcp @json-render/core @modelcontextprotocol/sdk
```
For the iframe-side UI, install the renderer for your framework:
```bash
# React
npm install @json-render/react react react-dom
# Vue
npm install @json-render/vue vue
# Svelte
npm install @json-render/svelte svelte
```
See the [MCP React example](https://github.com/vercel-labs/json-render/tree/main/examples/mcp-react) for a full working example.
## Overview
MCP Apps let MCP servers return interactive HTML UIs that render directly inside chat conversations. `@json-render/mcp` bridges json-render catalogs with the MCP Apps protocol:
1. Your **catalog** defines which components and actions the AI can use
2. The **MCP server** exposes the catalog as a tool with the spec schema
3. The **bundled HTML** renders json-render specs inside the host's sandboxed iframe
4. The AI generates a spec, the host renders it, and users interact with the live UI
## Server API
### createMcpApp
Create a fully-configured MCP server. This is the main entry point.
```typescript
import { createMcpApp } from "@json-render/mcp";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import fs from "node:fs";
const server = createMcpApp({
name: "My Dashboard",
version: "1.0.0",
catalog: myCatalog,
html: fs.readFileSync("dist/index.html", "utf-8"),
});
await server.connect(new StdioServerTransport());
```
#### CreateMcpAppOptions
<table>
<thead>
<tr>
<th>Option</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>name</code></td>
<td><code>string</code></td>
<td>Server name shown in client UIs</td>
</tr>
<tr>
<td><code>version</code></td>
<td><code>string</code></td>
<td>Server version</td>
</tr>
<tr>
<td><code>catalog</code></td>
<td><code>Catalog</code></td>
<td>json-render catalog defining available components</td>
</tr>
<tr>
<td><code>html</code></td>
<td><code>string</code></td>
<td>Self-contained HTML for the iframe UI</td>
</tr>
<tr>
<td><code>tool</code></td>
<td><code>McpToolOptions</code></td>
<td>Optional tool name/title/description overrides</td>
</tr>
</tbody>
</table>
### registerJsonRenderTool
Register a json-render tool on an existing `McpServer`. Use this when you need to add json-render to a server that has other tools.
```typescript
import { registerJsonRenderTool } from "@json-render/mcp";
registerJsonRenderTool(server, {
catalog,
name: "render-ui",
title: "Render UI",
description: "Render an interactive UI",
resourceUri: "ui://render-ui/view.html",
});
```
### registerJsonRenderResource
Register the UI resource that serves the bundled HTML.
```typescript
import { registerJsonRenderResource } from "@json-render/mcp";
registerJsonRenderResource(server, {
resourceUri: "ui://render-ui/view.html",
html: bundledHtml,
});
```
## Client API
These exports run inside the sandboxed iframe rendered by the MCP host. Framework-specific adapters are available at:
- `@json-render/mcp/app/react` -- React hook
- `@json-render/mcp/app/vue` -- Vue composable
- `@json-render/mcp/app/svelte` -- Svelte stores
The React hook is also re-exported from `@json-render/mcp/app` for backward compatibility.
### React -- useJsonRenderApp (`@json-render/mcp/app/react`)
React hook that connects to the MCP host, listens for tool results, and maintains the current json-render spec.
```tsx
import { useJsonRenderApp } from "@json-render/mcp/app/react";
import { JSONUIProvider, Renderer } from "@json-render/react";
function McpAppView({ registry }) {
const { spec, loading, connected, error } = useJsonRenderApp({
name: "my-app",
version: "1.0.0",
});
if (error) return <div>Error: {error.message}</div>;
if (!spec) return <div>Waiting...</div>;
return (
<JSONUIProvider registry={registry} initialState={spec.state ?? {}}>
<Renderer spec={spec} registry={registry} loading={loading} />
</JSONUIProvider>
);
}
```
### Vue -- useJsonRenderApp (`@json-render/mcp/app/vue`)
Vue composable with the same API shape. Values are Vue refs and computed properties.
```vue
<script setup>
import { useJsonRenderApp } from "@json-render/mcp/app/vue";
import { Renderer } from "@json-render/vue";
const { spec, loading, error } = useJsonRenderApp({ name: "my-app" });
</script>
<template>
<div v-if="error">Error: {{ error.message }}</div>
<div v-else-if="!spec">Waiting...</div>
<Renderer v-else :spec="spec" :registry="registry" :loading="loading" />
</template>
```
### Svelte -- createJsonRenderApp (`@json-render/mcp/app/svelte`)
Creates a json-render MCP App client using Svelte stores. Call `destroy()` in `onDestroy` to clean up.
```svelte
<script>
import { createJsonRenderApp } from "@json-render/mcp/app/svelte";
import { Renderer } from "@json-render/svelte";
import { onDestroy } from "svelte";
const { spec, loading, error, destroy } = createJsonRenderApp({ name: "my-app" });
onDestroy(destroy);
</script>
{#if $error}
<div>Error: {$error.message}</div>
{:else if !$spec}
<div>Waiting...</div>
{:else}
<Renderer spec={$spec} {registry} loading={$loading} />
{/if}
```
### Return values
All three adapters return the same logical state:
<table>
<thead>
<tr>
<th>Field</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>spec</code></td>
<td><code>{'Spec | null'}</code></td>
<td>Current json-render spec</td>
</tr>
<tr>
<td><code>loading</code></td>
<td><code>boolean</code></td>
<td>Whether the spec is still being received</td>
</tr>
<tr>
<td><code>connected</code></td>
<td><code>boolean</code></td>
<td>Whether connected to the host</td>
</tr>
<tr>
<td><code>connecting</code></td>
<td><code>boolean</code></td>
<td>Whether currently connecting</td>
</tr>
<tr>
<td><code>error</code></td>
<td><code>{'Error | null'}</code></td>
<td>Connection error, if any</td>
</tr>
<tr>
<td><code>app</code></td>
<td><code>{'App | null'}</code></td>
<td>The underlying MCP App instance</td>
</tr>
<tr>
<td><code>callServerTool</code></td>
<td><code>{'(name, args?) => Promise<void>'}</code></td>
<td>Call an MCP server tool and update spec from result</td>
</tr>
</tbody>
</table>
In React, values are plain primitives. In Vue, they are `Ref`/`ShallowRef`/`ComputedRef`. In Svelte, they are readable stores (use `$` prefix to subscribe). The Svelte adapter also returns a `destroy` function for cleanup.
### buildAppHtml (`@json-render/mcp/app`)
Generate a self-contained HTML page from bundled JavaScript and CSS. Framework-agnostic.
```typescript
import { buildAppHtml } from "@json-render/mcp/app";
import fs from "node:fs";
const html = buildAppHtml({
title: "Dashboard",
js: fs.readFileSync("dist/app.js", "utf-8"),
css: fs.readFileSync("dist/app.css", "utf-8"),
});
```
## Client Configuration
### Cursor
Add to `.cursor/mcp.json`:
```json
{
"mcpServers": {
"json-render": {
"command": "npx",
"args": ["tsx", "path/to/server.ts", "--stdio"]
}
}
}
```
### Claude Desktop
Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
```json
{
"mcpServers": {
"json-render": {
"command": "npx",
"args": ["tsx", "/absolute/path/to/server.ts", "--stdio"]
}
}
}
```
## Supported Clients
MCP Apps are supported by Claude (web and desktop), ChatGPT, VS Code (GitHub Copilot), Cursor, Goose, and Postman.
+104
View File
@@ -0,0 +1,104 @@
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/api/redux")
# @json-render/redux
Redux / Redux Toolkit adapter for json-render's `StateStore` interface.
## Installation
```bash
npm install @json-render/redux @json-render/core @json-render/react redux
# or with Redux Toolkit (recommended):
npm install @json-render/redux @json-render/core @json-render/react @reduxjs/toolkit
```
## reduxStateStore
Create a `StateStore` backed by a Redux store.
```typescript
import { reduxStateStore } from "@json-render/redux";
```
### Options
<table>
<thead>
<tr>
<th>Option</th>
<th>Type</th>
<th>Required</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>store</code></td>
<td><code>Store</code></td>
<td>Yes</td>
<td>The Redux store instance.</td>
</tr>
<tr>
<td><code>selector</code></td>
<td><code>{'(state: S) => StateModel'}</code></td>
<td>No</td>
<td>Select the json-render slice from the Redux state tree. Defaults to <code>{'(state) => state'}</code>.</td>
</tr>
<tr>
<td><code>dispatch</code></td>
<td><code>{'(nextState: StateModel, store: Store) => void'}</code></td>
<td>Yes</td>
<td>Dispatch an action that replaces the selected slice with the next state.</td>
</tr>
</tbody>
</table>
### Example
```typescript
import { configureStore, createSlice } from "@reduxjs/toolkit";
import { reduxStateStore } from "@json-render/redux";
import { StateProvider } from "@json-render/react";
const uiSlice = createSlice({
name: "ui",
initialState: { count: 0 } as Record<string, unknown>,
reducers: {
replaceUiState: (_state, action) => action.payload,
},
});
const reduxStore = configureStore({
reducer: { ui: uiSlice.reducer },
});
const store = reduxStateStore({
store: reduxStore,
selector: (state) => state.ui,
dispatch: (next, s) => s.dispatch(uiSlice.actions.replaceUiState(next)),
});
```
```tsx
<StateProvider store={store}>
{/* json-render reads/writes go through Redux */}
</StateProvider>
```
## Re-exports
<table>
<thead>
<tr>
<th>Export</th>
<th>Source</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>StateStore</code></td>
<td><code>@json-render/core</code></td>
</tr>
</tbody>
</table>
@@ -0,0 +1,126 @@
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/api/svelte")
# @json-render/svelte
Svelte 5 components, providers, and helpers for rendering json-render specs.
## Installation
<PackageInstall packages="@json-render/core @json-render/svelte" />
Peer dependencies: `svelte ^5.0.0` and `zod ^4.0.0`.
<PackageInstall packages="svelte zod" />
## Components
### Renderer
```svelte
<Renderer
spec={spec} // Spec | null
registry={registry}
loading={false}
/>
```
Renders a spec with your component registry. If `spec` is `null`, it renders nothing.
### JsonUIProvider
Convenience wrapper around `StateProvider`, `VisibilityProvider`, `ValidationProvider`, and `ActionProvider`.
```svelte
<JsonUIProvider
initialState={{}}
handlers={handlers}
validationFunctions={validationFunctions}
>
<Renderer {spec} {registry} />
</JsonUIProvider>
```
## defineRegistry
Create a typed component registry and action handlers from a catalog.
```typescript
import { defineRegistry } from "@json-render/svelte";
const { registry, handlers, executeAction } = defineRegistry(catalog, {
components: {
Card,
Button,
},
actions: {
submit: async (params, setState, state) => {
// custom action logic
},
},
});
```
`handlers` is designed for `JsonUIProvider`/`ActionProvider`. `executeAction` is an imperative helper.
## Component Props
Registry components receive `BaseComponentProps<TProps>`:
```typescript
interface BaseComponentProps<TProps> {
props: TProps;
children?: Snippet;
emit: (event: string) => void;
bindings?: Record<string, string>;
loading?: boolean;
}
```
Use `emit("eventName")` to trigger handlers declared in the spec `on` bindings.
## Context Helpers
Use these helpers inside Svelte components:
- `getStateValue(path)` - read/write state via `.current`
- `getBoundProp(() => value, () => bindingPath)` - write back resolved `$bindState` / `$bindItem` values
- `isVisible(condition)` - evaluate visibility via `.current`
- `getAction(name)` - read a registered action handler via `.current`
- `getFieldValidation(ctx, path, config)` - get field validation state + actions
For advanced usage, access full contexts:
- `getStateContext()`
- `getActionContext()`
- `getVisibilityContext()`
- `getValidationContext()`
- `getOptionalValidationContext()`
## Streaming
### createUIStream
```typescript
const stream = createUIStream({
api: "/api/generate-ui",
onComplete: (spec) => console.log(spec),
});
await stream.send("Create a login form");
console.log(stream.spec);
console.log(stream.isStreaming);
```
### createChatUI
```typescript
const chat = createChatUI({ api: "/api/chat-ui" });
await chat.send("Build a settings panel");
console.log(chat.messages, chat.isStreaming);
```
## Schema Export
Use `schema` from `@json-render/svelte` when defining catalogs for Svelte specs.
@@ -0,0 +1,77 @@
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/api/xstate")
# @json-render/xstate
[XState Store](https://stately.ai/docs/xstate-store) adapter for json-render's `StateStore` interface.
Requires `@xstate/store` v3+.
## Installation
```bash
npm install @json-render/xstate @json-render/core @json-render/react @xstate/store
```
## xstateStoreStateStore
Create a `StateStore` backed by an `@xstate/store` atom.
```typescript
import { xstateStoreStateStore } from "@json-render/xstate";
```
### Options
<table>
<thead>
<tr>
<th>Option</th>
<th>Type</th>
<th>Required</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>atom</code></td>
<td><code>{'Atom<StateModel>'}</code></td>
<td>Yes</td>
<td>An <code>@xstate/store</code> atom (from <code>createAtom</code>) holding the json-render state model.</td>
</tr>
</tbody>
</table>
### Example
```typescript
import { createAtom } from "@xstate/store";
import { xstateStoreStateStore } from "@json-render/xstate";
import { StateProvider } from "@json-render/react";
const uiAtom = createAtom({ count: 0 });
const store = xstateStoreStateStore({ atom: uiAtom });
```
```tsx
<StateProvider store={store}>
{/* json-render reads/writes go through @xstate/store */}
</StateProvider>
```
## Re-exports
<table>
<thead>
<tr>
<th>Export</th>
<th>Source</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>StateStore</code></td>
<td><code>@json-render/core</code></td>
</tr>
</tbody>
</table>
@@ -0,0 +1,108 @@
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/api/zustand")
# @json-render/zustand
Zustand adapter for json-render's `StateStore` interface.
Requires Zustand v5+. Zustand v4 is not supported due to breaking API changes in the vanilla store interface.
## Installation
```bash
npm install @json-render/zustand @json-render/core @json-render/react zustand
```
## zustandStateStore
Create a `StateStore` backed by a Zustand vanilla store.
```typescript
import { zustandStateStore } from "@json-render/zustand";
```
### Options
<table>
<thead>
<tr>
<th>Option</th>
<th>Type</th>
<th>Required</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>store</code></td>
<td><code>{'StoreApi<S>'}</code></td>
<td>Yes</td>
<td>A Zustand vanilla store (from <code>createStore</code> in <code>zustand/vanilla</code>).</td>
</tr>
<tr>
<td><code>selector</code></td>
<td><code>{'(state: S) => StateModel'}</code></td>
<td>No</td>
<td>Select the json-render slice from the store state. Defaults to the entire state.</td>
</tr>
<tr>
<td><code>updater</code></td>
<td><code>{'(nextState: StateModel, store: StoreApi<S>) => void'}</code></td>
<td>No</td>
<td>Apply a state change back to the store. Defaults to a shallow merge.</td>
</tr>
</tbody>
</table>
### Example
```typescript
import { createStore } from "zustand/vanilla";
import { zustandStateStore } from "@json-render/zustand";
import { StateProvider } from "@json-render/react";
const bearStore = createStore(() => ({
count: 0,
name: "Bear",
}));
const store = zustandStateStore({ store: bearStore });
```
```tsx
<StateProvider store={store}>
{/* json-render reads/writes go through Zustand */}
</StateProvider>
```
### Nested Slice
```typescript
const appStore = createStore(() => ({
ui: { count: 0 },
auth: { token: null },
}));
const store = zustandStateStore({
store: appStore,
selector: (s) => s.ui,
updater: (next, s) => s.setState({ ui: next }),
});
```
## Re-exports
<table>
<thead>
<tr>
<th>Export</th>
<th>Source</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>StateStore</code></td>
<td><code>@json-render/core</code></td>
</tr>
</tbody>
</table>
@@ -21,6 +21,14 @@ Peer dependencies: `vue ^3.5.0` and `zod ^4.0.0`.
<PackageInstall packages="vue zod" />
## For Svelte
<PackageInstall packages="@json-render/core @json-render/svelte" />
Peer dependencies: `svelte ^5.0.0` and `zod ^4.0.0`.
<PackageInstall packages="svelte zod" />
## For React UI with shadcn/ui
Pre-built components for fast prototyping and production use:
+117
View File
@@ -0,0 +1,117 @@
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/skills")
# Skills
json-render ships with skills that teach AI coding agents how to use each package. Install a skill and your agent in Cursor, Claude Code, or Codex can generate json-render UIs without manual guidance.
## Available Skills
- **core** — Core schemas, catalogs, and AI prompt generation.
- **react** — React renderer that turns JSON specs into React component trees.
- **react-pdf** — PDF renderer using `@react-pdf/renderer`.
- **react-email** — Email renderer that produces HTML or plain-text emails.
- **react-native** — React Native renderer for native mobile UIs.
- **shadcn** — Pre-built shadcn/ui components (Radix UI + Tailwind).
- **image** — Image renderer that turns JSON specs into SVG and PNG via Satori.
- **remotion** — Remotion renderer for video generation from JSON timeline specs.
- **vue** — Vue 3 renderer for Vue component trees.
- **svelte** — Svelte 5 renderer for Svelte component trees.
- **codegen** — Code generation utilities for building custom exporters.
- **mcp** — MCP Apps integration for Claude, ChatGPT, Cursor, and VS Code.
- **redux** — Redux adapter for json-render's `StateStore` interface.
- **zustand** — Zustand adapter for json-render's `StateStore` interface.
- **jotai** — Jotai adapter for json-render's `StateStore` interface.
- **xstate** — XState Store adapter for json-render's `StateStore` interface.
## Installation
```bash
npx skills add vercel-labs/json-render --skill core
npx skills add vercel-labs/json-render --skill react
npx skills add vercel-labs/json-render --skill react-pdf
npx skills add vercel-labs/json-render --skill react-email
npx skills add vercel-labs/json-render --skill react-native
npx skills add vercel-labs/json-render --skill shadcn
npx skills add vercel-labs/json-render --skill image
npx skills add vercel-labs/json-render --skill remotion
npx skills add vercel-labs/json-render --skill vue
npx skills add vercel-labs/json-render --skill svelte
npx skills add vercel-labs/json-render --skill codegen
npx skills add vercel-labs/json-render --skill mcp
npx skills add vercel-labs/json-render --skill redux
npx skills add vercel-labs/json-render --skill zustand
npx skills add vercel-labs/json-render --skill jotai
npx skills add vercel-labs/json-render --skill xstate
```
After installing, your AI agent will automatically activate the right skill when it encounters a matching request.
## core
The foundational skill. Teaches agents how to define catalogs, create schemas, build specs, and generate AI prompts. This is the starting point for any json-render project and covers `defineCatalog`, `defineSchema`, `specSchema`, `toPrompt`, and the full spec format.
## react
Teaches agents how to render JSON specs as React component trees using `JsonRender`, `JsonRenderClient`, and `useJsonRender`. Covers custom component registries, client-side interactivity, state management, and streaming integration.
## react-pdf
Teaches agents how to generate PDFs from JSON specs using `@react-pdf/renderer`. Covers the PDF-specific component registry, page layout, and styling.
## react-email
Teaches agents how to render JSON specs as HTML or plain-text emails using React Email components. Covers the email-specific registry and rendering pipeline.
## react-native
Teaches agents how to render JSON specs as native mobile UIs with React Native. Covers the native component registry and platform-specific considerations.
## shadcn
Teaches agents how to use the pre-built shadcn/ui component registry with json-render. Includes Radix UI primitives, Tailwind styling, and the full set of available shadcn components.
## image
Teaches agents how to turn JSON specs into SVG and PNG images using Satori. Covers the image-specific registry, dimensions, fonts, and rendering options.
## remotion
Teaches agents how to generate videos from JSON timeline specs using Remotion. Covers compositions, sequences, timeline structure, and video rendering.
## vue
Teaches agents how to render JSON specs as Vue 3 component trees. Covers the Vue renderer API, custom component registries, and reactivity integration.
## svelte
Teaches agents how to render JSON specs as Svelte 5 component trees. Covers the Svelte renderer API and component registration.
## codegen
Teaches agents how to use code generation utilities to export UI specs as framework-specific source code. Covers the codegen pipeline and custom exporter creation.
## mcp
Teaches agents how to build MCP Apps that serve json-render UIs inside AI tools like Claude, ChatGPT, Cursor, and VS Code. Covers MCP server setup, tool definitions, and UI streaming.
## redux
Teaches agents how to connect a Redux store to json-render's `StateStore` interface for state-driven UIs.
## zustand
Teaches agents how to connect a Zustand store to json-render's `StateStore` interface for lightweight state management.
## jotai
Teaches agents how to connect Jotai atoms to json-render's `StateStore` interface for atomic state management.
## xstate
Teaches agents how to connect an XState Store to json-render's `StateStore` interface for state-machine-driven UIs.
## Source
All skill files are in the [`skills/`](https://github.com/vercel-labs/json-render/tree/main/skills) directory of the repository.
+2 -1
View File
@@ -16,7 +16,8 @@ const SYSTEM_PROMPT = `You are a helpful documentation assistant for json-render
GitHub repository: https://github.com/vercel-labs/json-render
Documentation: https://json-render.dev/docs
npm packages: @json-render/core, @json-render/react, @json-render/react-email, @json-render/react-pdf, @json-render/image, @json-render/remotion, @json-render/codegen
npm packages: @json-render/core, @json-render/react, @json-render/vue, @json-render/svelte, @json-render/shadcn, @json-render/react-native, @json-render/react-email, @json-render/react-pdf, @json-render/image, @json-render/remotion, @json-render/codegen, @json-render/mcp, @json-render/redux, @json-render/zustand, @json-render/jotai, @json-render/xstate
Skills: json-render ships AI agent skills that teach coding agents how to use each package. Install with "npx skills add vercel-labs/json-render --skill <name>". Available skills: core, react, react-pdf, react-email, react-native, shadcn, image, remotion, vue, svelte, codegen, mcp, redux, zustand, jotai, xstate. See /docs/skills for details.
You have access to the full json-render documentation via the bash and readFile tools. The docs are available as markdown files in the /workspace/docs/ directory.
+18 -1
View File
@@ -16,6 +16,7 @@ export const docsNavigation: NavSection[] = [
{ title: "Introduction", href: "/docs" },
{ title: "Installation", href: "/docs/installation" },
{ title: "Quick Start", href: "/docs/quick-start" },
{ title: "Skills", href: "/docs/skills" },
{ title: "Migration Guide", href: "/docs/migration" },
{ title: "Changelog", href: "/docs/changelog" },
],
@@ -80,16 +81,26 @@ export const docsNavigation: NavSection[] = [
href: "https://github.com/vercel-labs/json-render/tree/main/examples/image",
external: true,
},
{
title: "Svelte",
href: "https://github.com/vercel-labs/json-render/tree/main/examples/svelte",
external: true,
},
{
title: "Vue",
href: "https://github.com/vercel-labs/json-render/tree/main/examples/vue",
external: true,
},
{
title: "Renders with Vite (Vue / React)",
title: "Renders with Vite (Vue / React / Svelte)",
href: "https://github.com/vercel-labs/json-render/tree/main/examples/vite-renderers",
external: true,
},
{
title: "MCP App",
href: "https://github.com/vercel-labs/json-render/tree/main/examples/mcp-react",
external: true,
},
],
},
{
@@ -121,7 +132,13 @@ export const docsNavigation: NavSection[] = [
{ title: "@json-render/image", href: "/docs/api/image" },
{ title: "@json-render/remotion", href: "/docs/api/remotion" },
{ title: "@json-render/vue", href: "/docs/api/vue" },
{ title: "@json-render/svelte", href: "/docs/api/svelte" },
{ title: "@json-render/codegen", href: "/docs/api/codegen" },
{ title: "@json-render/mcp", href: "/docs/api/mcp" },
{ title: "@json-render/redux", href: "/docs/api/redux" },
{ title: "@json-render/zustand", href: "/docs/api/zustand" },
{ title: "@json-render/jotai", href: "/docs/api/jotai" },
{ title: "@json-render/xstate", href: "/docs/api/xstate" },
],
},
];
+7
View File
@@ -37,6 +37,7 @@ export const PAGE_TITLES: Record<string, string> = {
"docs/ag-ui": "AG-UI Integration",
"docs/migration": "Migration Guide",
"docs/changelog": "Changelog",
"docs/skills": "Skills",
// API references
"docs/api/core": "@json-render/core API",
@@ -45,10 +46,16 @@ export const PAGE_TITLES: Record<string, string> = {
"docs/api/react-pdf": "@json-render/react-pdf API",
"docs/api/react-email": "@json-render/react-email API",
"docs/api/react-native": "@json-render/react-native API",
"docs/api/svelte": "@json-render/svelte API",
"docs/api/codegen": "@json-render/codegen API",
"docs/api/image": "@json-render/image API",
"docs/api/remotion": "@json-render/remotion API",
"docs/api/shadcn": "@json-render/shadcn API",
"docs/api/mcp": "@json-render/mcp API",
"docs/api/redux": "@json-render/redux API",
"docs/api/zustand": "@json-render/zustand API",
"docs/api/jotai": "@json-render/jotai API",
"docs/api/xstate": "@json-render/xstate API",
};
/**
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "web",
"version": "0.1.4",
"version": "0.1.5",
"type": "module",
"private": true,
"license": "Apache-2.0",
+9
View File
@@ -1,5 +1,14 @@
# example-chat
## 0.1.5
### Patch Changes
- Updated dependencies [63c339b]
- @json-render/core@0.12.0
- @json-render/react@0.12.0
- @json-render/shadcn@0.12.0
## 0.1.4
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts";
import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-chat",
"version": "0.1.4",
"version": "0.1.5",
"type": "module",
"private": true,
"scripts": {
+9
View File
@@ -1,5 +1,14 @@
# example-dashboard
## 0.1.5
### Patch Changes
- Updated dependencies [63c339b]
- @json-render/core@0.12.0
- @json-render/codegen@0.12.0
- @json-render/react@0.12.0
## 0.1.4
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts";
import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-dashboard",
"version": "0.1.4",
"version": "0.1.5",
"type": "module",
"private": true,
"scripts": {
+8
View File
@@ -1,5 +1,13 @@
# example-image
## 0.1.2
### Patch Changes
- Updated dependencies [63c339b]
- @json-render/core@0.12.0
- @json-render/image@0.12.0
## 0.1.1
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-image",
"version": "0.1.1",
"version": "0.1.2",
"type": "module",
"private": true,
"scripts": {
+11
View File
@@ -0,0 +1,11 @@
# example-mcp
## 0.1.1
### Patch Changes
- Updated dependencies [63c339b]
- @json-render/core@0.12.0
- @json-render/mcp@0.12.0
- @json-render/react@0.12.0
- @json-render/shadcn@0.12.0
+63
View File
@@ -0,0 +1,63 @@
# MCP App Example
A json-render MCP App that serves interactive shadcn/ui-based UIs directly inside Claude, ChatGPT, Cursor, VS Code, and other MCP-capable clients.
## Setup
```bash
pnpm install
pnpm build
```
## Usage
### With Cursor
Add to `.cursor/mcp.json`:
```json
{
"mcpServers": {
"json-render": {
"command": "npx",
"args": ["tsx", "examples/mcp-react/server.ts", "--stdio"]
}
}
}
```
### With Claude Desktop
Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
```json
{
"mcpServers": {
"json-render": {
"command": "npx",
"args": ["tsx", "/absolute/path/to/examples/mcp-react/server.ts", "--stdio"]
}
}
}
```
### HTTP Transport
```bash
pnpm start
# Server listens on http://localhost:3001/mcp
```
### Stdio Transport
```bash
pnpm start:stdio
```
## How It Works
1. The Vite build bundles the React app (with shadcn components and `useJsonRenderApp` hook) into a single self-contained HTML file
2. The MCP server registers a `render-ui` tool with the catalog prompt as its description
3. When the LLM calls the tool, it generates a json-render spec constrained to the catalog
4. The host renders the bundled HTML in a sandboxed iframe
5. The iframe receives the spec via the MCP Apps protocol and renders it with json-render
+13
View File
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline' 'unsafe-eval' data: blob:; img-src * data: blob:; font-src * data:; connect-src *; media-src *;" />
<title>json-render MCP App</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+34
View File
@@ -0,0 +1,34 @@
{
"name": "example-mcp-react",
"version": "0.1.1",
"type": "module",
"private": true,
"scripts": {
"build": "vite build",
"start": "tsx server.ts",
"start:stdio": "tsx server.ts --stdio",
"dev": "tsx watch server.ts"
},
"dependencies": {
"@json-render/core": "workspace:*",
"@json-render/mcp": "workspace:*",
"@json-render/react": "workspace:*",
"@json-render/shadcn": "workspace:*",
"@modelcontextprotocol/ext-apps": "^1.2.0",
"@modelcontextprotocol/sdk": "^1.27.1",
"react": "19.2.3",
"react-dom": "19.2.3",
"zod": "^4.3.6"
},
"devDependencies": {
"@types/react": "19.2.3",
"@types/react-dom": "19.2.3",
"@tailwindcss/vite": "^4.2.1",
"@vitejs/plugin-react": "^5.1.4",
"tailwindcss": "^4.2.1",
"tsx": "^4.21.0",
"typescript": "^5.7.2",
"vite": "^6.3.5",
"vite-plugin-singlefile": "^2.3.0"
}
}
+86
View File
@@ -0,0 +1,86 @@
import { createMcpApp } from "@json-render/mcp";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { catalog } from "./src/catalog.js";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
function loadHtml(): string {
const htmlPath = path.join(__dirname, "dist", "index.html");
if (!fs.existsSync(htmlPath)) {
throw new Error(
`Built HTML not found at ${htmlPath}. Run 'pnpm build' first.`,
);
}
return fs.readFileSync(htmlPath, "utf-8");
}
async function startStdio() {
const html = loadHtml();
const server = await createMcpApp({
name: "json-render Example",
version: "1.0.0",
catalog,
html,
});
await server.connect(new StdioServerTransport());
}
async function startHttp() {
const html = loadHtml();
const port = parseInt(process.env.PORT ?? "3001", 10);
const expressApp = createMcpExpressApp({ host: "0.0.0.0" });
expressApp.all("/mcp", async (req, res) => {
const server = await createMcpApp({
name: "json-render Example",
version: "1.0.0",
catalog,
html,
});
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
res.on("close", () => {
transport.close().catch(() => {});
server.close().catch(() => {});
});
try {
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
} catch (error) {
console.error("MCP error:", error);
if (!res.headersSent) {
res.status(500).json({
jsonrpc: "2.0",
error: { code: -32603, message: "Internal server error" },
id: null,
});
}
}
});
expressApp.listen(port, () => {
console.log(`MCP server listening on http://localhost:${port}/mcp`);
});
}
if (process.argv.includes("--stdio")) {
startStdio().catch((e) => {
console.error(e);
process.exit(1);
});
} else {
startHttp().catch((e) => {
console.error(e);
process.exit(1);
});
}
+10
View File
@@ -0,0 +1,10 @@
import { defineCatalog } from "@json-render/core";
import { schema } from "@json-render/react/schema";
import { shadcnComponentDefinitions } from "@json-render/shadcn/catalog";
export const catalog = defineCatalog(schema, {
components: {
...shadcnComponentDefinitions,
},
actions: {},
});
+106
View File
@@ -0,0 +1,106 @@
@import "tailwindcss";
@source "../../../packages/shadcn/src";
@source "../../../packages/react/src";
@custom-variant dark (&:is([data-theme="dark"] *));
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--radius-2xl: calc(var(--radius) + 8px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
}
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
}
[data-theme="dark"] {
--background: var(--color-background-secondary, var(--vscode-editor-background, oklch(0.145 0 0)));
--foreground: var(--color-text-primary, var(--vscode-foreground, oklch(0.985 0 0)));
--card: var(--color-background-primary, var(--vscode-sideBar-background, oklch(0.205 0 0)));
--card-foreground: var(--color-text-primary, var(--vscode-foreground, oklch(0.985 0 0)));
--popover: var(--color-background-primary, var(--vscode-sideBar-background, oklch(0.205 0 0)));
--popover-foreground: var(--color-text-primary, var(--vscode-foreground, oklch(0.985 0 0)));
--primary: var(--color-text-primary, var(--vscode-foreground, oklch(0.922 0 0)));
--primary-foreground: var(--color-background-primary, var(--vscode-sideBar-background, oklch(0.205 0 0)));
--secondary: var(--color-background-tertiary, var(--vscode-activityBar-background, oklch(0.269 0 0)));
--secondary-foreground: var(--color-text-primary, var(--vscode-foreground, oklch(0.985 0 0)));
--muted: var(--color-background-tertiary, var(--vscode-activityBar-background, oklch(0.269 0 0)));
--muted-foreground: var(--color-text-secondary, var(--vscode-descriptionForeground, oklch(0.708 0 0)));
--accent: var(--color-background-tertiary, var(--vscode-activityBar-background, oklch(0.269 0 0)));
--accent-foreground: var(--color-text-primary, var(--vscode-foreground, oklch(0.985 0 0)));
--destructive: var(--color-text-danger, var(--vscode-errorForeground, oklch(0.704 0.191 22.216)));
--border: var(--color-border-primary, var(--vscode-widget-border, oklch(1 0 0 / 10%)));
--input: var(--color-border-secondary, var(--vscode-editorWidget-border, oklch(1 0 0 / 15%)));
--ring: var(--color-ring-primary, var(--vscode-focusBorder, oklch(0.556 0 0)));
}
* {
box-sizing: border-box;
border-color: var(--color-border);
}
html, body, #root {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
body {
background-color: var(--color-background);
color: var(--color-foreground);
font-family: var(--vscode-font-family, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply text-foreground;
background-color: var(--color-background) !important;
}
}
button {
cursor: pointer;
}
+269
View File
@@ -0,0 +1,269 @@
import "./globals.css";
import {
Component,
useState,
useEffect,
useCallback,
type ReactNode,
} from "react";
import { createRoot } from "react-dom/client";
import { App as McpApp } from "@modelcontextprotocol/ext-apps";
import { JSONUIProvider, Renderer, defineRegistry } from "@json-render/react";
import { shadcnComponents } from "@json-render/shadcn";
import type { Spec } from "@json-render/core";
import { catalog } from "./catalog";
const { registry } = defineRegistry(catalog, {
components: {
...shadcnComponents,
Avatar: ({ props }: { props: Record<string, unknown> }) => {
const name = (props.name as string) || (props.alt as string) || "?";
const src = props.src as string | undefined;
const initials = name
.split(" ")
.map((n) => n[0])
.join("")
.slice(0, 2)
.toUpperCase();
const size = props.size === "lg" ? 48 : props.size === "sm" ? 32 : 40;
const [imgFailed, setImgFailed] = useState(false);
const onError = useCallback(() => setImgFailed(true), []);
const showImg = src && !imgFailed;
return (
<div
style={{
width: size,
height: size,
borderRadius: "50%",
overflow: "hidden",
backgroundColor: "var(--color-background-tertiary, #27272a)",
display: "flex",
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
}}
>
{showImg ? (
<img
src={src}
alt={name}
referrerPolicy="no-referrer"
crossOrigin="anonymous"
width={size}
height={size}
onError={onError}
style={{ objectFit: "cover", width: size, height: size }}
/>
) : (
<span
style={{
fontSize: size * 0.4,
color: "var(--color-text-secondary, #a1a1aa)",
}}
>
{initials}
</span>
)}
</div>
);
},
},
});
class ErrorBoundary extends Component<
{ children: ReactNode },
{ error: Error | null }
> {
state = { error: null as Error | null };
static getDerivedStateFromError(error: Error) {
return { error };
}
render() {
if (this.state.error) {
return (
<div
style={{
padding: 16,
color: "#dc2626",
fontFamily: "monospace",
fontSize: 13,
}}
>
Error: {this.state.error.message}
</div>
);
}
return this.props.children;
}
}
function forceFullWidth(spec: Spec): Spec {
if (!spec.elements) return spec;
const elements = { ...spec.elements };
for (const [key, el] of Object.entries(elements)) {
if (el.type === "Card" && el.props) {
elements[key] = {
...el,
props: { ...el.props, maxWidth: "full", centered: false },
};
}
}
return { ...spec, elements };
}
function parseSpec(
result: { content?: Array<{ type: string; text?: string }> } | undefined,
): Spec | null {
const text = result?.content?.find((c) => c.type === "text")?.text;
if (!text) return null;
try {
return forceFullWidth(JSON.parse(text) as Spec);
} catch {
return null;
}
}
function applyHostContext(ctx: {
theme?: string;
styles?: { variables?: Record<string, string> };
}) {
if (ctx.theme) {
document.documentElement.setAttribute("data-theme", ctx.theme);
document.documentElement.style.colorScheme = ctx.theme;
}
if (ctx.styles?.variables) {
const root = document.documentElement;
for (const [key, value] of Object.entries(ctx.styles.variables)) {
root.style.setProperty(key, value);
}
}
}
function McpAppView() {
const [spec, setSpec] = useState<Spec | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let specReceived = false;
function handleSpec(s: Spec) {
if (specReceived) return;
specReceived = true;
setSpec(s);
}
function onMessage(event: MessageEvent) {
if (specReceived) return;
const data = event.data as Record<string, unknown> | undefined;
if (!data || typeof data !== "object") return;
const method = data.method as string | undefined;
const params = data.params as Record<string, unknown> | undefined;
// Cursor sends the spec via tool-input
if (method === "ui/notifications/tool-input" && params?.arguments) {
const args = params.arguments as Record<string, unknown>;
const rawSpec = args.spec;
if (
rawSpec &&
typeof rawSpec === "object" &&
"root" in rawSpec &&
"elements" in rawSpec
) {
handleSpec(forceFullWidth(rawSpec as Spec));
}
}
}
window.addEventListener("message", onMessage);
const app = new McpApp({ name: "json-render", version: "1.0.0" });
app.ontoolresult = (result) => {
const parsed = parseSpec(
result as { content?: Array<{ type: string; text?: string }> },
);
if (parsed) handleSpec(parsed);
};
app.onhostcontextchanged = (ctx) =>
applyHostContext(ctx as Parameters<typeof applyHostContext>[0]);
app.onerror = (err: unknown) => {
setError(err instanceof Error ? err.message : String(err));
};
app
.connect()
.then(() => {
const ctx = app.getHostContext?.();
if (ctx)
applyHostContext(ctx as Parameters<typeof applyHostContext>[0]);
// Fallback: if host didn't provide theme, detect via media query
if (!ctx || !(ctx as Record<string, unknown>).theme) {
const prefersDark = window.matchMedia(
"(prefers-color-scheme: dark)",
).matches;
document.documentElement.setAttribute(
"data-theme",
prefersDark ? "dark" : "light",
);
document.documentElement.style.colorScheme = prefersDark
? "dark"
: "light";
}
})
.catch((err: unknown) =>
setError(err instanceof Error ? err.message : String(err)),
);
return () => {
window.removeEventListener("message", onMessage);
app.close().catch(() => {});
};
}, []);
if (error) {
return (
<div
style={{
padding: 16,
color: "#dc2626",
fontFamily: "monospace",
fontSize: 13,
}}
>
{error}
</div>
);
}
if (!spec) {
return (
<div
style={{
padding: 16,
color: "#6b7280",
fontFamily: "sans-serif",
fontSize: 14,
}}
>
Loading...
</div>
);
}
return (
<JSONUIProvider registry={registry} initialState={spec.state ?? {}}>
<div className="w-full">
<Renderer spec={spec} registry={registry} />
</div>
</JSONUIProvider>
);
}
createRoot(document.getElementById("root")!).render(
<ErrorBoundary>
<McpAppView />
</ErrorBoundary>,
);
+89
View File
@@ -0,0 +1,89 @@
import { JSONUIProvider, Renderer } from "@json-render/react";
import { shadcnComponents } from "@json-render/shadcn";
import { defineRegistry } from "@json-render/react";
import { useJsonRenderApp } from "@json-render/mcp/app/react";
import { catalog } from "./catalog";
import { useState, useEffect } from "react";
const { registry } = defineRegistry(catalog, {
components: {
...shadcnComponents,
},
});
const debugStyle = {
padding: 12,
fontFamily: "monospace",
fontSize: 12,
color: "#000",
background: "#fffbe6",
border: "1px solid #faad14",
borderRadius: 4,
margin: 8,
whiteSpace: "pre-wrap" as const,
wordBreak: "break-all" as const,
};
export function McpAppView() {
const [logs, setLogs] = useState<string[]>(["App mounted"]);
const addLog = (msg: string) => {
setLogs((prev) => [
...prev,
`${new Date().toISOString().slice(11, 23)} ${msg}`,
]);
};
const { spec, loading, connected, connecting, error } = useJsonRenderApp({
name: "json-render-mcp-example",
version: "1.0.0",
});
useEffect(() => {
addLog(
`State: connecting=${connecting} connected=${connected} error=${error?.message ?? "none"} loading=${loading} spec=${spec ? "yes" : "null"}`,
);
}, [connecting, connected, error, loading, spec]);
useEffect(() => {
if (spec) {
addLog(
`Spec received: root=${spec.root}, elements=${Object.keys(spec.elements ?? {}).join(",")}`,
);
}
}, [spec]);
if (error) {
return (
<div style={debugStyle}>
<div style={{ color: "#ef4444", fontWeight: "bold" }}>
Connection error: {error.message}
</div>
<div style={{ marginTop: 8 }}>{logs.join("\n")}</div>
</div>
);
}
if (!spec) {
return (
<div style={debugStyle}>
<div>
{connecting
? "Connecting to host..."
: loading
? "Waiting for UI spec..."
: "No spec received."}
</div>
<div style={{ marginTop: 8 }}>{logs.join("\n")}</div>
</div>
);
}
return (
<div>
<JSONUIProvider registry={registry} initialState={spec.state ?? {}}>
<Renderer spec={spec} registry={registry} loading={loading} />
</JSONUIProvider>
</div>
);
}
+17
View File
@@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
"noEmit": true,
"strict": true,
"skipLibCheck": true,
"jsx": "react-jsx"
},
"include": ["src", "server.ts"]
}
+15
View File
@@ -0,0 +1,15 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { viteSingleFile } from "vite-plugin-singlefile";
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
plugins: [react(), tailwindcss(), viteSingleFile()],
build: {
outDir: "dist",
emptyOutDir: false,
rollupOptions: {
input: "index.html",
},
},
});
+13
View File
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline' 'unsafe-eval' data: blob:; img-src * data: blob:; font-src * data:; connect-src *; media-src *;" />
<title>json-render MCP App (Svelte)</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
+28
View File
@@ -0,0 +1,28 @@
{
"name": "example-mcp-svelte",
"version": "0.1.0",
"type": "module",
"private": true,
"scripts": {
"build": "vite build",
"start": "tsx server.ts --stdio"
},
"dependencies": {
"@json-render/core": "workspace:*",
"@json-render/mcp": "workspace:*",
"@json-render/svelte": "workspace:*",
"@modelcontextprotocol/ext-apps": "^1.2.0",
"@modelcontextprotocol/sdk": "^1.27.1",
"svelte": "^5.49.2",
"zod": "^4.3.6"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^6.2.4",
"@tailwindcss/vite": "^4.2.1",
"tailwindcss": "^4.2.1",
"tsx": "^4.21.0",
"typescript": "^5.9.3",
"vite": "^7.3.1",
"vite-plugin-singlefile": "^2.3.0"
}
}
+34
View File
@@ -0,0 +1,34 @@
import { createMcpApp } from "@json-render/mcp";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { catalog } from "./src/catalog.js";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
function loadHtml(): string {
const htmlPath = path.join(__dirname, "dist", "index.html");
if (!fs.existsSync(htmlPath)) {
throw new Error(
`Built HTML not found at ${htmlPath}. Run 'pnpm build' first.`,
);
}
return fs.readFileSync(htmlPath, "utf-8");
}
async function main() {
const html = loadHtml();
const server = await createMcpApp({
name: "json-render Svelte Example",
version: "1.0.0",
catalog,
html,
});
await server.connect(new StdioServerTransport());
}
main().catch((e) => {
console.error(e);
process.exit(1);
});
+26
View File
@@ -0,0 +1,26 @@
<script lang="ts">
import { createJsonRenderApp } from "@json-render/mcp/app/svelte";
import { JsonUIProvider, Renderer } from "@json-render/svelte";
import { onDestroy } from "svelte";
import { registry } from "./registry";
const { spec, loading, connecting, error, destroy } = createJsonRenderApp({
name: "json-render-mcp-svelte",
version: "1.0.0",
});
onDestroy(destroy);
</script>
{#if $error}
<div class="p-4 text-destructive font-mono text-sm">
{$error.message}
</div>
{:else if !$spec}
<div class="p-4 text-muted-foreground text-sm">
{$connecting ? "Connecting to host..." : "Waiting for UI spec..."}
</div>
{:else}
<JsonUIProvider initialState={$spec.state ?? {}}>
<Renderer spec={$spec} {registry} loading={$loading} />
</JsonUIProvider>
{/if}
+54
View File
@@ -0,0 +1,54 @@
import { schema } from "@json-render/svelte/schema";
import { z } from "zod";
export const catalog = schema.createCatalog({
components: {
Stack: {
props: z.object({
gap: z.number().optional(),
padding: z.number().optional(),
direction: z.enum(["vertical", "horizontal"]).optional(),
align: z.enum(["start", "center", "end"]).optional(),
}),
slots: ["default"],
description:
"Layout container that stacks children vertically or horizontally",
},
Card: {
props: z.object({
title: z.string().optional(),
subtitle: z.string().optional(),
}),
slots: ["default"],
description: "A card container with optional title and subtitle",
},
Text: {
props: z.object({
content: z.string(),
size: z.enum(["sm", "md", "lg", "xl"]).optional(),
weight: z.enum(["normal", "medium", "bold"]).optional(),
color: z.string().optional(),
}),
slots: [],
description: "Displays a text string",
},
Button: {
props: z.object({
label: z.string(),
variant: z.enum(["primary", "secondary", "danger"]).optional(),
disabled: z.boolean().optional(),
}),
slots: [],
description: "A clickable button that emits a 'press' event",
},
Badge: {
props: z.object({
label: z.string(),
color: z.string().optional(),
}),
slots: [],
description: "A small badge/tag label",
},
},
actions: {},
});
@@ -0,0 +1,18 @@
<script lang="ts">
import type { BaseComponentProps } from "@json-render/svelte";
interface Props extends BaseComponentProps<{
label: string;
color?: string;
}> {}
let { props }: Props = $props();
</script>
<span
class="inline-flex items-center rounded-full px-3 py-1 text-xs font-medium bg-secondary text-secondary-foreground border border-border"
style:background-color={props.color ? `${props.color}20` : undefined}
style:color={props.color ?? undefined}
style:border-color={props.color ? `${props.color}40` : undefined}>
{props.label}
</span>
@@ -0,0 +1,26 @@
<script lang="ts">
import type { BaseComponentProps } from "@json-render/svelte";
interface Props extends BaseComponentProps<{
label: string;
variant?: "primary" | "secondary" | "danger";
disabled?: boolean;
}> {}
let { props, emit }: Props = $props();
const variantClasses: Record<string, string> = {
primary: "bg-primary text-primary-foreground hover:bg-primary/90",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
danger: "bg-destructive/10 text-destructive hover:bg-destructive/20 border border-destructive/20",
};
let variant = $derived(props.variant ?? "primary");
</script>
<button
disabled={props.disabled}
onclick={() => emit("press")}
class="inline-flex items-center justify-center rounded-lg px-4 py-2 text-sm font-medium transition-colors {variantClasses[variant] ?? variantClasses.primary} {props.disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}">
{props.label}
</button>
@@ -0,0 +1,29 @@
<script lang="ts">
import type { Snippet } from "svelte";
import type { BaseComponentProps } from "@json-render/svelte";
interface Props extends BaseComponentProps<{
title?: string;
subtitle?: string;
}> {
children?: Snippet;
}
let { props, children }: Props = $props();
</script>
<div class="rounded-xl border border-border bg-card text-card-foreground p-5 shadow-sm">
{#if props.title}
<h2 class="text-base font-semibold text-card-foreground mb-1">
{props.title}
</h2>
{/if}
{#if props.subtitle}
<p class="text-sm text-muted-foreground mb-3">
{props.subtitle}
</p>
{/if}
{#if children}
{@render children()}
{/if}
</div>
@@ -0,0 +1,33 @@
<script lang="ts">
import type { Snippet } from "svelte";
import type { BaseComponentProps } from "@json-render/svelte";
interface Props extends BaseComponentProps<{
gap?: number;
padding?: number;
direction?: "vertical" | "horizontal";
align?: "start" | "center" | "end";
}> {
children?: Snippet;
}
let { props, children }: Props = $props();
let horizontal = $derived(props.direction === "horizontal");
</script>
<div
class="flex"
style:flex-direction={horizontal ? "row" : "column"}
style:gap="{props.gap ?? 0}px"
style:padding="{props.padding ?? 0}px"
style:align-items={props.align === "start"
? "flex-start"
: props.align === "end"
? "flex-end"
: horizontal
? "center"
: "stretch"}>
{#if children}
{@render children()}
{/if}
</div>
@@ -0,0 +1,31 @@
<script lang="ts">
import type { BaseComponentProps } from "@json-render/svelte";
interface Props extends BaseComponentProps<{
content: string;
size?: "sm" | "md" | "lg" | "xl";
weight?: "normal" | "medium" | "bold";
color?: string;
}> {}
let { props }: Props = $props();
const sizeClasses: Record<string, string> = {
sm: "text-xs",
md: "text-sm",
lg: "text-base",
xl: "text-2xl",
};
const weightClasses: Record<string, string> = {
normal: "font-normal",
medium: "font-medium",
bold: "font-bold",
};
</script>
<span
class="{sizeClasses[props.size ?? 'md'] ?? 'text-sm'} {weightClasses[props.weight ?? 'normal'] ?? 'font-normal'} text-foreground"
style:color={props.color ?? undefined}>
{String(props.content ?? "")}
</span>
+98
View File
@@ -0,0 +1,98 @@
@import "tailwindcss";
@custom-variant dark (&:is([data-theme="dark"] *));
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--radius-2xl: calc(var(--radius) + 8px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
}
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
}
[data-theme="dark"] {
--background: var(--color-background-secondary, var(--vscode-editor-background, oklch(0.145 0 0)));
--foreground: var(--color-text-primary, var(--vscode-foreground, oklch(0.985 0 0)));
--card: var(--color-background-primary, var(--vscode-sideBar-background, oklch(0.205 0 0)));
--card-foreground: var(--color-text-primary, var(--vscode-foreground, oklch(0.985 0 0)));
--primary: var(--color-text-primary, var(--vscode-foreground, oklch(0.922 0 0)));
--primary-foreground: var(--color-background-primary, var(--vscode-sideBar-background, oklch(0.205 0 0)));
--secondary: var(--color-background-tertiary, var(--vscode-activityBar-background, oklch(0.269 0 0)));
--secondary-foreground: var(--color-text-primary, var(--vscode-foreground, oklch(0.985 0 0)));
--muted: var(--color-background-tertiary, var(--vscode-activityBar-background, oklch(0.269 0 0)));
--muted-foreground: var(--color-text-secondary, var(--vscode-descriptionForeground, oklch(0.708 0 0)));
--accent: var(--color-background-tertiary, var(--vscode-activityBar-background, oklch(0.269 0 0)));
--accent-foreground: var(--color-text-primary, var(--vscode-foreground, oklch(0.985 0 0)));
--destructive: var(--color-text-danger, var(--vscode-errorForeground, oklch(0.704 0.191 22.216)));
--border: var(--color-border-primary, var(--vscode-widget-border, oklch(1 0 0 / 10%)));
--input: var(--color-border-secondary, var(--vscode-editorWidget-border, oklch(1 0 0 / 15%)));
--ring: var(--color-ring-primary, var(--vscode-focusBorder, oklch(0.556 0 0)));
}
* {
box-sizing: border-box;
border-color: var(--color-border);
}
html, body, #app {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
body {
background-color: var(--color-background);
color: var(--color-foreground);
font-family: var(--vscode-font-family, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply text-foreground;
background-color: var(--color-background) !important;
}
}
button {
cursor: pointer;
}
+12
View File
@@ -0,0 +1,12 @@
import "./globals.css";
import { mount } from "svelte";
import App from "./App.svelte";
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
document.documentElement.setAttribute(
"data-theme",
prefersDark ? "dark" : "light",
);
document.documentElement.style.colorScheme = prefersDark ? "dark" : "light";
mount(App, { target: document.getElementById("app")! });
+18
View File
@@ -0,0 +1,18 @@
import { type Components, defineRegistry } from "@json-render/svelte";
import { catalog } from "./catalog";
import Stack from "./components/Stack.svelte";
import Card from "./components/Card.svelte";
import Text from "./components/Text.svelte";
import Button from "./components/Button.svelte";
import Badge from "./components/Badge.svelte";
const components: Components<typeof catalog> = {
Stack,
Card,
Text,
Button,
Badge,
};
export const { registry } = defineRegistry(catalog, { components });
+1
View File
@@ -0,0 +1 @@
export default {};
+16
View File
@@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
"noEmit": true,
"strict": true,
"skipLibCheck": true
},
"include": ["src", "server.ts"]
}
+14
View File
@@ -0,0 +1,14 @@
import { defineConfig } from "vite";
import { svelte } from "@sveltejs/vite-plugin-svelte";
import { viteSingleFile } from "vite-plugin-singlefile";
export default defineConfig({
plugins: [svelte(), viteSingleFile()],
build: {
outDir: "dist",
emptyOutDir: false,
rollupOptions: {
input: "index.html",
},
},
});
+13
View File
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline' 'unsafe-eval' data: blob:; img-src * data: blob:; font-src * data:; connect-src *; media-src *;" />
<title>json-render MCP App (Vue)</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
+29
View File
@@ -0,0 +1,29 @@
{
"name": "example-mcp-vue",
"version": "0.1.0",
"type": "module",
"private": true,
"scripts": {
"build": "vite build",
"start": "tsx server.ts",
"start:stdio": "tsx server.ts --stdio"
},
"dependencies": {
"@json-render/core": "workspace:*",
"@json-render/mcp": "workspace:*",
"@json-render/vue": "workspace:*",
"@modelcontextprotocol/ext-apps": "^1.2.0",
"@modelcontextprotocol/sdk": "^1.27.1",
"vue": "^3.5.0",
"zod": "^4.3.6"
},
"devDependencies": {
"@tailwindcss/vite": "^4.2.1",
"@vitejs/plugin-vue": "^6.0.4",
"tailwindcss": "^4.2.1",
"tsx": "^4.21.0",
"typescript": "^5.9.3",
"vite": "^7.3.1",
"vite-plugin-singlefile": "^2.3.0"
}
}
+34
View File
@@ -0,0 +1,34 @@
import { createMcpApp } from "@json-render/mcp";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { catalog } from "./src/catalog.js";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
function loadHtml(): string {
const htmlPath = path.join(__dirname, "dist", "index.html");
if (!fs.existsSync(htmlPath)) {
throw new Error(
`Built HTML not found at ${htmlPath}. Run 'pnpm build' first.`,
);
}
return fs.readFileSync(htmlPath, "utf-8");
}
async function main() {
const html = loadHtml();
const server = await createMcpApp({
name: "json-render Vue Example",
version: "1.0.0",
catalog,
html,
});
await server.connect(new StdioServerTransport());
}
main().catch((e) => {
console.error(e);
process.exit(1);
});
+31
View File
@@ -0,0 +1,31 @@
<script setup lang="ts">
import { useJsonRenderApp } from "@json-render/mcp/app/vue";
import { Renderer, defineRegistry, JSONUIProvider } from "@json-render/vue";
import { catalog } from "./catalog";
import { components } from "./registry";
const { registry } = defineRegistry(catalog, { components });
const { spec, loading, connecting, error } = useJsonRenderApp({
name: "json-render-mcp-vue",
version: "1.0.0",
});
</script>
<template>
<div
v-if="error"
class="p-4 font-mono text-sm text-destructive"
>
{{ error.message }}
</div>
<div
v-else-if="!spec"
class="p-4 text-sm text-muted-foreground"
>
{{ connecting ? "Connecting to host..." : "Waiting for UI spec..." }}
</div>
<JSONUIProvider v-else :registry="registry" :initialState="spec.state ?? {}">
<Renderer :spec="spec" :registry="registry" :loading="loading" />
</JSONUIProvider>
</template>
+56
View File
@@ -0,0 +1,56 @@
import { schema } from "@json-render/vue/schema";
import { z } from "zod";
export const catalog = schema.createCatalog({
components: {
Stack: {
props: z.object({
gap: z.number().optional(),
padding: z.number().optional(),
direction: z.enum(["vertical", "horizontal"]).optional(),
align: z.enum(["start", "center", "end"]).optional(),
}),
slots: ["default"],
description:
"Layout container that stacks children vertically or horizontally",
},
Card: {
props: z.object({
title: z.string().optional(),
subtitle: z.string().optional(),
}),
slots: ["default"],
description: "A card container with optional title and subtitle",
},
Text: {
props: z.object({
content: z.string(),
size: z.enum(["sm", "md", "lg", "xl"]).optional(),
weight: z.enum(["normal", "medium", "bold"]).optional(),
color: z.string().optional(),
}),
slots: [],
description: "Displays a text string",
},
Button: {
props: z.object({
label: z.string(),
variant: z.enum(["primary", "secondary", "danger"]).optional(),
disabled: z.boolean().optional(),
}),
slots: [],
description: "A clickable button that emits a 'press' event",
},
Badge: {
props: z.object({
label: z.string(),
color: z.string().optional(),
}),
slots: [],
description: "A small badge/tag label",
},
},
actions: {},
});
export type AppCatalog = typeof catalog;
+98
View File
@@ -0,0 +1,98 @@
@import "tailwindcss";
@custom-variant dark (&:is([data-theme="dark"] *));
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--radius-2xl: calc(var(--radius) + 8px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
}
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
}
[data-theme="dark"] {
--background: var(--color-background-secondary, var(--vscode-editor-background, oklch(0.145 0 0)));
--foreground: var(--color-text-primary, var(--vscode-foreground, oklch(0.985 0 0)));
--card: var(--color-background-primary, var(--vscode-sideBar-background, oklch(0.205 0 0)));
--card-foreground: var(--color-text-primary, var(--vscode-foreground, oklch(0.985 0 0)));
--primary: var(--color-text-primary, var(--vscode-foreground, oklch(0.922 0 0)));
--primary-foreground: var(--color-background-primary, var(--vscode-sideBar-background, oklch(0.205 0 0)));
--secondary: var(--color-background-tertiary, var(--vscode-activityBar-background, oklch(0.269 0 0)));
--secondary-foreground: var(--color-text-primary, var(--vscode-foreground, oklch(0.985 0 0)));
--muted: var(--color-background-tertiary, var(--vscode-activityBar-background, oklch(0.269 0 0)));
--muted-foreground: var(--color-text-secondary, var(--vscode-descriptionForeground, oklch(0.708 0 0)));
--accent: var(--color-background-tertiary, var(--vscode-activityBar-background, oklch(0.269 0 0)));
--accent-foreground: var(--color-text-primary, var(--vscode-foreground, oklch(0.985 0 0)));
--destructive: var(--color-text-danger, var(--vscode-errorForeground, oklch(0.704 0.191 22.216)));
--border: var(--color-border-primary, var(--vscode-widget-border, oklch(1 0 0 / 10%)));
--input: var(--color-border-secondary, var(--vscode-editorWidget-border, oklch(1 0 0 / 15%)));
--ring: var(--color-ring-primary, var(--vscode-focusBorder, oklch(0.556 0 0)));
}
* {
box-sizing: border-box;
border-color: var(--color-border);
}
html, body, #app {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
body {
background-color: var(--color-background);
color: var(--color-foreground);
font-family: var(--vscode-font-family, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply text-foreground;
background-color: var(--color-background) !important;
}
}
button {
cursor: pointer;
}
+13
View File
@@ -0,0 +1,13 @@
import "./globals.css";
import { createApp } from "vue";
import App from "./App.vue";
// Fallback theme before MCP host provides context
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
document.documentElement.setAttribute(
"data-theme",
prefersDark ? "dark" : "light",
);
document.documentElement.style.colorScheme = prefersDark ? "dark" : "light";
createApp(App).mount("#app");
+116
View File
@@ -0,0 +1,116 @@
import { h } from "vue";
import type { Components } from "@json-render/vue";
import type { AppCatalog } from "./catalog";
export const components: Components<AppCatalog> = {
Stack: ({ props, children }) => {
const horizontal = props.direction === "horizontal";
return h(
"div",
{
class: "flex",
style: {
flexDirection: horizontal ? "row" : "column",
gap: props.gap ? `${props.gap}px` : undefined,
padding: props.padding ? `${props.padding}px` : undefined,
alignItems: props.align ?? (horizontal ? "center" : "stretch"),
},
},
children,
);
},
Card: ({ props, children }) =>
h(
"div",
{
class:
"rounded-xl border border-border bg-card text-card-foreground p-5 shadow-sm",
},
[
props.title &&
h(
"h2",
{
class: "text-base font-semibold text-card-foreground mb-1",
},
props.title,
),
props.subtitle &&
h(
"p",
{
class: "text-sm text-muted-foreground mb-3",
},
props.subtitle,
),
children,
],
),
Text: ({ props }) => {
const sizeClasses: Record<string, string> = {
sm: "text-xs",
md: "text-sm",
lg: "text-base",
xl: "text-2xl",
};
const weightClasses: Record<string, string> = {
normal: "font-normal",
medium: "font-medium",
bold: "font-bold",
};
return h(
"span",
{
class: [
sizeClasses[props.size ?? "md"] ?? "text-sm",
weightClasses[props.weight ?? "normal"] ?? "font-normal",
"text-foreground",
].join(" "),
style: props.color ? { color: props.color } : undefined,
},
String(props.content ?? ""),
);
},
Button: ({ props, emit }) => {
const variant = props.variant ?? "primary";
const variantClasses: Record<string, string> = {
primary: "bg-primary text-primary-foreground hover:bg-primary/90",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
danger:
"bg-destructive/10 text-destructive hover:bg-destructive/20 border border-destructive/20",
};
return h(
"button",
{
disabled: props.disabled,
onClick: () => emit("press"),
class: [
"inline-flex items-center justify-center rounded-lg px-4 py-2 text-sm font-medium transition-colors",
variantClasses[variant] ?? variantClasses.primary,
props.disabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer",
].join(" "),
},
props.label,
);
},
Badge: ({ props }) =>
h(
"span",
{
class:
"inline-flex items-center rounded-full px-3 py-1 text-xs font-medium bg-secondary text-secondary-foreground border border-border",
style: props.color
? {
backgroundColor: `${props.color}20`,
color: props.color,
borderColor: `${props.color}40`,
}
: undefined,
},
props.label,
),
};
+17
View File
@@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
"noEmit": true,
"strict": true,
"skipLibCheck": true,
"jsx": "preserve"
},
"include": ["src", "server.ts"]
}
+15
View File
@@ -0,0 +1,15 @@
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import { viteSingleFile } from "vite-plugin-singlefile";
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
plugins: [vue(), tailwindcss(), viteSingleFile()],
build: {
outDir: "dist",
emptyOutDir: false,
rollupOptions: {
input: "index.html",
},
},
});
+9
View File
@@ -1,5 +1,14 @@
# example-no-ai
## 0.1.5
### Patch Changes
- Updated dependencies [63c339b]
- @json-render/core@0.12.0
- @json-render/react@0.12.0
- @json-render/shadcn@0.12.0
## 0.1.4
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts";
import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-no-ai",
"version": "0.1.4",
"version": "0.1.5",
"type": "module",
"private": true,
"scripts": {
+9
View File
@@ -0,0 +1,9 @@
# example-react-email
## 0.1.1
### Patch Changes
- Updated dependencies [63c339b]
- @json-render/core@0.12.0
- @json-render/react-email@0.12.0
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "example-react-email",
"version": "0.1.0",
"version": "0.1.1",
"type": "module",
"private": true,
"scripts": {
@@ -25,7 +25,7 @@
"react-resizable-panels": "^4.4.1",
"tailwind-merge": "^3.5.0",
"tailwindcss": "^4.1.18",
"zod": "4.3.5"
"zod": "4.3.6"
},
"devDependencies": {
"@internal/typescript-config": "workspace:*",
+8
View File
@@ -1,5 +1,13 @@
# example-react-native
## 0.1.5
### Patch Changes
- Updated dependencies [63c339b]
- @json-render/core@0.12.0
- @json-render/react-native@0.12.0
## 0.1.4
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-react-native",
"version": "0.1.4",
"version": "0.1.5",
"private": true,
"main": "expo-router/entry",
"scripts": {
+8
View File
@@ -1,5 +1,13 @@
# example-react-pdf
## 0.1.5
### Patch Changes
- Updated dependencies [63c339b]
- @json-render/core@0.12.0
- @json-render/react-pdf@0.12.0
## 0.1.4
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts";
import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-react-pdf",
"version": "0.1.4",
"version": "0.1.5",
"type": "module",
"private": true,
"scripts": {
+8
View File
@@ -1,5 +1,13 @@
# example-remotion
## 0.1.5
### Patch Changes
- Updated dependencies [63c339b]
- @json-render/core@0.12.0
- @json-render/remotion@0.12.0
## 0.1.4
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts";
import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-remotion",
"version": "0.1.4",
"version": "0.1.5",
"type": "module",
"private": true,
"scripts": {
+1 -1
View File
@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts";
import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
@@ -1,5 +1,13 @@
# com.example.json-render-demo
## 0.0.6
### Patch Changes
- Updated dependencies [63c339b]
- @json-render/core@0.12.0
- @json-render/react@0.12.0
## 0.0.5
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "com.example.json-render-demo",
"version": "0.0.5",
"version": "0.0.6",
"description": "Test",
"private": true,
"license": "~~proprietary~~",
@@ -1,5 +1,13 @@
# com.example.json-render-fullpage-demo
## 0.0.6
### Patch Changes
- Updated dependencies [63c339b]
- @json-render/core@0.12.0
- @json-render/react@0.12.0
## 0.0.5
### Patch Changes
@@ -1,6 +1,6 @@
{
"name": "com.example.json-render-fullpage-demo",
"version": "0.0.5",
"version": "0.0.6",
"description": "Full-page Stripe App example (alpha)",
"private": true,
"license": "~~proprietary~~",
+23
View File
@@ -0,0 +1,23 @@
node_modules
# Output
.output
.vercel
.netlify
.wrangler
/.svelte-kit
/build
# OS
.DS_Store
Thumbs.db
# Env
.env
.env.*
!.env.example
!.env.test
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
+1
View File
@@ -0,0 +1 @@
engine-strict=true
+9
View File
@@ -0,0 +1,9 @@
# svelte-chat
## 0.0.2
### Patch Changes
- Updated dependencies [63c339b]
- @json-render/core@0.12.0
- @json-render/svelte@0.12.0
+42
View File
@@ -0,0 +1,42 @@
# sv
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
## Creating a project
If you're seeing this, you've probably already done this step. Congrats!
```sh
# create a new project
npx sv create my-app
```
To recreate this project with the same configuration:
```sh
# recreate this project
npx sv create --template minimal --types ts --no-install svelte-chat
```
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```sh
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building
To create a production version of your app:
```sh
npm run build
```
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.
+16
View File
@@ -0,0 +1,16 @@
{
"$schema": "https://shadcn-svelte.com/schema.json",
"tailwind": {
"css": "src/app.css",
"baseColor": "zinc"
},
"aliases": {
"components": "$lib/components",
"utils": "$lib/utils",
"ui": "$lib/components/ui",
"hooks": "$lib/hooks",
"lib": "$lib"
},
"typescript": true,
"registry": "https://shadcn-svelte.com/registry"
}
+39
View File
@@ -0,0 +1,39 @@
{
"name": "svelte-chat",
"private": true,
"version": "0.0.2",
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"prepare": "svelte-kit sync || echo ''",
"check-types": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json"
},
"devDependencies": {
"@internationalized/date": "^3.10.0",
"@lucide/svelte": "^0.561.0",
"@sveltejs/adapter-auto": "^7.0.0",
"@sveltejs/kit": "^2.50.2",
"@sveltejs/vite-plugin-svelte": "^6.2.4",
"@tailwindcss/vite": "^4.0.0",
"bits-ui": "^2.14.4",
"svelte": "^5.49.2",
"svelte-check": "^4.3.6",
"tailwind-variants": "^3.2.2",
"tailwindcss": "^4.0.0",
"typescript": "^5.9.3",
"vite": "^7.3.1"
},
"dependencies": {
"@ai-sdk/gateway": "^3.0.46",
"@ai-sdk/svelte": "^4.0.96",
"@json-render/core": "workspace:*",
"@json-render/svelte": "workspace:*",
"ai": "^6.0.86",
"clsx": "^2.1.1",
"lucide-svelte": "^0.500.0",
"tailwind-merge": "^3.2.0",
"zod": "4.3.6"
}
}
+125
View File
@@ -0,0 +1,125 @@
@import "tailwindcss";
@custom-variant dark (&:is(.dark *));
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--radius-2xl: calc(var(--radius) + 8px);
--radius-3xl: calc(var(--radius) + 12px);
--radius-4xl: calc(var(--radius) + 16px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
}
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}
button {
cursor: pointer;
}
@keyframes shimmer {
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
}
}
.animate-shimmer {
background: linear-gradient(
90deg,
currentColor 25%,
hsl(0 0% 64%) 50%,
currentColor 75%
);
background-size: 200% 100%;
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
animation: shimmer 2s ease-in-out infinite;
}
+13
View File
@@ -0,0 +1,13 @@
// See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};
+11
View File
@@ -0,0 +1,11 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>
+92
View File
@@ -0,0 +1,92 @@
import { ToolLoopAgent, stepCountIs } from "ai";
import { createGateway } from "@ai-sdk/gateway";
import { explorerCatalog } from "./render/catalog";
import { getWeather } from "./tools/weather";
import { getGitHubRepo, getGitHubPullRequests } from "./tools/github";
import { getCryptoPrice, getCryptoPriceHistory } from "./tools/crypto";
import { getHackerNewsTop } from "./tools/hackernews";
import { webSearch } from "./tools/search";
import { env } from "$env/dynamic/private";
const DEFAULT_MODEL = "anthropic/claude-haiku-4.5";
const AGENT_INSTRUCTIONS = `You are a knowledgeable assistant that helps users explore data and learn about any topic. You look up real-time information, build visual dashboards, and create rich educational content.
WORKFLOW:
1. Call the appropriate tools to gather relevant data. Use webSearch for general topics not covered by specialized tools.
2. Respond with a brief, conversational summary of what you found.
3. Then output the JSONL UI spec wrapped in a \`\`\`spec fence to render a rich visual experience.
RULES:
- Always call tools FIRST to get real data. Never make up data.
- Embed the fetched data directly in /state paths so components can reference it.
- Use Card components to group related information.
- NEVER nest a Card inside another Card. If you need sub-sections inside a Card, use Stack, Separator, Heading, or Accordion instead.
- Use Grid for multi-column layouts.
- Use Metric for key numeric values (temperature, stars, price, etc.).
- Use Table for lists of items (stories, forecasts, languages, etc.).
- Use BarChart or LineChart for numeric trends and time-series data.
- Use PieChart for compositional/proportional data (market share, breakdowns, distributions).
- Use Tabs when showing multiple categories of data side by side.
- Use Badge for status indicators.
- Use Callout for key facts, tips, warnings, or important takeaways.
- Use Accordion to organize detailed sections the user can expand for deeper reading.
- Use Timeline for historical events, processes, step-by-step explanations, or milestones.
- When teaching about a topic, combine multiple component types to create a rich, engaging experience.
DATA BINDING:
- The state model is the single source of truth. Put fetched data in /state, then reference it with { "$state": "/json/pointer" } in any prop.
- $state works on ANY prop at ANY nesting level. The renderer resolves expressions before components receive props.
- Scalar binding: "title": { "$state": "/quiz/title" }
- Array binding: "items": { "$state": "/quiz/questions" } (for Accordion, Timeline, etc.)
- For Table, BarChart, LineChart, and PieChart, use { "$state": "/path" } on the data prop to bind read-only data from state.
- Always emit /state patches BEFORE the elements that reference them, so data is available when the UI renders.
- Always use the { "$state": "/foo" } object syntax for data binding.
INTERACTIVITY:
- You can use visible, repeat, on.press, and $cond/$then/$else freely.
- visible: Conditionally show/hide elements based on state. e.g. "visible": { "$state": "/q1/answer", "eq": "a" }
- repeat: Iterate over state arrays. e.g. "repeat": { "statePath": "/items" }
- on.press: Trigger actions on button clicks. e.g. "on": { "press": { "action": "setState", "params": { "statePath": "/submitted", "value": true } } }
- $cond/$then/$else: Conditional prop values. e.g. { "$cond": { "$state": "/correct" }, "$then": "Correct!", "$else": "Try again" }
BUILT-IN ACTIONS (use with on.press):
- setState: Set a value at a state path. params: { statePath: "/foo", value: "bar" }
- pushState: Append to an array. params: { statePath: "/items", value: { ... } }
- removeState: Remove by index. params: { statePath: "/items", index: 0 }
INPUT COMPONENTS:
- RadioGroup: Renders radio buttons. Writes selected value to statePath automatically.
- SelectInput: Dropdown select. Writes selected value to statePath automatically.
- TextInput: Text input field. Writes entered value to statePath automatically.
- Button: Clickable button. Use on.press to trigger actions.
${explorerCatalog.prompt({
mode: "chat",
customRules: [
"NEVER use viewport height classes (min-h-screen, h-screen) — the UI renders inside a fixed-size container.",
"Prefer Grid with columns='2' or columns='3' for side-by-side layouts.",
"Use Metric components for key numbers instead of plain Text.",
"Put chart data arrays in /state and reference them with { $state: '/path' } on the data prop.",
"Keep the UI clean and information-dense — no excessive padding or empty space.",
"For educational prompts ('teach me about', 'explain', 'what is'), use a mix of Callout, Accordion, Timeline, and charts to make the content visually rich.",
],
})}`;
export const gateway = createGateway({ apiKey: env.AI_GATEWAY_API_KEY });
export const agent = new ToolLoopAgent({
model: gateway(env.AI_GATEWAY_MODEL || DEFAULT_MODEL),
instructions: AGENT_INSTRUCTIONS,
tools: {
getWeather,
getGitHubRepo,
getGitHubPullRequests,
getCryptoPrice,
getCryptoPriceHistory,
getHackerNewsTop,
webSearch,
},
stopWhen: stepCountIs(5),
temperature: 0.7,
});
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.157 22.819c-10.4-14.885-30.94-19.297-45.792-9.835L22.282 29.608A29.92 29.92 0 0 0 8.764 49.65a31.5 31.5 0 0 0 3.108 20.231 30 30 0 0 0-4.477 11.183 31.9 31.9 0 0 0 5.448 24.116c10.402 14.887 30.942 19.297 45.791 9.835l26.083-16.624A29.92 29.92 0 0 0 98.235 78.35a31.53 31.53 0 0 0-3.105-20.232 30 30 0 0 0 4.474-11.182 31.88 31.88 0 0 0-5.447-24.116" style="fill:#ff3e00"/><path d="M45.817 106.582a20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.503 18 18 0 0 1 .624-2.435l.49-1.498 1.337.981a33.6 33.6 0 0 0 10.203 5.098l.97.294-.09.968a5.85 5.85 0 0 0 1.052 3.878 6.24 6.24 0 0 0 6.695 2.485 5.8 5.8 0 0 0 1.603-.704L69.27 76.28a5.43 5.43 0 0 0 2.45-3.631 5.8 5.8 0 0 0-.987-4.371 6.24 6.24 0 0 0-6.698-2.487 5.7 5.7 0 0 0-1.6.704l-9.953 6.345a19 19 0 0 1-5.296 2.326 20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.502 17.99 17.99 0 0 1 8.13-12.052l26.081-16.623a19 19 0 0 1 5.3-2.329 20.72 20.72 0 0 1 22.237 8.243 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-.624 2.435l-.49 1.498-1.337-.98a33.6 33.6 0 0 0-10.203-5.1l-.97-.294.09-.968a5.86 5.86 0 0 0-1.052-3.878 6.24 6.24 0 0 0-6.696-2.485 5.8 5.8 0 0 0-1.602.704L37.73 51.72a5.42 5.42 0 0 0-2.449 3.63 5.79 5.79 0 0 0 .986 4.372 6.24 6.24 0 0 0 6.698 2.486 5.8 5.8 0 0 0 1.602-.704l9.952-6.342a19 19 0 0 1 5.295-2.328 20.72 20.72 0 0 1 22.237 8.242 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-8.13 12.053l-26.081 16.622a19 19 0 0 1-5.3 2.328" style="fill:#fff"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,22 @@
<script lang="ts">
import { Accordion as AccordionPrimitive } from "bits-ui";
import { cn, type WithoutChild } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithoutChild<AccordionPrimitive.ContentProps> = $props();
</script>
<AccordionPrimitive.Content
bind:ref
data-slot="accordion-content"
class="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
{...restProps}
>
<div class={cn("pt-0 pb-4", className)}>
{@render children?.()}
</div>
</AccordionPrimitive.Content>
@@ -0,0 +1,17 @@
<script lang="ts">
import { Accordion as AccordionPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: AccordionPrimitive.ItemProps = $props();
</script>
<AccordionPrimitive.Item
bind:ref
data-slot="accordion-item"
class={cn("border-b last:border-b-0", className)}
{...restProps}
/>
@@ -0,0 +1,32 @@
<script lang="ts">
import { Accordion as AccordionPrimitive } from "bits-ui";
import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
import { cn, type WithoutChild } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
level = 3,
children,
...restProps
}: WithoutChild<AccordionPrimitive.TriggerProps> & {
level?: AccordionPrimitive.HeaderProps["level"];
} = $props();
</script>
<AccordionPrimitive.Header {level} class="flex">
<AccordionPrimitive.Trigger
data-slot="accordion-trigger"
bind:ref
class={cn(
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-start text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
className
)}
{...restProps}
>
{@render children?.()}
<ChevronDownIcon
class="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200"
/>
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
@@ -0,0 +1,16 @@
<script lang="ts">
import { Accordion as AccordionPrimitive } from "bits-ui";
let {
ref = $bindable(null),
value = $bindable(),
...restProps
}: AccordionPrimitive.RootProps = $props();
</script>
<AccordionPrimitive.Root
bind:ref
bind:value={value as never}
data-slot="accordion"
{...restProps}
/>
@@ -0,0 +1,16 @@
import Root from "./accordion.svelte";
import Content from "./accordion-content.svelte";
import Item from "./accordion-item.svelte";
import Trigger from "./accordion-trigger.svelte";
export {
Root,
Content,
Item,
Trigger,
//
Root as Accordion,
Content as AccordionContent,
Item as AccordionItem,
Trigger as AccordionTrigger,
};
@@ -0,0 +1,23 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="alert-description"
class={cn(
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
className
)}
{...restProps}
>
{@render children?.()}
</div>
@@ -0,0 +1,20 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="alert-title"
class={cn("col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight", className)}
{...restProps}
>
{@render children?.()}
</div>

Some files were not shown because too many files have changed in this diff Show More