mirror of
https://github.com/vercel-labs/json-render.git
synced 2026-10-03 20:38:21 +08:00
Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
caa90b8b84 | ||
|
|
54a1ecf817 | ||
|
|
dc9e9d17f3 | ||
|
|
1977fb3a11 | ||
|
|
4606c01085 | ||
|
|
c1a700d719 | ||
|
|
6f15faaae0 | ||
|
|
63c339b4bb | ||
|
|
1cc87310c9 | ||
|
|
5b929dffa6 |
@@ -17,7 +17,9 @@
|
|||||||
"@json-render/jotai",
|
"@json-render/jotai",
|
||||||
"@json-render/vue",
|
"@json-render/vue",
|
||||||
"@json-render/xstate",
|
"@json-render/xstate",
|
||||||
"@json-render/image"
|
"@json-render/image",
|
||||||
|
"@json-render/mcp",
|
||||||
|
"@json-render/svelte"
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"linked": [],
|
"linked": [],
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"json-render": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["tsx", "examples/mcp/server.ts", "--stdio"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
node_modules
|
node_modules
|
||||||
.pnp
|
.pnp
|
||||||
.pnp.js
|
.pnp.js
|
||||||
|
.pnpm-store/
|
||||||
|
|
||||||
# Local env files
|
# Local env files
|
||||||
.env*
|
.env*
|
||||||
|
|||||||
Vendored
+9
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"servers": {
|
||||||
|
"json-render": {
|
||||||
|
"type": "stdio",
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["tsx", "examples/mcp/server.ts", "--stdio"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 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 `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`)
|
- 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`
|
- A `packages/<name>/README.md`
|
||||||
6. **Run `pnpm type-check`** after all changes to verify nothing is broken
|
6. **Run `pnpm type-check`** after all changes to verify nothing is broken
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
npm install @json-render/core @json-render/react-email @react-email/components @react-email/render
|
||||||
# or for Vue
|
# or for Vue
|
||||||
npm install @json-render/core @json-render/vue
|
npm install @json-render/core @json-render/vue
|
||||||
|
# or for Svelte
|
||||||
|
npm install @json-render/core @json-render/svelte
|
||||||
```
|
```
|
||||||
|
|
||||||
## Why json-render?
|
## 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
|
- **Guardrailed** - AI can only use components in your catalog
|
||||||
- **Predictable** - JSON output matches your schema, every time
|
- **Predictable** - JSON output matches your schema, every time
|
||||||
- **Fast** - Stream and render progressively as the model responds
|
- **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
|
- **Batteries Included** - 36 pre-built shadcn/ui components ready to use
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
@@ -116,16 +118,19 @@ function Dashboard({ spec }) {
|
|||||||
| `@json-render/core` | Schemas, catalogs, AI prompts, dynamic props, SpecStream utilities |
|
| `@json-render/core` | Schemas, catalogs, AI prompts, dynamic props, SpecStream utilities |
|
||||||
| `@json-render/react` | React renderer, contexts, hooks |
|
| `@json-render/react` | React renderer, contexts, hooks |
|
||||||
| `@json-render/vue` | Vue 3 renderer, composables, providers |
|
| `@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/shadcn` | 36 pre-built shadcn/ui components (Radix UI + Tailwind CSS) |
|
||||||
| `@json-render/react-native` | React Native renderer with standard mobile components |
|
| `@json-render/react-native` | React Native renderer with standard mobile components |
|
||||||
| `@json-render/remotion` | Remotion video renderer, timeline schema |
|
| `@json-render/remotion` | Remotion video renderer, timeline schema |
|
||||||
| `@json-render/react-pdf` | React PDF renderer for generating PDF documents from specs |
|
| `@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/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/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/redux` | Redux / Redux Toolkit adapter for `StateStore` |
|
||||||
| `@json-render/zustand` | Zustand adapter for `StateStore` |
|
| `@json-render/zustand` | Zustand adapter for `StateStore` |
|
||||||
| `@json-render/jotai` | Jotai adapter for `StateStore` |
|
| `@json-render/jotai` | Jotai adapter for `StateStore` |
|
||||||
| `@json-render/xstate` | XState Store (atom) 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
|
## Renderers
|
||||||
|
|
||||||
@@ -177,6 +182,23 @@ const { registry } = defineRegistry(catalog, {
|
|||||||
// <Renderer :spec="spec" :registry="registry" />
|
// <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)
|
### shadcn/ui (Web)
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
|
|||||||
@@ -1,5 +1,23 @@
|
|||||||
# web
|
# web
|
||||||
|
|
||||||
|
## 0.1.6
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [54a1ecf]
|
||||||
|
- @json-render/core@0.12.1
|
||||||
|
- @json-render/codegen@0.12.1
|
||||||
|
- @json-render/react@0.12.1
|
||||||
|
|
||||||
|
## 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
|
## 0.1.4
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ export const metadata = pageMetadata("docs/ai-sdk")
|
|||||||
|
|
||||||
# AI SDK Integration
|
# AI SDK Integration
|
||||||
|
|
||||||
Use json-render with the [Vercel AI SDK](https://sdk.vercel.ai) for seamless streaming. json-render supports two modes: **Generate** (standalone UI) and **Chat** (UI embedded in conversation). See [Generation Modes](/docs/generation-modes) for a detailed comparison.
|
Use json-render with the [Vercel AI SDK](https://sdk.vercel.ai) for seamless streaming. json-render supports two modes: **Standalone** (standalone UI) and **Inline** (UI embedded in conversation). See [Generation Modes](/docs/generation-modes) for a detailed comparison.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@@ -11,9 +11,9 @@ Use json-render with the [Vercel AI SDK](https://sdk.vercel.ai) for seamless str
|
|||||||
npm install ai @ai-sdk/react
|
npm install ai @ai-sdk/react
|
||||||
```
|
```
|
||||||
|
|
||||||
## Generate Mode
|
## Standalone Mode
|
||||||
|
|
||||||
In generate mode, the AI outputs only JSONL patches. The entire response is a UI spec with no prose. This is the default mode and is ideal for playgrounds, builders, and dashboard generators.
|
In standalone mode, the AI outputs only JSONL patches. The entire response is a UI spec with no prose. This is the default mode and is ideal for playgrounds, builders, and dashboard generators.
|
||||||
|
|
||||||
### API Route
|
### API Route
|
||||||
|
|
||||||
@@ -73,9 +73,9 @@ function GenerativeUI() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Chat Mode
|
## Inline Mode
|
||||||
|
|
||||||
In chat mode, the AI responds conversationally and includes JSONL patches inline. Text-only replies are allowed when no UI is needed. This is ideal for chatbots, copilots, and educational assistants.
|
In inline mode, the AI responds conversationally and includes JSONL patches inline. Text-only replies are allowed when no UI is needed. This is ideal for chatbots, copilots, and educational assistants.
|
||||||
|
|
||||||
### API Route
|
### API Route
|
||||||
|
|
||||||
@@ -96,7 +96,7 @@ export async function POST(req: Request) {
|
|||||||
|
|
||||||
const result = streamText({
|
const result = streamText({
|
||||||
model: yourModel,
|
model: yourModel,
|
||||||
system: catalog.prompt({ mode: "chat" }),
|
system: catalog.prompt({ mode: "inline" }),
|
||||||
messages,
|
messages,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -182,13 +182,13 @@ const systemPrompt = catalog.prompt({
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
### Chat Mode Prompt
|
### Inline Mode Prompt
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const chatPrompt = catalog.prompt({ mode: "chat" });
|
const inlinePrompt = catalog.prompt({ mode: "inline" });
|
||||||
```
|
```
|
||||||
|
|
||||||
In chat mode, the prompt instructs the AI to respond conversationally first, then include JSONL patches on their own lines when UI is needed. Text-only replies are allowed.
|
In inline mode, the prompt instructs the AI to respond conversationally first, then include JSONL patches on their own lines when UI is needed. Text-only replies are allowed.
|
||||||
|
|
||||||
## Which Mode?
|
## Which Mode?
|
||||||
|
|
||||||
@@ -197,8 +197,8 @@ In chat mode, the prompt instructs the AI to respond conversationally first, the
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th></th>
|
<th></th>
|
||||||
<th>Generate</th>
|
<th>Standalone</th>
|
||||||
<th>Chat</th>
|
<th>Inline</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -215,7 +215,7 @@ In chat mode, the prompt instructs the AI to respond conversationally first, the
|
|||||||
<tr>
|
<tr>
|
||||||
<td>System prompt</td>
|
<td>System prompt</td>
|
||||||
<td><code>catalog.prompt()</code></td>
|
<td><code>catalog.prompt()</code></td>
|
||||||
<td><code>{"catalog.prompt({ mode: \"chat\" })"}</code></td>
|
<td><code>{"catalog.prompt({ mode: \"inline\" })"}</code></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Stream utility</td>
|
<td>Stream utility</td>
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ interface Catalog {
|
|||||||
interface PromptOptions {
|
interface PromptOptions {
|
||||||
system?: string; // Custom system message intro
|
system?: string; // Custom system message intro
|
||||||
customRules?: string[]; // Additional rules to append
|
customRules?: string[]; // Additional rules to append
|
||||||
mode?: "generate" | "chat"; // Output mode (default: "generate")
|
mode?: "standalone" | "inline" | "generate" | "chat"; // Output mode (default: "standalone")
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SpecValidationResult<T> {
|
interface SpecValidationResult<T> {
|
||||||
@@ -370,7 +370,7 @@ Most users should use `pipeJsonRender()` instead, which wraps this transform for
|
|||||||
|
|
||||||
### createMixedStreamParser
|
### createMixedStreamParser
|
||||||
|
|
||||||
Parse a mixed stream of text and JSONL patches (used for Chat + GenUI mode):
|
Parse a mixed stream of text and JSONL patches (used for Inline mode):
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { createMixedStreamParser } from '@json-render/core';
|
import { createMixedStreamParser } from '@json-render/core';
|
||||||
@@ -389,7 +389,7 @@ parser.flush();
|
|||||||
|
|
||||||
### pipeJsonRender
|
### pipeJsonRender
|
||||||
|
|
||||||
Pipe an AI SDK `UIMessageStream` through the json-render transform. Lines that parse as JSONL patches are emitted as `data-spec` parts; everything else passes through as text. Used in Chat mode API routes.
|
Pipe an AI SDK `UIMessageStream` through the json-render transform. Lines that parse as JSONL patches are emitted as `data-spec` parts; everything else passes through as text. Used in Inline mode API routes.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { pipeJsonRender } from '@json-render/core';
|
import { pipeJsonRender } from '@json-render/core';
|
||||||
@@ -403,7 +403,7 @@ const stream = createUIMessageStream({
|
|||||||
return createUIMessageStreamResponse({ stream });
|
return createUIMessageStreamResponse({ stream });
|
||||||
```
|
```
|
||||||
|
|
||||||
See [Generation Modes](/docs/generation-modes) for full Chat mode setup.
|
See [Generation Modes](/docs/generation-modes) for full Inline mode setup.
|
||||||
|
|
||||||
### SpecStream Types
|
### SpecStream Types
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -0,0 +1,247 @@
|
|||||||
|
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 React UI, also install:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @json-render/react react react-dom
|
||||||
|
```
|
||||||
|
|
||||||
|
See the [MCP example](https://github.com/vercel-labs/json-render/tree/main/examples/mcp) 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 (`@json-render/mcp/app`)
|
||||||
|
|
||||||
|
These exports run inside the sandboxed iframe rendered by the MCP host.
|
||||||
|
|
||||||
|
### useJsonRenderApp
|
||||||
|
|
||||||
|
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";
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### UseJsonRenderAppReturn
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
### buildAppHtml
|
||||||
|
|
||||||
|
Generate a self-contained HTML page from bundled JavaScript and CSS.
|
||||||
|
|
||||||
|
```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.
|
||||||
@@ -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,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>
|
||||||
@@ -341,6 +341,8 @@ February 2026
|
|||||||
|
|
||||||
### New: Chat Mode (Inline GenUI)
|
### New: Chat Mode (Inline GenUI)
|
||||||
|
|
||||||
|
> **Note:** These modes were renamed in v0.12.1 — "Generate" is now "Standalone" and "Chat" is now "Inline". The old names are accepted as deprecated aliases.
|
||||||
|
|
||||||
json-render now supports two generation modes: **Generate** (JSONL-only, the default) and **Chat** (text + JSONL inline). Chat mode lets the AI respond conversationally with embedded UI specs, ideal for chatbots and copilot experiences.
|
json-render now supports two generation modes: **Generate** (JSONL-only, the default) and **Chat** (text + JSONL inline). Chat mode lets the AI respond conversationally with embedded UI specs, ideal for chatbots and copilot experiences.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
|
|||||||
@@ -3,15 +3,15 @@ export const metadata = pageMetadata("docs/generation-modes")
|
|||||||
|
|
||||||
# Generation Modes
|
# Generation Modes
|
||||||
|
|
||||||
json-render supports two modes for AI-generated UI: **Generate mode** for standalone UI and **Chat mode** for inline UI within a conversation.
|
json-render supports two modes for AI-generated UI: **Standalone mode** for standalone UI and **Inline mode** for inline UI within a conversation.
|
||||||
|
|
||||||
The mode controls how the AI formats its output and how your app processes the stream. The underlying JSONL patch format is the same in both modes.
|
The mode controls how the AI formats its output and how your app processes the stream. The underlying JSONL patch format is the same in both modes.
|
||||||
|
|
||||||
<GenerationModesDiagram />
|
<GenerationModesDiagram />
|
||||||
|
|
||||||
## Generate Mode (Standalone)
|
## Standalone Mode
|
||||||
|
|
||||||
In generate mode, the AI outputs **only JSONL patches** — no prose, no markdown. The entire response is a UI spec.
|
In standalone mode, the AI outputs **only JSONL patches** — no prose, no markdown. The entire response is a UI spec.
|
||||||
|
|
||||||
This is the default mode and is ideal for:
|
This is the default mode and is ideal for:
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ This is the default mode and is ideal for:
|
|||||||
```typescript
|
```typescript
|
||||||
import { streamText } from "ai";
|
import { streamText } from "ai";
|
||||||
|
|
||||||
// Generate mode is the default (no mode option needed)
|
// Standalone mode is the default (no mode option needed)
|
||||||
const systemPrompt = catalog.prompt({
|
const systemPrompt = catalog.prompt({
|
||||||
customRules: [
|
customRules: [
|
||||||
"Use Card as root for forms and small UIs.",
|
"Use Card as root for forms and small UIs.",
|
||||||
@@ -74,9 +74,9 @@ The AI outputs only JSONL — one patch per line, no surrounding text:
|
|||||||
{"op":"add","path":"/elements/submit","value":{"type":"Button","props":{"label":"Sign In"}}}
|
{"op":"add","path":"/elements/submit","value":{"type":"Button","props":{"label":"Sign In"}}}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Chat Mode (Inline)
|
## Inline Mode
|
||||||
|
|
||||||
In chat mode, the AI responds **conversationally first**, then outputs JSONL patches on their own lines. Text-only replies are allowed when no UI is needed (e.g. greetings, clarifying questions).
|
In inline mode, the AI responds **conversationally first**, then outputs JSONL patches on their own lines. Text-only replies are allowed when no UI is needed (e.g. greetings, clarifying questions).
|
||||||
|
|
||||||
This is ideal for:
|
This is ideal for:
|
||||||
|
|
||||||
@@ -92,8 +92,8 @@ import { streamText } from "ai";
|
|||||||
import { pipeJsonRender } from "@json-render/core";
|
import { pipeJsonRender } from "@json-render/core";
|
||||||
import { createUIMessageStream, createUIMessageStreamResponse } from "ai";
|
import { createUIMessageStream, createUIMessageStreamResponse } from "ai";
|
||||||
|
|
||||||
// Enable chat mode
|
// Enable inline mode
|
||||||
const systemPrompt = catalog.prompt({ mode: "chat" });
|
const systemPrompt = catalog.prompt({ mode: "inline" });
|
||||||
|
|
||||||
const result = streamText({
|
const result = streamText({
|
||||||
model: yourModel,
|
model: yourModel,
|
||||||
@@ -180,8 +180,8 @@ If the user asks a simple question ("what does BTC stand for?"), the AI replies
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th />
|
<th />
|
||||||
<th>Generate</th>
|
<th>Standalone</th>
|
||||||
<th>Chat</th>
|
<th>Inline</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -198,7 +198,7 @@ If the user asks a simple question ("what does BTC stand for?"), the AI replies
|
|||||||
<tr>
|
<tr>
|
||||||
<td>System prompt</td>
|
<td>System prompt</td>
|
||||||
<td><code>{"catalog.prompt()"}</code></td>
|
<td><code>{"catalog.prompt()"}</code></td>
|
||||||
<td><code>{'catalog.prompt({ mode: "chat" })'}</code></td>
|
<td><code>{'catalog.prompt({ mode: "inline" })'}</code></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Stream utility</td>
|
<td>Stream utility</td>
|
||||||
|
|||||||
@@ -309,10 +309,35 @@ const catalog = defineCatalog(schema, {
|
|||||||
|
|
||||||
const prompt = catalog.prompt();
|
const prompt = catalog.prompt();
|
||||||
|
|
||||||
// Chat mode prompt
|
// Inline mode prompt (formerly "chat")
|
||||||
|
const inlinePrompt = catalog.prompt({ mode: "inline" });
|
||||||
|
```
|
||||||
|
|
||||||
|
## Generation Modes
|
||||||
|
|
||||||
|
The generation mode values passed to `catalog.prompt()` have been renamed for clarity:
|
||||||
|
|
||||||
|
- `"generate"` is now `"standalone"`
|
||||||
|
- `"chat"` is now `"inline"`
|
||||||
|
|
||||||
|
The old names are accepted as deprecated aliases, so existing code will continue to work. Update when convenient.
|
||||||
|
|
||||||
|
**Before:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const prompt = catalog.prompt({ mode: "generate" });
|
||||||
const chatPrompt = catalog.prompt({ mode: "chat" });
|
const chatPrompt = catalog.prompt({ mode: "chat" });
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**After:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const prompt = catalog.prompt({ mode: "standalone" });
|
||||||
|
const inlinePrompt = catalog.prompt({ mode: "inline" });
|
||||||
|
```
|
||||||
|
|
||||||
|
The default mode (when no `mode` option is provided) is `"standalone"`, which behaves identically to the previous `"generate"` default.
|
||||||
|
|
||||||
## Validation
|
## Validation
|
||||||
|
|
||||||
`ValidationCheck` now uses `type` instead of `fn`, `ValidationProvider` uses `customFunctions` instead of `functions`, and `useFieldValidation` takes a config object instead of a checks array.
|
`ValidationCheck` now uses `type` instead of `fn`, `ValidationProvider` uses `customFunctions` instead of `functions`, and `useFieldValidation` takes a config object instead of a checks array.
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ The result is a native UI built from your own components — not an iframe, not
|
|||||||
- **[Streaming](/docs/streaming)** — Render progressively as the AI responds. Each JSONL patch adds to the spec and the UI updates in real time.
|
- **[Streaming](/docs/streaming)** — Render progressively as the AI responds. Each JSONL patch adds to the spec and the UI updates in real time.
|
||||||
- **[Data Binding](/docs/data-binding)** — Bind props to runtime data with `$state` paths, repeat elements over arrays, and wire two-way input bindings.
|
- **[Data Binding](/docs/data-binding)** — Bind props to runtime data with `$state` paths, repeat elements over arrays, and wire two-way input bindings.
|
||||||
- **[Visibility](/docs/visibility)** — Show or hide elements based on state conditions. The AI can generate conditional UIs without writing logic.
|
- **[Visibility](/docs/visibility)** — Show or hide elements based on state conditions. The AI can generate conditional UIs without writing logic.
|
||||||
- **[Generation Modes](/docs/generation-modes)** — Generate standalone UI (playground/builder) or inline UI within a chat conversation.
|
- **[Generation Modes](/docs/generation-modes)** — Standalone mode for full-page generated UI or inline mode for UI embedded in a conversation.
|
||||||
|
|
||||||
## Next
|
## Next
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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
|
GitHub repository: https://github.com/vercel-labs/json-render
|
||||||
Documentation: https://json-render.dev/docs
|
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.
|
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.
|
||||||
|
|
||||||
|
|||||||
@@ -19,11 +19,11 @@ function FormUI() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ChatModeDiagram() {
|
function InlineModeDiagram() {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
<div className="text-xs font-medium text-muted-foreground mb-3 text-center">
|
<div className="text-xs font-medium text-muted-foreground mb-3 text-center">
|
||||||
Chat Mode
|
Inline Mode
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 border border-border rounded-lg bg-background overflow-hidden flex flex-col">
|
<div className="flex-1 border border-border rounded-lg bg-background overflow-hidden flex flex-col">
|
||||||
{/* Chat area */}
|
{/* Chat area */}
|
||||||
@@ -69,11 +69,11 @@ function ChatModeDiagram() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function GenerateModeDiagram() {
|
function StandaloneModeDiagram() {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
<div className="text-xs font-medium text-muted-foreground mb-3 text-center">
|
<div className="text-xs font-medium text-muted-foreground mb-3 text-center">
|
||||||
Generate Mode
|
Standalone Mode
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 border border-border rounded-lg bg-background overflow-hidden flex flex-row">
|
<div className="flex-1 border border-border rounded-lg bg-background overflow-hidden flex flex-row">
|
||||||
{/* Left panel - prompt */}
|
{/* Left panel - prompt */}
|
||||||
@@ -104,10 +104,10 @@ export function GenerationModesDiagram() {
|
|||||||
<div className="not-prose my-8">
|
<div className="not-prose my-8">
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
||||||
<div className="h-[280px]">
|
<div className="h-[280px]">
|
||||||
<ChatModeDiagram />
|
<InlineModeDiagram />
|
||||||
</div>
|
</div>
|
||||||
<div className="h-[280px]">
|
<div className="h-[280px]">
|
||||||
<GenerateModeDiagram />
|
<StandaloneModeDiagram />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export const docsNavigation: NavSection[] = [
|
|||||||
{ title: "Introduction", href: "/docs" },
|
{ title: "Introduction", href: "/docs" },
|
||||||
{ title: "Installation", href: "/docs/installation" },
|
{ title: "Installation", href: "/docs/installation" },
|
||||||
{ title: "Quick Start", href: "/docs/quick-start" },
|
{ title: "Quick Start", href: "/docs/quick-start" },
|
||||||
|
{ title: "Skills", href: "/docs/skills" },
|
||||||
{ title: "Migration Guide", href: "/docs/migration" },
|
{ title: "Migration Guide", href: "/docs/migration" },
|
||||||
{ title: "Changelog", href: "/docs/changelog" },
|
{ title: "Changelog", href: "/docs/changelog" },
|
||||||
],
|
],
|
||||||
@@ -95,6 +96,11 @@ export const docsNavigation: NavSection[] = [
|
|||||||
href: "https://github.com/vercel-labs/json-render/tree/main/examples/vite-renderers",
|
href: "https://github.com/vercel-labs/json-render/tree/main/examples/vite-renderers",
|
||||||
external: true,
|
external: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "MCP App",
|
||||||
|
href: "https://github.com/vercel-labs/json-render/tree/main/examples/mcp",
|
||||||
|
external: true,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -128,6 +134,11 @@ export const docsNavigation: NavSection[] = [
|
|||||||
{ title: "@json-render/vue", href: "/docs/api/vue" },
|
{ title: "@json-render/vue", href: "/docs/api/vue" },
|
||||||
{ title: "@json-render/svelte", href: "/docs/api/svelte" },
|
{ title: "@json-render/svelte", href: "/docs/api/svelte" },
|
||||||
{ title: "@json-render/codegen", href: "/docs/api/codegen" },
|
{ 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" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export const PAGE_TITLES: Record<string, string> = {
|
|||||||
"docs/ag-ui": "AG-UI Integration",
|
"docs/ag-ui": "AG-UI Integration",
|
||||||
"docs/migration": "Migration Guide",
|
"docs/migration": "Migration Guide",
|
||||||
"docs/changelog": "Changelog",
|
"docs/changelog": "Changelog",
|
||||||
|
"docs/skills": "Skills",
|
||||||
|
|
||||||
// API references
|
// API references
|
||||||
"docs/api/core": "@json-render/core API",
|
"docs/api/core": "@json-render/core API",
|
||||||
@@ -50,6 +51,11 @@ export const PAGE_TITLES: Record<string, string> = {
|
|||||||
"docs/api/image": "@json-render/image API",
|
"docs/api/image": "@json-render/image API",
|
||||||
"docs/api/remotion": "@json-render/remotion API",
|
"docs/api/remotion": "@json-render/remotion API",
|
||||||
"docs/api/shadcn": "@json-render/shadcn 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,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "web",
|
"name": "web",
|
||||||
"version": "0.1.4",
|
"version": "0.1.6",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
|||||||
@@ -1,5 +1,23 @@
|
|||||||
# example-chat
|
# example-chat
|
||||||
|
|
||||||
|
## 0.1.6
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [54a1ecf]
|
||||||
|
- @json-render/core@0.12.1
|
||||||
|
- @json-render/react@0.12.1
|
||||||
|
- @json-render/shadcn@0.12.1
|
||||||
|
|
||||||
|
## 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
|
## 0.1.4
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ When the user asks for a quiz, test, or Q&A, build an interactive experience:
|
|||||||
4. You can also add a final score section that becomes visible when all questions are submitted.
|
4. You can also add a final score section that becomes visible when all questions are submitted.
|
||||||
|
|
||||||
${explorerCatalog.prompt({
|
${explorerCatalog.prompt({
|
||||||
mode: "chat",
|
mode: "inline",
|
||||||
customRules: [
|
customRules: [
|
||||||
"NEVER use viewport height classes (min-h-screen, h-screen) — the UI renders inside a fixed-size container.",
|
"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.",
|
"Prefer Grid with columns='2' or columns='3' for side-by-side layouts.",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "example-chat",
|
"name": "example-chat",
|
||||||
"version": "0.1.4",
|
"version": "0.1.6",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,5 +1,23 @@
|
|||||||
# example-dashboard
|
# example-dashboard
|
||||||
|
|
||||||
|
## 0.1.6
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [54a1ecf]
|
||||||
|
- @json-render/core@0.12.1
|
||||||
|
- @json-render/codegen@0.12.1
|
||||||
|
- @json-render/react@0.12.1
|
||||||
|
|
||||||
|
## 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
|
## 0.1.4
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "example-dashboard",
|
"name": "example-dashboard",
|
||||||
"version": "0.1.4",
|
"version": "0.1.6",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,5 +1,21 @@
|
|||||||
# example-image
|
# example-image
|
||||||
|
|
||||||
|
## 0.1.3
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [54a1ecf]
|
||||||
|
- @json-render/core@0.12.1
|
||||||
|
- @json-render/image@0.12.1
|
||||||
|
|
||||||
|
## 0.1.2
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [63c339b]
|
||||||
|
- @json-render/core@0.12.0
|
||||||
|
- @json-render/image@0.12.0
|
||||||
|
|
||||||
## 0.1.1
|
## 0.1.1
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "example-image",
|
"name": "example-image",
|
||||||
"version": "0.1.1",
|
"version": "0.1.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# example-mcp
|
||||||
|
|
||||||
|
## 0.1.2
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [54a1ecf]
|
||||||
|
- @json-render/core@0.12.1
|
||||||
|
- @json-render/mcp@0.12.1
|
||||||
|
- @json-render/react@0.12.1
|
||||||
|
- @json-render/shadcn@0.12.1
|
||||||
|
|
||||||
|
## 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
|
||||||
@@ -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/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/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
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"name": "example-mcp",
|
||||||
|
"version": "0.1.2",
|
||||||
|
"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.0.0",
|
||||||
|
"react-dom": "^19.0.0",
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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: {},
|
||||||
|
});
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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>,
|
||||||
|
);
|
||||||
@@ -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";
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
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()],
|
||||||
|
resolve: {
|
||||||
|
dedupe: ["react", "react-dom"],
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
outDir: "dist",
|
||||||
|
emptyOutDir: false,
|
||||||
|
rollupOptions: {
|
||||||
|
input: "index.html",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -1,5 +1,23 @@
|
|||||||
# example-no-ai
|
# example-no-ai
|
||||||
|
|
||||||
|
## 0.1.6
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [54a1ecf]
|
||||||
|
- @json-render/core@0.12.1
|
||||||
|
- @json-render/react@0.12.1
|
||||||
|
- @json-render/shadcn@0.12.1
|
||||||
|
|
||||||
|
## 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
|
## 0.1.4
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "example-no-ai",
|
"name": "example-no-ai",
|
||||||
"version": "0.1.4",
|
"version": "0.1.6",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# example-react-email
|
||||||
|
|
||||||
|
## 0.1.2
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [54a1ecf]
|
||||||
|
- @json-render/core@0.12.1
|
||||||
|
- @json-render/react-email@0.12.1
|
||||||
|
|
||||||
|
## 0.1.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [63c339b]
|
||||||
|
- @json-render/core@0.12.0
|
||||||
|
- @json-render/react-email@0.12.0
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "example-react-email",
|
"name": "example-react-email",
|
||||||
"version": "0.1.0",
|
"version": "0.1.2",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,5 +1,21 @@
|
|||||||
# example-react-native
|
# example-react-native
|
||||||
|
|
||||||
|
## 0.1.6
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [54a1ecf]
|
||||||
|
- @json-render/core@0.12.1
|
||||||
|
- @json-render/react-native@0.12.1
|
||||||
|
|
||||||
|
## 0.1.5
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [63c339b]
|
||||||
|
- @json-render/core@0.12.0
|
||||||
|
- @json-render/react-native@0.12.0
|
||||||
|
|
||||||
## 0.1.4
|
## 0.1.4
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "example-react-native",
|
"name": "example-react-native",
|
||||||
"version": "0.1.4",
|
"version": "0.1.6",
|
||||||
"private": true,
|
"private": true,
|
||||||
"main": "expo-router/entry",
|
"main": "expo-router/entry",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,5 +1,21 @@
|
|||||||
# example-react-pdf
|
# example-react-pdf
|
||||||
|
|
||||||
|
## 0.1.6
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [54a1ecf]
|
||||||
|
- @json-render/core@0.12.1
|
||||||
|
- @json-render/react-pdf@0.12.1
|
||||||
|
|
||||||
|
## 0.1.5
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [63c339b]
|
||||||
|
- @json-render/core@0.12.0
|
||||||
|
- @json-render/react-pdf@0.12.0
|
||||||
|
|
||||||
## 0.1.4
|
## 0.1.4
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "example-react-pdf",
|
"name": "example-react-pdf",
|
||||||
"version": "0.1.4",
|
"version": "0.1.6",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,5 +1,21 @@
|
|||||||
# example-remotion
|
# example-remotion
|
||||||
|
|
||||||
|
## 0.1.6
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [54a1ecf]
|
||||||
|
- @json-render/core@0.12.1
|
||||||
|
- @json-render/remotion@0.12.1
|
||||||
|
|
||||||
|
## 0.1.5
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [63c339b]
|
||||||
|
- @json-render/core@0.12.0
|
||||||
|
- @json-render/remotion@0.12.0
|
||||||
|
|
||||||
## 0.1.4
|
## 0.1.4
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "example-remotion",
|
"name": "example-remotion",
|
||||||
"version": "0.1.4",
|
"version": "0.1.6",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
/// <reference types="next" />
|
/// <reference types="next" />
|
||||||
/// <reference types="next/image-types/global" />
|
/// <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
|
// NOTE: This file should not be edited
|
||||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
|
|||||||
@@ -1,5 +1,21 @@
|
|||||||
# com.example.json-render-demo
|
# com.example.json-render-demo
|
||||||
|
|
||||||
|
## 0.0.7
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [54a1ecf]
|
||||||
|
- @json-render/core@0.12.1
|
||||||
|
- @json-render/react@0.12.1
|
||||||
|
|
||||||
|
## 0.0.6
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [63c339b]
|
||||||
|
- @json-render/core@0.12.0
|
||||||
|
- @json-render/react@0.12.0
|
||||||
|
|
||||||
## 0.0.5
|
## 0.0.5
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "com.example.json-render-demo",
|
"name": "com.example.json-render-demo",
|
||||||
"version": "0.0.5",
|
"version": "0.0.7",
|
||||||
"description": "Test",
|
"description": "Test",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "~~proprietary~~",
|
"license": "~~proprietary~~",
|
||||||
|
|||||||
@@ -1,5 +1,21 @@
|
|||||||
# com.example.json-render-fullpage-demo
|
# com.example.json-render-fullpage-demo
|
||||||
|
|
||||||
|
## 0.0.7
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [54a1ecf]
|
||||||
|
- @json-render/core@0.12.1
|
||||||
|
- @json-render/react@0.12.1
|
||||||
|
|
||||||
|
## 0.0.6
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [63c339b]
|
||||||
|
- @json-render/core@0.12.0
|
||||||
|
- @json-render/react@0.12.0
|
||||||
|
|
||||||
## 0.0.5
|
## 0.0.5
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "com.example.json-render-fullpage-demo",
|
"name": "com.example.json-render-fullpage-demo",
|
||||||
"version": "0.0.5",
|
"version": "0.0.7",
|
||||||
"description": "Full-page Stripe App example (alpha)",
|
"description": "Full-page Stripe App example (alpha)",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "~~proprietary~~",
|
"license": "~~proprietary~~",
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# svelte-chat
|
||||||
|
|
||||||
|
## 0.0.3
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [54a1ecf]
|
||||||
|
- @json-render/core@0.12.1
|
||||||
|
- @json-render/svelte@0.12.1
|
||||||
|
|
||||||
|
## 0.0.2
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [63c339b]
|
||||||
|
- @json-render/core@0.12.0
|
||||||
|
- @json-render/svelte@0.12.0
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "svelte-chat",
|
"name": "svelte-chat",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.1",
|
"version": "0.0.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ INPUT COMPONENTS:
|
|||||||
- Button: Clickable button. Use on.press to trigger actions.
|
- Button: Clickable button. Use on.press to trigger actions.
|
||||||
|
|
||||||
${explorerCatalog.prompt({
|
${explorerCatalog.prompt({
|
||||||
mode: "chat",
|
mode: "inline",
|
||||||
customRules: [
|
customRules: [
|
||||||
"NEVER use viewport height classes (min-h-screen, h-screen) — the UI renders inside a fixed-size container.",
|
"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.",
|
"Prefer Grid with columns='2' or columns='3' for side-by-side layouts.",
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { defineRegistry, type ComponentRegistry } from "@json-render/svelte";
|
import { defineRegistry } from "@json-render/svelte";
|
||||||
import { explorerCatalog } from "./catalog";
|
import { explorerCatalog } from "./catalog";
|
||||||
|
|
||||||
// Import render components
|
|
||||||
import StackComponent from "./components/Stack.svelte";
|
import StackComponent from "./components/Stack.svelte";
|
||||||
import CardComponent from "./components/Card.svelte";
|
import CardComponent from "./components/Card.svelte";
|
||||||
import GridComponent from "./components/Grid.svelte";
|
import GridComponent from "./components/Grid.svelte";
|
||||||
@@ -28,7 +27,7 @@ import SelectInputComponent from "./components/SelectInput.svelte";
|
|||||||
import TextInputComponent from "./components/TextInput.svelte";
|
import TextInputComponent from "./components/TextInput.svelte";
|
||||||
import ButtonComponent from "./components/Button.svelte";
|
import ButtonComponent from "./components/Button.svelte";
|
||||||
|
|
||||||
const components: ComponentRegistry = {
|
const components = {
|
||||||
Stack: StackComponent,
|
Stack: StackComponent,
|
||||||
Card: CardComponent,
|
Card: CardComponent,
|
||||||
Grid: GridComponent,
|
Grid: GridComponent,
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# example-svelte
|
||||||
|
|
||||||
|
## 0.1.3
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [54a1ecf]
|
||||||
|
- @json-render/core@0.12.1
|
||||||
|
- @json-render/svelte@0.12.1
|
||||||
|
|
||||||
|
## 0.1.2
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [63c339b]
|
||||||
|
- @json-render/core@0.12.0
|
||||||
|
- @json-render/svelte@0.12.0
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "example-svelte",
|
"name": "example-svelte",
|
||||||
"version": "0.1.1",
|
"version": "0.1.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,5 +1,25 @@
|
|||||||
# vite-renderers
|
# vite-renderers
|
||||||
|
|
||||||
|
## 0.1.4
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [54a1ecf]
|
||||||
|
- @json-render/core@0.12.1
|
||||||
|
- @json-render/react@0.12.1
|
||||||
|
- @json-render/svelte@0.12.1
|
||||||
|
- @json-render/vue@0.12.1
|
||||||
|
|
||||||
|
## 0.1.3
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [63c339b]
|
||||||
|
- @json-render/core@0.12.0
|
||||||
|
- @json-render/svelte@0.12.0
|
||||||
|
- @json-render/react@0.12.0
|
||||||
|
- @json-render/vue@0.12.0
|
||||||
|
|
||||||
## 0.1.2
|
## 0.1.2
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "vite-renderers",
|
"name": "vite-renderers",
|
||||||
"version": "0.1.2",
|
"version": "0.1.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,5 +1,21 @@
|
|||||||
# example-vue
|
# example-vue
|
||||||
|
|
||||||
|
## 0.1.4
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [54a1ecf]
|
||||||
|
- @json-render/core@0.12.1
|
||||||
|
- @json-render/vue@0.12.1
|
||||||
|
|
||||||
|
## 0.1.3
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [63c339b]
|
||||||
|
- @json-render/core@0.12.0
|
||||||
|
- @json-render/vue@0.12.0
|
||||||
|
|
||||||
## 0.1.2
|
## 0.1.2
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "example-vue",
|
"name": "example-vue",
|
||||||
"version": "0.1.2",
|
"version": "0.1.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"predev": "command -v portless >/dev/null 2>&1 || (echo '\\nportless is required but not installed. Run: npm i -g portless\\nSee: https://github.com/vercel-labs/portless\\n' && exit 1)",
|
"predev": "command -v portless >/dev/null 2>&1 || (echo '\\nportless is required but not installed. Run: npm i -g portless\\nSee: https://github.com/vercel-labs/portless\\n' && exit 1)",
|
||||||
|
|||||||
@@ -1,5 +1,19 @@
|
|||||||
# @json-render/codegen
|
# @json-render/codegen
|
||||||
|
|
||||||
|
## 0.12.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [54a1ecf]
|
||||||
|
- @json-render/core@0.12.1
|
||||||
|
|
||||||
|
## 0.12.0
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [63c339b]
|
||||||
|
- @json-render/core@0.12.0
|
||||||
|
|
||||||
## 0.11.0
|
## 0.11.0
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@json-render/codegen",
|
"name": "@json-render/codegen",
|
||||||
"version": "0.11.0",
|
"version": "0.12.1",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"description": "Utilities for generating code from json-render UI trees",
|
"description": "Utilities for generating code from json-render UI trees",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
"url": "git+https://github.com/vercel-labs/json-render.git",
|
"url": "git+https://github.com/vercel-labs/json-render.git",
|
||||||
"directory": "packages/codegen"
|
"directory": "packages/codegen"
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/vercel-labs/json-render#readme",
|
"homepage": "https://json-render.dev",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/vercel-labs/json-render/issues"
|
"url": "https://github.com/vercel-labs/json-render/issues"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,36 @@
|
|||||||
# @json-render/core
|
# @json-render/core
|
||||||
|
|
||||||
|
## 0.12.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- 54a1ecf: Rename generation modes and fix MCP React duplicate module error.
|
||||||
|
|
||||||
|
### Changed:
|
||||||
|
- **`@json-render/core`** — Renamed generation modes from `"generate"` / `"chat"` to `"standalone"` / `"inline"`. The old names still work but emit a deprecation warning.
|
||||||
|
|
||||||
|
### Fixed:
|
||||||
|
- **`@json-render/mcp`** — Resolved React duplicate module error (`useRef` returning null) by adding `resolve.dedupe` Vite configuration. Added `./build-app-html` export entry point.
|
||||||
|
|
||||||
|
### Other:
|
||||||
|
- Updated `homepage` URLs across all packages to point to `https://json-render.dev`.
|
||||||
|
- Reorganized skills directory structure for cleaner naming.
|
||||||
|
- Added skills documentation page to the web app.
|
||||||
|
|
||||||
|
## 0.12.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- 63c339b: Add Svelte renderer, React Email renderer, and MCP Apps integration.
|
||||||
|
|
||||||
|
### New:
|
||||||
|
- **`@json-render/svelte`** — Svelte 5 renderer with runes-based reactivity. Full support for data binding, visibility, actions, validation, watchers, streaming, and repeat scopes. Includes `defineRegistry`, `Renderer`, `schema`, composables, and context providers.
|
||||||
|
- **`@json-render/react-email`** — React Email renderer for generating HTML and plain-text emails from JSON specs. 17 standard components (Html, Head, Body, Container, Section, Row, Column, Heading, Text, Link, Button, Image, Hr, Preview, Markdown). Server-side `renderToHtml` / `renderToPlainText` APIs. Custom catalog and registry support.
|
||||||
|
- **`@json-render/mcp`** — MCP Apps integration that serves json-render UIs as interactive apps inside Claude, ChatGPT, Cursor, VS Code, and other MCP-capable clients. `createMcpApp` server factory, `useJsonRenderApp` React hook for iframes, and `buildAppHtml` utility.
|
||||||
|
|
||||||
|
### Fixed:
|
||||||
|
- **`@json-render/svelte`** — Corrected JSDoc comment and added missing `zod` peer dependency.
|
||||||
|
|
||||||
## 0.11.0
|
## 0.11.0
|
||||||
|
|
||||||
### Minor Changes
|
### Minor Changes
|
||||||
|
|||||||
@@ -227,7 +227,7 @@ Schema options:
|
|||||||
| `ActionBinding` | Action binding with `action`, `params`, `confirm`, `preventDefault`, etc. |
|
| `ActionBinding` | Action binding with `action`, `params`, `confirm`, `preventDefault`, etc. |
|
||||||
| `BuiltInAction` | Built-in action definition with `name` and `description` |
|
| `BuiltInAction` | Built-in action definition with `name` and `description` |
|
||||||
|
|
||||||
### Chat Mode (Mixed Streams)
|
### Inline Mode (Mixed Streams)
|
||||||
|
|
||||||
| Export | Purpose |
|
| Export | Purpose |
|
||||||
|--------|---------|
|
|--------|---------|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@json-render/core",
|
"name": "@json-render/core",
|
||||||
"version": "0.11.0",
|
"version": "0.12.1",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"description": "JSON becomes real things. Define your catalog, register your components, let AI generate.",
|
"description": "JSON becomes real things. Define your catalog, register your components, let AI generate.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
"url": "git+https://github.com/vercel-labs/json-render.git",
|
"url": "git+https://github.com/vercel-labs/json-render.git",
|
||||||
"directory": "packages/core"
|
"directory": "packages/core"
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/vercel-labs/json-render#readme",
|
"homepage": "https://json-render.dev",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/vercel-labs/json-render/issues"
|
"url": "https://github.com/vercel-labs/json-render/issues"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -257,7 +257,7 @@ describe("catalog.prompt", () => {
|
|||||||
expect(prompt).toContain("Keep UIs simple");
|
expect(prompt).toContain("Keep UIs simple");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("generates chat mode prompt when mode is chat", () => {
|
it("generates inline mode prompt when mode is inline", () => {
|
||||||
const catalog = defineCatalog(testSchema, {
|
const catalog = defineCatalog(testSchema, {
|
||||||
components: {
|
components: {
|
||||||
Text: {
|
Text: {
|
||||||
@@ -268,13 +268,13 @@ describe("catalog.prompt", () => {
|
|||||||
},
|
},
|
||||||
actions: {},
|
actions: {},
|
||||||
});
|
});
|
||||||
const prompt = catalog.prompt({ mode: "chat" });
|
const prompt = catalog.prompt({ mode: "inline" });
|
||||||
expect(prompt).toContain("```spec");
|
expect(prompt).toContain("```spec");
|
||||||
expect(prompt).toContain("conversationally");
|
expect(prompt).toContain("conversationally");
|
||||||
expect(prompt).toContain("text + JSONL");
|
expect(prompt).toContain("text + JSONL");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("generates generate mode prompt by default", () => {
|
it("generates standalone mode prompt by default", () => {
|
||||||
const catalog = defineCatalog(testSchema, {
|
const catalog = defineCatalog(testSchema, {
|
||||||
components: {
|
components: {
|
||||||
Text: {
|
Text: {
|
||||||
@@ -290,6 +290,38 @@ describe("catalog.prompt", () => {
|
|||||||
expect(prompt).not.toContain("conversationally");
|
expect(prompt).not.toContain("conversationally");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("accepts deprecated 'chat' as alias for 'inline'", () => {
|
||||||
|
const catalog = defineCatalog(testSchema, {
|
||||||
|
components: {
|
||||||
|
Text: {
|
||||||
|
props: z.object({}),
|
||||||
|
description: "",
|
||||||
|
slots: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions: {},
|
||||||
|
});
|
||||||
|
const inlinePrompt = catalog.prompt({ mode: "inline" });
|
||||||
|
const chatPrompt = catalog.prompt({ mode: "chat" });
|
||||||
|
expect(chatPrompt).toEqual(inlinePrompt);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("accepts deprecated 'generate' as alias for 'standalone'", () => {
|
||||||
|
const catalog = defineCatalog(testSchema, {
|
||||||
|
components: {
|
||||||
|
Text: {
|
||||||
|
props: z.object({}),
|
||||||
|
description: "",
|
||||||
|
slots: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions: {},
|
||||||
|
});
|
||||||
|
const standalonePrompt = catalog.prompt({ mode: "standalone" });
|
||||||
|
const generatePrompt = catalog.prompt({ mode: "generate" });
|
||||||
|
expect(generatePrompt).toEqual(standalonePrompt);
|
||||||
|
});
|
||||||
|
|
||||||
it("uses actual catalog component names in examples", () => {
|
it("uses actual catalog component names in examples", () => {
|
||||||
const catalog = defineCatalog(testSchema, {
|
const catalog = defineCatalog(testSchema, {
|
||||||
components: {
|
components: {
|
||||||
|
|||||||
@@ -112,11 +112,14 @@ export interface PromptOptions {
|
|||||||
/**
|
/**
|
||||||
* Output mode for the generated prompt.
|
* Output mode for the generated prompt.
|
||||||
*
|
*
|
||||||
* - `"generate"` (default): The LLM should output only JSONL patches (no prose).
|
* - `"standalone"` (default): The LLM should output only JSONL patches (no prose).
|
||||||
* - `"chat"`: The LLM should respond conversationally first, then output JSONL patches.
|
* - `"inline"`: The LLM should respond conversationally first, then output JSONL patches.
|
||||||
* Includes rules about interleaving text with JSONL and not wrapping in code fences.
|
* Includes rules about interleaving text with JSONL and not wrapping in code fences.
|
||||||
|
*
|
||||||
|
* @deprecated `"generate"` — use `"standalone"` instead.
|
||||||
|
* @deprecated `"chat"` — use `"inline"` instead.
|
||||||
*/
|
*/
|
||||||
mode?: "generate" | "chat";
|
mode?: "standalone" | "inline" | "generate" | "chat";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -563,15 +566,28 @@ function generatePrompt<TDef extends SchemaDefinition, TCatalog>(
|
|||||||
const {
|
const {
|
||||||
system = "You are a UI generator that outputs JSON.",
|
system = "You are a UI generator that outputs JSON.",
|
||||||
customRules = [],
|
customRules = [],
|
||||||
mode = "generate",
|
mode: rawMode = "standalone",
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
|
const mode: "standalone" | "inline" =
|
||||||
|
rawMode === "chat"
|
||||||
|
? (console.warn(
|
||||||
|
'[json-render] mode "chat" is deprecated, use "inline" instead',
|
||||||
|
),
|
||||||
|
"inline")
|
||||||
|
: rawMode === "generate"
|
||||||
|
? (console.warn(
|
||||||
|
'[json-render] mode "generate" is deprecated, use "standalone" instead',
|
||||||
|
),
|
||||||
|
"standalone")
|
||||||
|
: rawMode;
|
||||||
|
|
||||||
const lines: string[] = [];
|
const lines: string[] = [];
|
||||||
lines.push(system);
|
lines.push(system);
|
||||||
lines.push("");
|
lines.push("");
|
||||||
|
|
||||||
// Output format section - explain JSONL streaming patch format
|
// Output format section - explain JSONL streaming patch format
|
||||||
if (mode === "chat") {
|
if (mode === "inline") {
|
||||||
lines.push("OUTPUT FORMAT (text + JSONL, RFC 6902 JSON Patch):");
|
lines.push("OUTPUT FORMAT (text + JSONL, RFC 6902 JSON Patch):");
|
||||||
lines.push(
|
lines.push(
|
||||||
"You respond conversationally. When generating UI, first write a brief explanation (1-3 sentences), then output JSONL patch lines wrapped in a ```spec code fence.",
|
"You respond conversationally. When generating UI, first write a brief explanation (1-3 sentences), then output JSONL patch lines wrapped in a ```spec code fence.",
|
||||||
@@ -1014,7 +1030,7 @@ Note: state patches appear right after the elements that use them, so the UI fil
|
|||||||
// Rules
|
// Rules
|
||||||
lines.push("RULES:");
|
lines.push("RULES:");
|
||||||
const baseRules =
|
const baseRules =
|
||||||
mode === "chat"
|
mode === "inline"
|
||||||
? [
|
? [
|
||||||
"When generating UI, wrap all JSONL patches in a ```spec code fence - one JSON object per line inside the fence",
|
"When generating UI, wrap all JSONL patches in a ```spec code fence - one JSON object per line inside the fence",
|
||||||
"Write a brief conversational response before any JSONL output",
|
"Write a brief conversational response before any JSONL output",
|
||||||
|
|||||||
@@ -1,5 +1,19 @@
|
|||||||
# @json-render/image
|
# @json-render/image
|
||||||
|
|
||||||
|
## 0.12.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [54a1ecf]
|
||||||
|
- @json-render/core@0.12.1
|
||||||
|
|
||||||
|
## 0.12.0
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [63c339b]
|
||||||
|
- @json-render/core@0.12.0
|
||||||
|
|
||||||
## 0.11.0
|
## 0.11.0
|
||||||
|
|
||||||
### Minor Changes
|
### Minor Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@json-render/image",
|
"name": "@json-render/image",
|
||||||
"version": "0.11.0",
|
"version": "0.12.1",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"description": "Image renderer for @json-render/core. JSON becomes SVG and PNG images via Satori.",
|
"description": "Image renderer for @json-render/core. JSON becomes SVG and PNG images via Satori.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
"url": "git+https://github.com/vercel-labs/json-render.git",
|
"url": "git+https://github.com/vercel-labs/json-render.git",
|
||||||
"directory": "packages/image"
|
"directory": "packages/image"
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/vercel-labs/json-render#readme",
|
"homepage": "https://json-render.dev",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/vercel-labs/json-render/issues"
|
"url": "https://github.com/vercel-labs/json-render/issues"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,19 @@
|
|||||||
# @json-render/jotai
|
# @json-render/jotai
|
||||||
|
|
||||||
|
## 0.12.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [54a1ecf]
|
||||||
|
- @json-render/core@0.12.1
|
||||||
|
|
||||||
|
## 0.12.0
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [63c339b]
|
||||||
|
- @json-render/core@0.12.0
|
||||||
|
|
||||||
## 0.11.0
|
## 0.11.0
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@json-render/jotai",
|
"name": "@json-render/jotai",
|
||||||
"version": "0.11.0",
|
"version": "0.12.1",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"description": "Jotai adapter for json-render StateStore",
|
"description": "Jotai adapter for json-render StateStore",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
"url": "git+https://github.com/vercel-labs/json-render.git",
|
"url": "git+https://github.com/vercel-labs/json-render.git",
|
||||||
"directory": "packages/jotai"
|
"directory": "packages/jotai"
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/vercel-labs/json-render#readme",
|
"homepage": "https://json-render.dev",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/vercel-labs/json-render/issues"
|
"url": "https://github.com/vercel-labs/json-render/issues"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# @json-render/mcp
|
||||||
|
|
||||||
|
## 0.12.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- 54a1ecf: Rename generation modes and fix MCP React duplicate module error.
|
||||||
|
|
||||||
|
### Changed:
|
||||||
|
- **`@json-render/core`** — Renamed generation modes from `"generate"` / `"chat"` to `"standalone"` / `"inline"`. The old names still work but emit a deprecation warning.
|
||||||
|
|
||||||
|
### Fixed:
|
||||||
|
- **`@json-render/mcp`** — Resolved React duplicate module error (`useRef` returning null) by adding `resolve.dedupe` Vite configuration. Added `./build-app-html` export entry point.
|
||||||
|
|
||||||
|
### Other:
|
||||||
|
- Updated `homepage` URLs across all packages to point to `https://json-render.dev`.
|
||||||
|
- Reorganized skills directory structure for cleaner naming.
|
||||||
|
- Added skills documentation page to the web app.
|
||||||
|
|
||||||
|
- Updated dependencies [54a1ecf]
|
||||||
|
- @json-render/core@0.12.1
|
||||||
|
|
||||||
|
## 0.12.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- 63c339b: Add Svelte renderer, React Email renderer, and MCP Apps integration.
|
||||||
|
|
||||||
|
### New:
|
||||||
|
- **`@json-render/svelte`** — Svelte 5 renderer with runes-based reactivity. Full support for data binding, visibility, actions, validation, watchers, streaming, and repeat scopes. Includes `defineRegistry`, `Renderer`, `schema`, composables, and context providers.
|
||||||
|
- **`@json-render/react-email`** — React Email renderer for generating HTML and plain-text emails from JSON specs. 17 standard components (Html, Head, Body, Container, Section, Row, Column, Heading, Text, Link, Button, Image, Hr, Preview, Markdown). Server-side `renderToHtml` / `renderToPlainText` APIs. Custom catalog and registry support.
|
||||||
|
- **`@json-render/mcp`** — MCP Apps integration that serves json-render UIs as interactive apps inside Claude, ChatGPT, Cursor, VS Code, and other MCP-capable clients. `createMcpApp` server factory, `useJsonRenderApp` React hook for iframes, and `buildAppHtml` utility.
|
||||||
|
|
||||||
|
### Fixed:
|
||||||
|
- **`@json-render/svelte`** — Corrected JSDoc comment and added missing `zod` peer dependency.
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [63c339b]
|
||||||
|
- @json-render/core@0.12.0
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
# @json-render/mcp
|
||||||
|
|
||||||
|
MCP Apps integration for [json-render](https://github.com/vercel-labs/json-render). Serve json-render UIs as interactive MCP Apps inside Claude, ChatGPT, Cursor, VS Code, and other MCP-capable clients.
|
||||||
|
|
||||||
|
## What are MCP Apps?
|
||||||
|
|
||||||
|
[MCP Apps](https://modelcontextprotocol.io/docs/extensions/apps) is an extension to the Model Context Protocol that lets MCP servers return interactive HTML UIs rendered directly inside chat conversations. Instead of text-only tool responses, users get full interactive interfaces -- dashboards, forms, data visualizations -- embedded inline.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @json-render/mcp @json-render/core @modelcontextprotocol/sdk
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### 1. Define your catalog
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { defineCatalog } from "@json-render/core";
|
||||||
|
import { schema } from "@json-render/react/schema";
|
||||||
|
import { shadcnComponentDefinitions } from "@json-render/shadcn/catalog";
|
||||||
|
|
||||||
|
const catalog = defineCatalog(schema, {
|
||||||
|
components: { ...shadcnComponentDefinitions },
|
||||||
|
actions: {},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Create the MCP server
|
||||||
|
|
||||||
|
```ts
|
||||||
|
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,
|
||||||
|
html: fs.readFileSync("dist/index.html", "utf-8"),
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.connect(new StdioServerTransport());
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Build the UI (iframe)
|
||||||
|
|
||||||
|
Create a React app that uses `useJsonRenderApp` from `@json-render/mcp/app`:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { useJsonRenderApp } from "@json-render/mcp/app";
|
||||||
|
import { JSONUIProvider, Renderer } from "@json-render/react";
|
||||||
|
|
||||||
|
function McpAppView({ registry }) {
|
||||||
|
const { spec, loading, connected, error } = useJsonRenderApp();
|
||||||
|
|
||||||
|
if (error) return <div>Error: {error.message}</div>;
|
||||||
|
if (!spec) return <div>Waiting for spec...</div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<JSONUIProvider registry={registry} initialState={spec.state ?? {}}>
|
||||||
|
<Renderer spec={spec} registry={registry} loading={loading} />
|
||||||
|
</JSONUIProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Bundle with Vite + `vite-plugin-singlefile` into a single HTML file, then pass it to `createMcpApp` as the `html` option.
|
||||||
|
|
||||||
|
### 4. Connect to a client
|
||||||
|
|
||||||
|
Add to `.cursor/mcp.json` or Claude Desktop config:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"my-app": {
|
||||||
|
"command": "node",
|
||||||
|
"args": ["./server.js", "--stdio"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
|
||||||
|
### Server Side (main export)
|
||||||
|
|
||||||
|
#### `createMcpApp(options)`
|
||||||
|
|
||||||
|
Creates a fully-configured `McpServer` with a json-render tool and UI resource.
|
||||||
|
|
||||||
|
| Option | Type | Description |
|
||||||
|
|--------|------|-------------|
|
||||||
|
| `name` | `string` | Server name shown in client UIs |
|
||||||
|
| `version` | `string` | Server version |
|
||||||
|
| `catalog` | `Catalog` | json-render catalog defining available components |
|
||||||
|
| `html` | `string` | Bundled HTML for the iframe UI |
|
||||||
|
| `tool` | `McpToolOptions` | Optional tool name/title/description overrides |
|
||||||
|
|
||||||
|
#### `registerJsonRenderTool(server, options)`
|
||||||
|
|
||||||
|
Register a json-render tool on an existing `McpServer`.
|
||||||
|
|
||||||
|
#### `registerJsonRenderResource(server, options)`
|
||||||
|
|
||||||
|
Register a json-render UI resource on an existing `McpServer`.
|
||||||
|
|
||||||
|
### Client Side (`@json-render/mcp/app`)
|
||||||
|
|
||||||
|
#### `useJsonRenderApp(options?)`
|
||||||
|
|
||||||
|
React hook for the iframe-side app. Connects to the MCP host, receives tool results, and maintains the current json-render spec.
|
||||||
|
|
||||||
|
Returns `{ spec, loading, connected, connecting, error, app, callServerTool }`.
|
||||||
|
|
||||||
|
#### `buildAppHtml(options)`
|
||||||
|
|
||||||
|
Generate a self-contained HTML string from bundled JS/CSS for use as a UI resource.
|
||||||
|
|
||||||
|
## Client Support
|
||||||
|
|
||||||
|
MCP Apps are supported by Claude, ChatGPT, VS Code (Copilot), Cursor, Goose, and Postman.
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
{
|
||||||
|
"name": "@json-render/mcp",
|
||||||
|
"version": "0.12.1",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"description": "MCP Apps integration for @json-render/core. Serve json-render UIs as interactive MCP Apps in Claude, ChatGPT, Cursor, and VS Code.",
|
||||||
|
"keywords": [
|
||||||
|
"json",
|
||||||
|
"ui",
|
||||||
|
"mcp",
|
||||||
|
"model-context-protocol",
|
||||||
|
"mcp-apps",
|
||||||
|
"ai",
|
||||||
|
"generative-ui",
|
||||||
|
"llm",
|
||||||
|
"renderer",
|
||||||
|
"claude",
|
||||||
|
"chatgpt",
|
||||||
|
"cursor"
|
||||||
|
],
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/vercel-labs/json-render.git",
|
||||||
|
"directory": "packages/mcp"
|
||||||
|
},
|
||||||
|
"homepage": "https://json-render.dev",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/vercel-labs/json-render/issues"
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"main": "./dist/index.js",
|
||||||
|
"module": "./dist/index.mjs",
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"import": "./dist/index.mjs",
|
||||||
|
"require": "./dist/index.js"
|
||||||
|
},
|
||||||
|
"./app": {
|
||||||
|
"types": "./dist/app.d.ts",
|
||||||
|
"import": "./dist/app.mjs",
|
||||||
|
"require": "./dist/app.js"
|
||||||
|
},
|
||||||
|
"./build-app-html": {
|
||||||
|
"types": "./dist/build-app-html-entry.d.ts",
|
||||||
|
"import": "./dist/build-app-html-entry.mjs",
|
||||||
|
"require": "./dist/build-app-html-entry.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsup",
|
||||||
|
"dev": "tsup --watch",
|
||||||
|
"typecheck": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@json-render/core": "workspace:*",
|
||||||
|
"@modelcontextprotocol/ext-apps": "^1.2.0",
|
||||||
|
"@modelcontextprotocol/sdk": "^1.27.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@internal/typescript-config": "workspace:*",
|
||||||
|
"@types/react": "19.2.3",
|
||||||
|
"tsup": "^8.0.2",
|
||||||
|
"typescript": "^5.4.5"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* Client-side (iframe) utilities for rendering json-render specs
|
||||||
|
* inside an MCP App view.
|
||||||
|
*
|
||||||
|
* This module is intended to run **inside the sandboxed iframe** that
|
||||||
|
* MCP hosts render. It connects to the host via the MCP Apps protocol,
|
||||||
|
* receives tool results containing json-render specs, and provides
|
||||||
|
* React hooks / helpers to render them.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```tsx
|
||||||
|
* import { useJsonRenderApp } from "@json-render/mcp/app";
|
||||||
|
* import { Renderer } from "@json-render/react";
|
||||||
|
*
|
||||||
|
* function McpAppView({ registry }) {
|
||||||
|
* const { spec, loading } = useJsonRenderApp();
|
||||||
|
* return <Renderer spec={spec} registry={registry} loading={loading} />;
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @packageDocumentation
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { useJsonRenderApp } from "./use-json-render-app.js";
|
||||||
|
export type {
|
||||||
|
UseJsonRenderAppOptions,
|
||||||
|
UseJsonRenderAppReturn,
|
||||||
|
} from "./use-json-render-app.js";
|
||||||
|
export { buildAppHtml } from "./build-app-html.js";
|
||||||
|
export type { BuildAppHtmlOptions } from "./build-app-html.js";
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
/**
|
||||||
|
* Server-side utility for building self-contained MCP App HTML.
|
||||||
|
*
|
||||||
|
* This entry point does NOT depend on React and is safe to import
|
||||||
|
* in Node.js / server environments.
|
||||||
|
*
|
||||||
|
* @packageDocumentation
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { buildAppHtml } from "./build-app-html.js";
|
||||||
|
export type { BuildAppHtmlOptions } from "./build-app-html.js";
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
/**
|
||||||
|
* Options for `buildAppHtml`.
|
||||||
|
*/
|
||||||
|
export interface BuildAppHtmlOptions {
|
||||||
|
/** Title for the HTML page. Defaults to `"json-render"`. */
|
||||||
|
title?: string;
|
||||||
|
/**
|
||||||
|
* Inline CSS to inject into the page `<style>` tag.
|
||||||
|
* Use this to include Tailwind output or custom styles.
|
||||||
|
*/
|
||||||
|
css?: string;
|
||||||
|
/**
|
||||||
|
* Bundled JavaScript for the app entry point.
|
||||||
|
* This should be the output of a bundler (Vite, esbuild, etc.)
|
||||||
|
* that bundles your React app, registry, and the
|
||||||
|
* `useJsonRenderApp` hook into a single script.
|
||||||
|
*/
|
||||||
|
js: string;
|
||||||
|
/**
|
||||||
|
* Additional `<head>` content (meta tags, font links, etc.).
|
||||||
|
*/
|
||||||
|
head?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a self-contained HTML string for an MCP App UI resource.
|
||||||
|
*
|
||||||
|
* The resulting HTML is designed to be served as a `ui://` resource
|
||||||
|
* via `registerJsonRenderResource` or `createMcpApp`.
|
||||||
|
*
|
||||||
|
* Typically you'd use a bundler (Vite + `vite-plugin-singlefile`, or
|
||||||
|
* esbuild) to produce the `js` and `css` strings, then pass them here.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* import { buildAppHtml } from "@json-render/mcp/app";
|
||||||
|
* import { readFileSync } from "node:fs";
|
||||||
|
*
|
||||||
|
* const html = buildAppHtml({
|
||||||
|
* title: "Dashboard",
|
||||||
|
* js: readFileSync("dist/app.js", "utf-8"),
|
||||||
|
* css: readFileSync("dist/app.css", "utf-8"),
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function buildAppHtml(options: BuildAppHtmlOptions): string {
|
||||||
|
const { title = "json-render", css = "", js, head = "" } = options;
|
||||||
|
|
||||||
|
return `<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>${escapeHtml(title)}</title>
|
||||||
|
${head}
|
||||||
|
<style>${css}</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module">${js}</script>
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(str: string): string {
|
||||||
|
return str
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/"/g, """);
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
export type {
|
||||||
|
CreateMcpAppOptions,
|
||||||
|
McpToolOptions,
|
||||||
|
RegisterToolOptions,
|
||||||
|
RegisterResourceOptions,
|
||||||
|
} from "./types.js";
|
||||||
|
|
||||||
|
export {
|
||||||
|
createMcpApp,
|
||||||
|
registerJsonRenderTool,
|
||||||
|
registerJsonRenderResource,
|
||||||
|
} from "./server.js";
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||||
|
import type {
|
||||||
|
CreateMcpAppOptions,
|
||||||
|
RegisterToolOptions,
|
||||||
|
RegisterResourceOptions,
|
||||||
|
} from "./types.js";
|
||||||
|
|
||||||
|
const RESOURCE_MIME_TYPE = "text/html;profile=mcp-app";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dynamically import the ext-apps server helpers to avoid CJS/ESM type
|
||||||
|
* mismatches at compile time while still getting the proper runtime
|
||||||
|
* `_meta.ui` normalization that hosts require.
|
||||||
|
*/
|
||||||
|
async function getExtApps() {
|
||||||
|
const mod = await import("@modelcontextprotocol/ext-apps/server");
|
||||||
|
return mod;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a json-render tool on an existing MCP server.
|
||||||
|
*
|
||||||
|
* Uses `registerAppTool` from `@modelcontextprotocol/ext-apps/server`
|
||||||
|
* so that MCP Apps-capable hosts (Claude, VS Code, Cursor, ChatGPT)
|
||||||
|
* see the `_meta.ui.resourceUri` in the tool listing and know to
|
||||||
|
* fetch and render the `ui://` resource as an interactive iframe.
|
||||||
|
*
|
||||||
|
* The tool accepts a json-render spec as input and returns it as text
|
||||||
|
* content, which the iframe receives via `ontoolresult`.
|
||||||
|
*/
|
||||||
|
export async function registerJsonRenderTool(
|
||||||
|
server: McpServer,
|
||||||
|
options: RegisterToolOptions,
|
||||||
|
): Promise<void> {
|
||||||
|
const { catalog, name, title, description, resourceUri } = options;
|
||||||
|
const { registerAppTool } = await getExtApps();
|
||||||
|
|
||||||
|
const specZodSchema = catalog.zodSchema();
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
(registerAppTool as any)(
|
||||||
|
server,
|
||||||
|
name,
|
||||||
|
{
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
inputSchema: { spec: specZodSchema },
|
||||||
|
_meta: { ui: { resourceUri } },
|
||||||
|
},
|
||||||
|
async (args: { spec?: unknown }) => {
|
||||||
|
const spec = args.spec;
|
||||||
|
const validation = catalog.validate(spec);
|
||||||
|
const validSpec = validation.success ? validation.data : spec;
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text" as const,
|
||||||
|
text: JSON.stringify(validSpec),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a json-render UI resource on an existing MCP server.
|
||||||
|
*
|
||||||
|
* The resource serves the self-contained HTML page that renders
|
||||||
|
* json-render specs received from tool results.
|
||||||
|
*/
|
||||||
|
export async function registerJsonRenderResource(
|
||||||
|
server: McpServer,
|
||||||
|
options: RegisterResourceOptions,
|
||||||
|
): Promise<void> {
|
||||||
|
const { resourceUri, html } = options;
|
||||||
|
const { registerAppResource, RESOURCE_MIME_TYPE: mimeType } =
|
||||||
|
await getExtApps();
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
(registerAppResource as any)(
|
||||||
|
server,
|
||||||
|
resourceUri,
|
||||||
|
resourceUri,
|
||||||
|
{ mimeType },
|
||||||
|
async () => ({
|
||||||
|
contents: [
|
||||||
|
{
|
||||||
|
uri: resourceUri,
|
||||||
|
mimeType,
|
||||||
|
text: html,
|
||||||
|
_meta: {
|
||||||
|
ui: {
|
||||||
|
csp: {
|
||||||
|
resourceDomains: ["https:"],
|
||||||
|
connectDomains: ["https:"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a fully-configured MCP server that serves a json-render catalog
|
||||||
|
* as an MCP App.
|
||||||
|
*
|
||||||
|
* This is the main entry point for most users. It creates an `McpServer`,
|
||||||
|
* registers the render tool and UI resource, and returns the server
|
||||||
|
* ready for transport connection.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* import { createMcpApp } from "@json-render/mcp";
|
||||||
|
*
|
||||||
|
* const server = createMcpApp({
|
||||||
|
* name: "My Dashboard",
|
||||||
|
* version: "1.0.0",
|
||||||
|
* catalog: myCatalog,
|
||||||
|
* html: myBundledHtml,
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* // Connect via stdio
|
||||||
|
* import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||||
|
* await server.connect(new StdioServerTransport());
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export async function createMcpApp(
|
||||||
|
options: CreateMcpAppOptions,
|
||||||
|
): Promise<McpServer> {
|
||||||
|
const { name, version, catalog, html, tool } = options;
|
||||||
|
|
||||||
|
const toolName = tool?.name ?? "render-ui";
|
||||||
|
const toolTitle = tool?.title ?? "Render UI";
|
||||||
|
const resourceUri = `ui://${toolName}/view.html`;
|
||||||
|
|
||||||
|
const catalogPrompt = catalog.prompt();
|
||||||
|
const toolDescription =
|
||||||
|
tool?.description ??
|
||||||
|
`Render an interactive UI. The spec argument must be a json-render spec conforming to the catalog.\n\n${catalogPrompt}`;
|
||||||
|
|
||||||
|
const server = new McpServer({ name, version });
|
||||||
|
|
||||||
|
await registerJsonRenderTool(server, {
|
||||||
|
catalog,
|
||||||
|
name: toolName,
|
||||||
|
title: toolTitle,
|
||||||
|
description: toolDescription,
|
||||||
|
resourceUri,
|
||||||
|
});
|
||||||
|
|
||||||
|
await registerJsonRenderResource(server, { resourceUri, html });
|
||||||
|
|
||||||
|
return server;
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import type { Catalog } from "@json-render/core";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options for creating an MCP App server backed by a json-render catalog.
|
||||||
|
*/
|
||||||
|
export interface CreateMcpAppOptions {
|
||||||
|
/** Display name for the MCP server shown in client UIs. */
|
||||||
|
name: string;
|
||||||
|
/** Semantic version of the MCP server. */
|
||||||
|
version: string;
|
||||||
|
/** The json-render catalog defining available components and actions. */
|
||||||
|
catalog: Catalog;
|
||||||
|
/**
|
||||||
|
* Pre-built HTML string for the UI resource.
|
||||||
|
* Generate this with `buildAppHtml` from `@json-render/mcp/app` or
|
||||||
|
* provide your own self-contained HTML page.
|
||||||
|
*/
|
||||||
|
html: string;
|
||||||
|
/**
|
||||||
|
* Optional tool configuration overrides.
|
||||||
|
*/
|
||||||
|
tool?: McpToolOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options for configuring the MCP tool that renders json-render specs.
|
||||||
|
*/
|
||||||
|
export interface McpToolOptions {
|
||||||
|
/** Tool name exposed to the LLM. Defaults to `"render-ui"`. */
|
||||||
|
name?: string;
|
||||||
|
/** Human-readable title. Defaults to `"Render UI"`. */
|
||||||
|
title?: string;
|
||||||
|
/** Tool description shown to the LLM. When omitted, a description is
|
||||||
|
* auto-generated from the catalog prompt. */
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options for registering the MCP App tool.
|
||||||
|
*/
|
||||||
|
export interface RegisterToolOptions {
|
||||||
|
/** The json-render catalog. */
|
||||||
|
catalog: Catalog;
|
||||||
|
/** Tool name. */
|
||||||
|
name: string;
|
||||||
|
/** Tool title. */
|
||||||
|
title: string;
|
||||||
|
/** Tool description. */
|
||||||
|
description: string;
|
||||||
|
/** The `ui://` resource URI this tool's view is served from. */
|
||||||
|
resourceUri: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options for registering the MCP App UI resource.
|
||||||
|
*/
|
||||||
|
export interface RegisterResourceOptions {
|
||||||
|
/** The `ui://` resource URI. */
|
||||||
|
resourceUri: string;
|
||||||
|
/** Self-contained HTML string for the view. */
|
||||||
|
html: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
import { useState, useEffect, useCallback, useRef } from "react";
|
||||||
|
import type { Spec } from "@json-render/core";
|
||||||
|
import { App } from "@modelcontextprotocol/ext-apps";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options for the `useJsonRenderApp` hook.
|
||||||
|
*/
|
||||||
|
export interface UseJsonRenderAppOptions {
|
||||||
|
/** App name shown during initialization. Defaults to `"json-render"`. */
|
||||||
|
name?: string;
|
||||||
|
/** App version. Defaults to `"1.0.0"`. */
|
||||||
|
version?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return value of `useJsonRenderApp`.
|
||||||
|
*/
|
||||||
|
export interface UseJsonRenderAppReturn {
|
||||||
|
/** The current json-render spec (null until the first tool result). */
|
||||||
|
spec: Spec | null;
|
||||||
|
/** Whether the app is still connecting to the host. */
|
||||||
|
connecting: boolean;
|
||||||
|
/** Whether the app is connected to the host. */
|
||||||
|
connected: boolean;
|
||||||
|
/** Connection error, if any. */
|
||||||
|
error: Error | null;
|
||||||
|
/** Whether the spec is still being received / parsed. */
|
||||||
|
loading: boolean;
|
||||||
|
/** The underlying MCP App instance. */
|
||||||
|
app: App | null;
|
||||||
|
/**
|
||||||
|
* Call a tool on the MCP server and update the spec from the result.
|
||||||
|
* Useful for refresh / drill-down interactions.
|
||||||
|
*/
|
||||||
|
callServerTool: (
|
||||||
|
name: string,
|
||||||
|
args?: Record<string, unknown>,
|
||||||
|
) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ToolResultContent {
|
||||||
|
type: string;
|
||||||
|
text?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseSpecFromToolResult(result: {
|
||||||
|
content?: ToolResultContent[];
|
||||||
|
}): Spec | null {
|
||||||
|
const textContent = result.content?.find(
|
||||||
|
(c: ToolResultContent) => c.type === "text",
|
||||||
|
);
|
||||||
|
if (!textContent?.text) return null;
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(textContent.text);
|
||||||
|
if (parsed && typeof parsed === "object" && "spec" in parsed) {
|
||||||
|
return parsed.spec as Spec;
|
||||||
|
}
|
||||||
|
return parsed as Spec;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* React hook that connects to the MCP host, listens for tool results,
|
||||||
|
* and maintains the current json-render spec.
|
||||||
|
*
|
||||||
|
* Follows the official MCP Apps pattern: create an `App` instance,
|
||||||
|
* register the `ontoolresult` handler, then call `app.connect()`
|
||||||
|
* which internally creates a PostMessageTransport to the host.
|
||||||
|
*/
|
||||||
|
export function useJsonRenderApp(
|
||||||
|
options: UseJsonRenderAppOptions = {},
|
||||||
|
): UseJsonRenderAppReturn {
|
||||||
|
const { name = "json-render", version = "1.0.0" } = options;
|
||||||
|
|
||||||
|
const [spec, setSpec] = useState<Spec | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [connected, setConnected] = useState(false);
|
||||||
|
const [error, setError] = useState<Error | null>(null);
|
||||||
|
const appRef = useRef<App | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const app = new App({ name, version });
|
||||||
|
appRef.current = app;
|
||||||
|
|
||||||
|
app.ontoolresult = (result: { content?: ToolResultContent[] }) => {
|
||||||
|
const parsed = parseSpecFromToolResult(result);
|
||||||
|
if (parsed) {
|
||||||
|
setSpec(parsed);
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Let the App class handle transport creation internally,
|
||||||
|
// matching the official MCP Apps quickstart pattern.
|
||||||
|
app
|
||||||
|
.connect()
|
||||||
|
.then(() => {
|
||||||
|
setConnected(true);
|
||||||
|
})
|
||||||
|
.catch((err: unknown) => {
|
||||||
|
setError(err instanceof Error ? err : new Error(String(err)));
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
app.close().catch(() => {});
|
||||||
|
};
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const callServerTool = useCallback(
|
||||||
|
async (toolName: string, args: Record<string, unknown> = {}) => {
|
||||||
|
if (!appRef.current) return;
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const result = await appRef.current.callServerTool({
|
||||||
|
name: toolName,
|
||||||
|
arguments: args,
|
||||||
|
});
|
||||||
|
const parsed = parseSpecFromToolResult(result);
|
||||||
|
if (parsed) setSpec(parsed);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
spec,
|
||||||
|
connecting: !connected && !error,
|
||||||
|
connected,
|
||||||
|
error,
|
||||||
|
loading,
|
||||||
|
app: appRef.current,
|
||||||
|
callServerTool,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"extends": "@internal/typescript-config/react-library.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src"
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { defineConfig } from "tsup";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
entry: ["src/index.ts", "src/app.ts", "src/build-app-html-entry.ts"],
|
||||||
|
format: ["cjs", "esm"],
|
||||||
|
dts: true,
|
||||||
|
sourcemap: true,
|
||||||
|
clean: true,
|
||||||
|
external: [
|
||||||
|
"react",
|
||||||
|
"react-dom",
|
||||||
|
"@json-render/core",
|
||||||
|
"@json-render/react",
|
||||||
|
"@modelcontextprotocol/sdk",
|
||||||
|
"@modelcontextprotocol/ext-apps",
|
||||||
|
],
|
||||||
|
});
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# @json-render/react-email
|
||||||
|
|
||||||
|
## 0.12.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [54a1ecf]
|
||||||
|
- @json-render/core@0.12.1
|
||||||
|
|
||||||
|
## 0.12.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- 63c339b: Add Svelte renderer, React Email renderer, and MCP Apps integration.
|
||||||
|
|
||||||
|
### New:
|
||||||
|
- **`@json-render/svelte`** — Svelte 5 renderer with runes-based reactivity. Full support for data binding, visibility, actions, validation, watchers, streaming, and repeat scopes. Includes `defineRegistry`, `Renderer`, `schema`, composables, and context providers.
|
||||||
|
- **`@json-render/react-email`** — React Email renderer for generating HTML and plain-text emails from JSON specs. 17 standard components (Html, Head, Body, Container, Section, Row, Column, Heading, Text, Link, Button, Image, Hr, Preview, Markdown). Server-side `renderToHtml` / `renderToPlainText` APIs. Custom catalog and registry support.
|
||||||
|
- **`@json-render/mcp`** — MCP Apps integration that serves json-render UIs as interactive apps inside Claude, ChatGPT, Cursor, VS Code, and other MCP-capable clients. `createMcpApp` server factory, `useJsonRenderApp` React hook for iframes, and `buildAppHtml` utility.
|
||||||
|
|
||||||
|
### Fixed:
|
||||||
|
- **`@json-render/svelte`** — Corrected JSDoc comment and added missing `zod` peer dependency.
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [63c339b]
|
||||||
|
- @json-render/core@0.12.0
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@json-render/react-email",
|
"name": "@json-render/react-email",
|
||||||
"version": "0.8.0",
|
"version": "0.12.1",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"description": "React Email renderer for @json-render/core. JSON becomes HTML emails.",
|
"description": "React Email renderer for @json-render/core. JSON becomes HTML emails.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
"url": "git+https://github.com/vercel-labs/json-render.git",
|
"url": "git+https://github.com/vercel-labs/json-render.git",
|
||||||
"directory": "packages/react-email"
|
"directory": "packages/react-email"
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/vercel-labs/json-render#readme",
|
"homepage": "https://json-render.dev",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/vercel-labs/json-render/issues"
|
"url": "https://github.com/vercel-labs/json-render/issues"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,19 @@
|
|||||||
# @json-render/react-native
|
# @json-render/react-native
|
||||||
|
|
||||||
|
## 0.12.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [54a1ecf]
|
||||||
|
- @json-render/core@0.12.1
|
||||||
|
|
||||||
|
## 0.12.0
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [63c339b]
|
||||||
|
- @json-render/core@0.12.0
|
||||||
|
|
||||||
## 0.11.0
|
## 0.11.0
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@json-render/react-native",
|
"name": "@json-render/react-native",
|
||||||
"version": "0.11.0",
|
"version": "0.12.1",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"description": "React Native renderer for @json-render/core. JSON becomes React Native components.",
|
"description": "React Native renderer for @json-render/core. JSON becomes React Native components.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
"url": "git+https://github.com/vercel-labs/json-render.git",
|
"url": "git+https://github.com/vercel-labs/json-render.git",
|
||||||
"directory": "packages/react-native"
|
"directory": "packages/react-native"
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/vercel-labs/json-render#readme",
|
"homepage": "https://json-render.dev",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/vercel-labs/json-render/issues"
|
"url": "https://github.com/vercel-labs/json-render/issues"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,19 @@
|
|||||||
# @json-render/react-pdf
|
# @json-render/react-pdf
|
||||||
|
|
||||||
|
## 0.12.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [54a1ecf]
|
||||||
|
- @json-render/core@0.12.1
|
||||||
|
|
||||||
|
## 0.12.0
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [63c339b]
|
||||||
|
- @json-render/core@0.12.0
|
||||||
|
|
||||||
## 0.11.0
|
## 0.11.0
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@json-render/react-pdf",
|
"name": "@json-render/react-pdf",
|
||||||
"version": "0.11.0",
|
"version": "0.12.1",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"description": "React PDF renderer for @json-render/core. JSON becomes PDF documents.",
|
"description": "React PDF renderer for @json-render/core. JSON becomes PDF documents.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
"url": "git+https://github.com/vercel-labs/json-render.git",
|
"url": "git+https://github.com/vercel-labs/json-render.git",
|
||||||
"directory": "packages/react-pdf"
|
"directory": "packages/react-pdf"
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/vercel-labs/json-render#readme",
|
"homepage": "https://json-render.dev",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/vercel-labs/json-render/issues"
|
"url": "https://github.com/vercel-labs/json-render/issues"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,19 @@
|
|||||||
# @internal/react-state
|
# @internal/react-state
|
||||||
|
|
||||||
|
## 0.8.6
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [54a1ecf]
|
||||||
|
- @json-render/core@0.12.1
|
||||||
|
|
||||||
|
## 0.8.5
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [63c339b]
|
||||||
|
- @json-render/core@0.12.0
|
||||||
|
|
||||||
## 0.8.4
|
## 0.8.4
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@internal/react-state",
|
"name": "@internal/react-state",
|
||||||
"version": "0.8.4",
|
"version": "0.8.6",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"description": "Shared React state context for json-render renderer packages",
|
"description": "Shared React state context for json-render renderer packages",
|
||||||
|
|||||||
@@ -1,5 +1,19 @@
|
|||||||
# @json-render/react
|
# @json-render/react
|
||||||
|
|
||||||
|
## 0.12.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [54a1ecf]
|
||||||
|
- @json-render/core@0.12.1
|
||||||
|
|
||||||
|
## 0.12.0
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [63c339b]
|
||||||
|
- @json-render/core@0.12.0
|
||||||
|
|
||||||
## 0.11.0
|
## 0.11.0
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@json-render/react",
|
"name": "@json-render/react",
|
||||||
"version": "0.11.0",
|
"version": "0.12.1",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"description": "React renderer for @json-render/core. JSON becomes React components.",
|
"description": "React renderer for @json-render/core. JSON becomes React components.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
"url": "git+https://github.com/vercel-labs/json-render.git",
|
"url": "git+https://github.com/vercel-labs/json-render.git",
|
||||||
"directory": "packages/react"
|
"directory": "packages/react"
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/vercel-labs/json-render#readme",
|
"homepage": "https://json-render.dev",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/vercel-labs/json-render/issues"
|
"url": "https://github.com/vercel-labs/json-render/issues"
|
||||||
},
|
},
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user