Compare commits

..
Author SHA1 Message Date
Chris Tate b2020fad6f cleanup 2026-03-09 02:06:22 -05:00
Chris Tate 5b51204960 vue 2026-03-09 01:34:12 -05:00
Chris Tate 3418d42a43 svelte/vue clients for mcp 2026-03-09 01:18:31 -05:00
133 changed files with 1707 additions and 571 deletions
+11 -3
View File
@@ -1,8 +1,16 @@
{
"mcpServers": {
"json-render": {
"json-render-react": {
"command": "npx",
"args": ["tsx", "examples/mcp/server.ts", "--stdio"]
"args": ["tsx", "examples/mcp-react/server.ts", "--stdio"]
},
"json-render-vue": {
"command": "npx",
"args": ["tsx", "examples/mcp-vue/server.ts", "--stdio"]
},
"json-render-svelte": {
"command": "npx",
"args": ["tsx", "examples/mcp-svelte/server.ts", "--stdio"]
}
}
}
}
+1 -1
View File
@@ -3,7 +3,7 @@
"json-render": {
"type": "stdio",
"command": "npx",
"args": ["tsx", "examples/mcp/server.ts", "--stdio"]
"args": ["tsx", "examples/mcp-react/server.ts", "--stdio"]
}
}
}
+1
View File
@@ -486,6 +486,7 @@ pnpm dev
- Chat Example: run `pnpm dev` in `examples/chat`
- Svelte Example: run `pnpm dev` in `examples/svelte` or `examples/svelte-chat`
- Vue Example: run `pnpm dev` in `examples/vue`
- MCP Vue Example: run `pnpm build && pnpm start:stdio` in `examples/mcp-vue`
- Vite Renderers (React + Vue + Svelte): run `pnpm dev` in `examples/vite-renderers`
- React Native example: run `npx expo start` in `examples/react-native`
-9
View File
@@ -1,14 +1,5 @@
# 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
+12 -12
View File
@@ -3,7 +3,7 @@ export const metadata = pageMetadata("docs/ai-sdk")
# AI SDK Integration
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.
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.
## 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
```
## Standalone Mode
## Generate Mode
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.
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.
### API Route
@@ -73,9 +73,9 @@ function GenerativeUI() {
}
```
## Inline Mode
## Chat Mode
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.
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.
### API Route
@@ -96,7 +96,7 @@ export async function POST(req: Request) {
const result = streamText({
model: yourModel,
system: catalog.prompt({ mode: "inline" }),
system: catalog.prompt({ mode: "chat" }),
messages,
});
@@ -182,13 +182,13 @@ const systemPrompt = catalog.prompt({
});
```
### Inline Mode Prompt
### Chat Mode Prompt
```typescript
const inlinePrompt = catalog.prompt({ mode: "inline" });
const chatPrompt = catalog.prompt({ mode: "chat" });
```
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.
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.
## Which Mode?
@@ -197,8 +197,8 @@ In inline mode, the prompt instructs the AI to respond conversationally first, t
<thead>
<tr>
<th></th>
<th>Standalone</th>
<th>Inline</th>
<th>Generate</th>
<th>Chat</th>
</tr>
</thead>
<tbody>
@@ -215,7 +215,7 @@ In inline mode, the prompt instructs the AI to respond conversationally first, t
<tr>
<td>System prompt</td>
<td><code>catalog.prompt()</code></td>
<td><code>{"catalog.prompt({ mode: \"inline\" })"}</code></td>
<td><code>{"catalog.prompt({ mode: \"chat\" })"}</code></td>
</tr>
<tr>
<td>Stream utility</td>
+4 -4
View File
@@ -75,7 +75,7 @@ interface Catalog {
interface PromptOptions {
system?: string; // Custom system message intro
customRules?: string[]; // Additional rules to append
mode?: "standalone" | "inline" | "generate" | "chat"; // Output mode (default: "standalone")
mode?: "generate" | "chat"; // Output mode (default: "generate")
}
interface SpecValidationResult<T> {
@@ -370,7 +370,7 @@ Most users should use `pipeJsonRender()` instead, which wraps this transform for
### createMixedStreamParser
Parse a mixed stream of text and JSONL patches (used for Inline mode):
Parse a mixed stream of text and JSONL patches (used for Chat + GenUI mode):
```typescript
import { createMixedStreamParser } from '@json-render/core';
@@ -389,7 +389,7 @@ parser.flush();
### 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 Inline 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 Chat mode API routes.
```typescript
import { pipeJsonRender } from '@json-render/core';
@@ -403,7 +403,7 @@ const stream = createUIMessageStream({
return createUIMessageStreamResponse({ stream });
```
See [Generation Modes](/docs/generation-modes) for full Inline mode setup.
See [Generation Modes](/docs/generation-modes) for full Chat mode setup.
### SpecStream Types
+68 -9
View File
@@ -11,13 +11,20 @@ MCP Apps integration for json-render. Serve json-render UIs as interactive [MCP
npm install @json-render/mcp @json-render/core @modelcontextprotocol/sdk
```
For the iframe-side React UI, also install:
For the iframe-side UI, install the renderer for your framework:
```bash
# React
npm install @json-render/react react react-dom
# Vue
npm install @json-render/vue vue
# Svelte
npm install @json-render/svelte svelte
```
See the [MCP example](https://github.com/vercel-labs/json-render/tree/main/examples/mcp) for a full working example.
See the [MCP React example](https://github.com/vercel-labs/json-render/tree/main/examples/mcp-react) for a full working example.
## Overview
@@ -117,16 +124,22 @@ registerJsonRenderResource(server, {
});
```
## Client API (`@json-render/mcp/app`)
## Client API
These exports run inside the sandboxed iframe rendered by the MCP host.
These exports run inside the sandboxed iframe rendered by the MCP host. Framework-specific adapters are available at:
### useJsonRenderApp
- `@json-render/mcp/app/react` -- React hook
- `@json-render/mcp/app/vue` -- Vue composable
- `@json-render/mcp/app/svelte` -- Svelte stores
The React hook is also re-exported from `@json-render/mcp/app` for backward compatibility.
### React -- useJsonRenderApp (`@json-render/mcp/app/react`)
React hook that connects to the MCP host, listens for tool results, and maintains the current json-render spec.
```tsx
import { useJsonRenderApp } from "@json-render/mcp/app";
import { useJsonRenderApp } from "@json-render/mcp/app/react";
import { JSONUIProvider, Renderer } from "@json-render/react";
function McpAppView({ registry }) {
@@ -146,7 +159,51 @@ function McpAppView({ registry }) {
}
```
#### UseJsonRenderAppReturn
### Vue -- useJsonRenderApp (`@json-render/mcp/app/vue`)
Vue composable with the same API shape. Values are Vue refs and computed properties.
```vue
<script setup>
import { useJsonRenderApp } from "@json-render/mcp/app/vue";
import { Renderer } from "@json-render/vue";
const { spec, loading, error } = useJsonRenderApp({ name: "my-app" });
</script>
<template>
<div v-if="error">Error: {{ error.message }}</div>
<div v-else-if="!spec">Waiting...</div>
<Renderer v-else :spec="spec" :registry="registry" :loading="loading" />
</template>
```
### Svelte -- createJsonRenderApp (`@json-render/mcp/app/svelte`)
Creates a json-render MCP App client using Svelte stores. Call `destroy()` in `onDestroy` to clean up.
```svelte
<script>
import { createJsonRenderApp } from "@json-render/mcp/app/svelte";
import { Renderer } from "@json-render/svelte";
import { onDestroy } from "svelte";
const { spec, loading, error, destroy } = createJsonRenderApp({ name: "my-app" });
onDestroy(destroy);
</script>
{#if $error}
<div>Error: {$error.message}</div>
{:else if !$spec}
<div>Waiting...</div>
{:else}
<Renderer spec={$spec} {registry} loading={$loading} />
{/if}
```
### Return values
All three adapters return the same logical state:
<table>
<thead>
@@ -195,9 +252,11 @@ function McpAppView({ registry }) {
</tbody>
</table>
### buildAppHtml
In React, values are plain primitives. In Vue, they are `Ref`/`ShallowRef`/`ComputedRef`. In Svelte, they are readable stores (use `$` prefix to subscribe). The Svelte adapter also returns a `destroy` function for cleanup.
Generate a self-contained HTML page from bundled JavaScript and CSS.
### buildAppHtml (`@json-render/mcp/app`)
Generate a self-contained HTML page from bundled JavaScript and CSS. Framework-agnostic.
```typescript
import { buildAppHtml } from "@json-render/mcp/app";
@@ -341,8 +341,6 @@ February 2026
### 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.
```typescript
@@ -3,15 +3,15 @@ export const metadata = pageMetadata("docs/generation-modes")
# Generation Modes
json-render supports two modes for AI-generated UI: **Standalone mode** for standalone UI and **Inline mode** for inline UI within a conversation.
json-render supports two modes for AI-generated UI: **Generate mode** for standalone UI and **Chat 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.
<GenerationModesDiagram />
## Standalone Mode
## Generate Mode (Standalone)
In standalone mode, the AI outputs **only JSONL patches** — no prose, no markdown. The entire response is a UI spec.
In generate 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:
@@ -25,7 +25,7 @@ This is the default mode and is ideal for:
```typescript
import { streamText } from "ai";
// Standalone mode is the default (no mode option needed)
// Generate mode is the default (no mode option needed)
const systemPrompt = catalog.prompt({
customRules: [
"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"}}}
```
## Inline Mode
## Chat Mode (Inline)
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).
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).
This is ideal for:
@@ -92,8 +92,8 @@ import { streamText } from "ai";
import { pipeJsonRender } from "@json-render/core";
import { createUIMessageStream, createUIMessageStreamResponse } from "ai";
// Enable inline mode
const systemPrompt = catalog.prompt({ mode: "inline" });
// Enable chat mode
const systemPrompt = catalog.prompt({ mode: "chat" });
const result = streamText({
model: yourModel,
@@ -180,8 +180,8 @@ If the user asks a simple question ("what does BTC stand for?"), the AI replies
<thead>
<tr>
<th />
<th>Standalone</th>
<th>Inline</th>
<th>Generate</th>
<th>Chat</th>
</tr>
</thead>
<tbody>
@@ -198,7 +198,7 @@ If the user asks a simple question ("what does BTC stand for?"), the AI replies
<tr>
<td>System prompt</td>
<td><code>{"catalog.prompt()"}</code></td>
<td><code>{'catalog.prompt({ mode: "inline" })'}</code></td>
<td><code>{'catalog.prompt({ mode: "chat" })'}</code></td>
</tr>
<tr>
<td>Stream utility</td>
+1 -26
View File
@@ -309,35 +309,10 @@ const catalog = defineCatalog(schema, {
const prompt = catalog.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" });
// Chat mode prompt
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
`ValidationCheck` now uses `type` instead of `fn`, `ValidationProvider` uses `customFunctions` instead of `functions`, and `useFieldValidation` takes a config object instead of a checks array.
+1 -1
View File
@@ -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.
- **[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.
- **[Generation Modes](/docs/generation-modes)** — Standalone mode for full-page generated UI or inline mode for UI embedded in a conversation.
- **[Generation Modes](/docs/generation-modes)** — Generate standalone UI (playground/builder) or inline UI within a chat conversation.
## Next
@@ -19,11 +19,11 @@ function FormUI() {
);
}
function InlineModeDiagram() {
function ChatModeDiagram() {
return (
<div className="flex flex-col h-full">
<div className="text-xs font-medium text-muted-foreground mb-3 text-center">
Inline Mode
Chat Mode
</div>
<div className="flex-1 border border-border rounded-lg bg-background overflow-hidden flex flex-col">
{/* Chat area */}
@@ -69,11 +69,11 @@ function InlineModeDiagram() {
);
}
function StandaloneModeDiagram() {
function GenerateModeDiagram() {
return (
<div className="flex flex-col h-full">
<div className="text-xs font-medium text-muted-foreground mb-3 text-center">
Standalone Mode
Generate Mode
</div>
<div className="flex-1 border border-border rounded-lg bg-background overflow-hidden flex flex-row">
{/* Left panel - prompt */}
@@ -104,10 +104,10 @@ export function GenerationModesDiagram() {
<div className="not-prose my-8">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
<div className="h-[280px]">
<InlineModeDiagram />
<ChatModeDiagram />
</div>
<div className="h-[280px]">
<StandaloneModeDiagram />
<GenerateModeDiagram />
</div>
</div>
</div>
+1 -1
View File
@@ -98,7 +98,7 @@ export const docsNavigation: NavSection[] = [
},
{
title: "MCP App",
href: "https://github.com/vercel-labs/json-render/tree/main/examples/mcp",
href: "https://github.com/vercel-labs/json-render/tree/main/examples/mcp-react",
external: true,
},
],
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "web",
"version": "0.1.6",
"version": "0.1.5",
"type": "module",
"private": true,
"license": "Apache-2.0",
-9
View File
@@ -1,14 +1,5 @@
# 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
+1 -1
View File
@@ -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.
${explorerCatalog.prompt({
mode: "inline",
mode: "chat",
customRules: [
"NEVER use viewport height classes (min-h-screen, h-screen) — the UI renders inside a fixed-size container.",
"Prefer Grid with columns='2' or columns='3' for side-by-side layouts.",
+1 -1
View File
@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts";
import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-chat",
"version": "0.1.6",
"version": "0.1.5",
"type": "module",
"private": true,
"scripts": {
-9
View File
@@ -1,14 +1,5 @@
# 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
+1 -1
View File
@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts";
import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-dashboard",
"version": "0.1.6",
"version": "0.1.5",
"type": "module",
"private": true,
"scripts": {
-8
View File
@@ -1,13 +1,5 @@
# 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
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-image",
"version": "0.1.3",
"version": "0.1.2",
"type": "module",
"private": true,
"scripts": {
@@ -1,15 +1,5 @@
# 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
@@ -20,7 +20,7 @@ Add to `.cursor/mcp.json`:
"mcpServers": {
"json-render": {
"command": "npx",
"args": ["tsx", "examples/mcp/server.ts", "--stdio"]
"args": ["tsx", "examples/mcp-react/server.ts", "--stdio"]
}
}
}
@@ -35,7 +35,7 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
"mcpServers": {
"json-render": {
"command": "npx",
"args": ["tsx", "/absolute/path/to/examples/mcp/server.ts", "--stdio"]
"args": ["tsx", "/absolute/path/to/examples/mcp-react/server.ts", "--stdio"]
}
}
}
@@ -1,6 +1,6 @@
{
"name": "example-mcp",
"version": "0.1.2",
"name": "example-mcp-react",
"version": "0.1.1",
"type": "module",
"private": true,
"scripts": {
@@ -16,8 +16,8 @@
"@json-render/shadcn": "workspace:*",
"@modelcontextprotocol/ext-apps": "^1.2.0",
"@modelcontextprotocol/sdk": "^1.27.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react": "19.2.3",
"react-dom": "19.2.3",
"zod": "^4.3.6"
},
"devDependencies": {
@@ -1,7 +1,7 @@
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 { useJsonRenderApp } from "@json-render/mcp/app/react";
import { catalog } from "./catalog";
import { useState, useEffect } from "react";
@@ -5,9 +5,6 @@ import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
plugins: [react(), tailwindcss(), viteSingleFile()],
resolve: {
dedupe: ["react", "react-dom"],
},
build: {
outDir: "dist",
emptyOutDir: false,
+13
View File
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline' 'unsafe-eval' data: blob:; img-src * data: blob:; font-src * data:; connect-src *; media-src *;" />
<title>json-render MCP App (Svelte)</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
+28
View File
@@ -0,0 +1,28 @@
{
"name": "example-mcp-svelte",
"version": "0.1.0",
"type": "module",
"private": true,
"scripts": {
"build": "vite build",
"start": "tsx server.ts --stdio"
},
"dependencies": {
"@json-render/core": "workspace:*",
"@json-render/mcp": "workspace:*",
"@json-render/svelte": "workspace:*",
"@modelcontextprotocol/ext-apps": "^1.2.0",
"@modelcontextprotocol/sdk": "^1.27.1",
"svelte": "^5.49.2",
"zod": "^4.3.6"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^6.2.4",
"@tailwindcss/vite": "^4.2.1",
"tailwindcss": "^4.2.1",
"tsx": "^4.21.0",
"typescript": "^5.9.3",
"vite": "^7.3.1",
"vite-plugin-singlefile": "^2.3.0"
}
}
+34
View File
@@ -0,0 +1,34 @@
import { createMcpApp } from "@json-render/mcp";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { catalog } from "./src/catalog.js";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
function loadHtml(): string {
const htmlPath = path.join(__dirname, "dist", "index.html");
if (!fs.existsSync(htmlPath)) {
throw new Error(
`Built HTML not found at ${htmlPath}. Run 'pnpm build' first.`,
);
}
return fs.readFileSync(htmlPath, "utf-8");
}
async function main() {
const html = loadHtml();
const server = await createMcpApp({
name: "json-render Svelte Example",
version: "1.0.0",
catalog,
html,
});
await server.connect(new StdioServerTransport());
}
main().catch((e) => {
console.error(e);
process.exit(1);
});
+26
View File
@@ -0,0 +1,26 @@
<script lang="ts">
import { createJsonRenderApp } from "@json-render/mcp/app/svelte";
import { JsonUIProvider, Renderer } from "@json-render/svelte";
import { onDestroy } from "svelte";
import { registry } from "./registry";
const { spec, loading, connecting, error, destroy } = createJsonRenderApp({
name: "json-render-mcp-svelte",
version: "1.0.0",
});
onDestroy(destroy);
</script>
{#if $error}
<div class="p-4 text-destructive font-mono text-sm">
{$error.message}
</div>
{:else if !$spec}
<div class="p-4 text-muted-foreground text-sm">
{$connecting ? "Connecting to host..." : "Waiting for UI spec..."}
</div>
{:else}
<JsonUIProvider initialState={$spec.state ?? {}}>
<Renderer spec={$spec} {registry} loading={$loading} />
</JsonUIProvider>
{/if}
+54
View File
@@ -0,0 +1,54 @@
import { schema } from "@json-render/svelte/schema";
import { z } from "zod";
export const catalog = schema.createCatalog({
components: {
Stack: {
props: z.object({
gap: z.number().optional(),
padding: z.number().optional(),
direction: z.enum(["vertical", "horizontal"]).optional(),
align: z.enum(["start", "center", "end"]).optional(),
}),
slots: ["default"],
description:
"Layout container that stacks children vertically or horizontally",
},
Card: {
props: z.object({
title: z.string().optional(),
subtitle: z.string().optional(),
}),
slots: ["default"],
description: "A card container with optional title and subtitle",
},
Text: {
props: z.object({
content: z.string(),
size: z.enum(["sm", "md", "lg", "xl"]).optional(),
weight: z.enum(["normal", "medium", "bold"]).optional(),
color: z.string().optional(),
}),
slots: [],
description: "Displays a text string",
},
Button: {
props: z.object({
label: z.string(),
variant: z.enum(["primary", "secondary", "danger"]).optional(),
disabled: z.boolean().optional(),
}),
slots: [],
description: "A clickable button that emits a 'press' event",
},
Badge: {
props: z.object({
label: z.string(),
color: z.string().optional(),
}),
slots: [],
description: "A small badge/tag label",
},
},
actions: {},
});
@@ -0,0 +1,18 @@
<script lang="ts">
import type { BaseComponentProps } from "@json-render/svelte";
interface Props extends BaseComponentProps<{
label: string;
color?: string;
}> {}
let { props }: Props = $props();
</script>
<span
class="inline-flex items-center rounded-full px-3 py-1 text-xs font-medium bg-secondary text-secondary-foreground border border-border"
style:background-color={props.color ? `${props.color}20` : undefined}
style:color={props.color ?? undefined}
style:border-color={props.color ? `${props.color}40` : undefined}>
{props.label}
</span>
@@ -0,0 +1,26 @@
<script lang="ts">
import type { BaseComponentProps } from "@json-render/svelte";
interface Props extends BaseComponentProps<{
label: string;
variant?: "primary" | "secondary" | "danger";
disabled?: boolean;
}> {}
let { props, emit }: Props = $props();
const variantClasses: Record<string, string> = {
primary: "bg-primary text-primary-foreground hover:bg-primary/90",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
danger: "bg-destructive/10 text-destructive hover:bg-destructive/20 border border-destructive/20",
};
let variant = $derived(props.variant ?? "primary");
</script>
<button
disabled={props.disabled}
onclick={() => emit("press")}
class="inline-flex items-center justify-center rounded-lg px-4 py-2 text-sm font-medium transition-colors {variantClasses[variant] ?? variantClasses.primary} {props.disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}">
{props.label}
</button>
@@ -0,0 +1,29 @@
<script lang="ts">
import type { Snippet } from "svelte";
import type { BaseComponentProps } from "@json-render/svelte";
interface Props extends BaseComponentProps<{
title?: string;
subtitle?: string;
}> {
children?: Snippet;
}
let { props, children }: Props = $props();
</script>
<div class="rounded-xl border border-border bg-card text-card-foreground p-5 shadow-sm">
{#if props.title}
<h2 class="text-base font-semibold text-card-foreground mb-1">
{props.title}
</h2>
{/if}
{#if props.subtitle}
<p class="text-sm text-muted-foreground mb-3">
{props.subtitle}
</p>
{/if}
{#if children}
{@render children()}
{/if}
</div>
@@ -0,0 +1,33 @@
<script lang="ts">
import type { Snippet } from "svelte";
import type { BaseComponentProps } from "@json-render/svelte";
interface Props extends BaseComponentProps<{
gap?: number;
padding?: number;
direction?: "vertical" | "horizontal";
align?: "start" | "center" | "end";
}> {
children?: Snippet;
}
let { props, children }: Props = $props();
let horizontal = $derived(props.direction === "horizontal");
</script>
<div
class="flex"
style:flex-direction={horizontal ? "row" : "column"}
style:gap="{props.gap ?? 0}px"
style:padding="{props.padding ?? 0}px"
style:align-items={props.align === "start"
? "flex-start"
: props.align === "end"
? "flex-end"
: horizontal
? "center"
: "stretch"}>
{#if children}
{@render children()}
{/if}
</div>
@@ -0,0 +1,31 @@
<script lang="ts">
import type { BaseComponentProps } from "@json-render/svelte";
interface Props extends BaseComponentProps<{
content: string;
size?: "sm" | "md" | "lg" | "xl";
weight?: "normal" | "medium" | "bold";
color?: string;
}> {}
let { props }: Props = $props();
const sizeClasses: Record<string, string> = {
sm: "text-xs",
md: "text-sm",
lg: "text-base",
xl: "text-2xl",
};
const weightClasses: Record<string, string> = {
normal: "font-normal",
medium: "font-medium",
bold: "font-bold",
};
</script>
<span
class="{sizeClasses[props.size ?? 'md'] ?? 'text-sm'} {weightClasses[props.weight ?? 'normal'] ?? 'font-normal'} text-foreground"
style:color={props.color ?? undefined}>
{String(props.content ?? "")}
</span>
+98
View File
@@ -0,0 +1,98 @@
@import "tailwindcss";
@custom-variant dark (&:is([data-theme="dark"] *));
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--radius-2xl: calc(var(--radius) + 8px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
}
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
}
[data-theme="dark"] {
--background: var(--color-background-secondary, var(--vscode-editor-background, oklch(0.145 0 0)));
--foreground: var(--color-text-primary, var(--vscode-foreground, oklch(0.985 0 0)));
--card: var(--color-background-primary, var(--vscode-sideBar-background, oklch(0.205 0 0)));
--card-foreground: var(--color-text-primary, var(--vscode-foreground, oklch(0.985 0 0)));
--primary: var(--color-text-primary, var(--vscode-foreground, oklch(0.922 0 0)));
--primary-foreground: var(--color-background-primary, var(--vscode-sideBar-background, oklch(0.205 0 0)));
--secondary: var(--color-background-tertiary, var(--vscode-activityBar-background, oklch(0.269 0 0)));
--secondary-foreground: var(--color-text-primary, var(--vscode-foreground, oklch(0.985 0 0)));
--muted: var(--color-background-tertiary, var(--vscode-activityBar-background, oklch(0.269 0 0)));
--muted-foreground: var(--color-text-secondary, var(--vscode-descriptionForeground, oklch(0.708 0 0)));
--accent: var(--color-background-tertiary, var(--vscode-activityBar-background, oklch(0.269 0 0)));
--accent-foreground: var(--color-text-primary, var(--vscode-foreground, oklch(0.985 0 0)));
--destructive: var(--color-text-danger, var(--vscode-errorForeground, oklch(0.704 0.191 22.216)));
--border: var(--color-border-primary, var(--vscode-widget-border, oklch(1 0 0 / 10%)));
--input: var(--color-border-secondary, var(--vscode-editorWidget-border, oklch(1 0 0 / 15%)));
--ring: var(--color-ring-primary, var(--vscode-focusBorder, oklch(0.556 0 0)));
}
* {
box-sizing: border-box;
border-color: var(--color-border);
}
html, body, #app {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
body {
background-color: var(--color-background);
color: var(--color-foreground);
font-family: var(--vscode-font-family, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply text-foreground;
background-color: var(--color-background) !important;
}
}
button {
cursor: pointer;
}
+12
View File
@@ -0,0 +1,12 @@
import "./globals.css";
import { mount } from "svelte";
import App from "./App.svelte";
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
document.documentElement.setAttribute(
"data-theme",
prefersDark ? "dark" : "light",
);
document.documentElement.style.colorScheme = prefersDark ? "dark" : "light";
mount(App, { target: document.getElementById("app")! });
+18
View File
@@ -0,0 +1,18 @@
import { type Components, defineRegistry } from "@json-render/svelte";
import { catalog } from "./catalog";
import Stack from "./components/Stack.svelte";
import Card from "./components/Card.svelte";
import Text from "./components/Text.svelte";
import Button from "./components/Button.svelte";
import Badge from "./components/Badge.svelte";
const components: Components<typeof catalog> = {
Stack,
Card,
Text,
Button,
Badge,
};
export const { registry } = defineRegistry(catalog, { components });
+1
View File
@@ -0,0 +1 @@
export default {};
+16
View File
@@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
"noEmit": true,
"strict": true,
"skipLibCheck": true
},
"include": ["src", "server.ts"]
}
+14
View File
@@ -0,0 +1,14 @@
import { defineConfig } from "vite";
import { svelte } from "@sveltejs/vite-plugin-svelte";
import { viteSingleFile } from "vite-plugin-singlefile";
export default defineConfig({
plugins: [svelte(), viteSingleFile()],
build: {
outDir: "dist",
emptyOutDir: false,
rollupOptions: {
input: "index.html",
},
},
});
+13
View File
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline' 'unsafe-eval' data: blob:; img-src * data: blob:; font-src * data:; connect-src *; media-src *;" />
<title>json-render MCP App (Vue)</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
+29
View File
@@ -0,0 +1,29 @@
{
"name": "example-mcp-vue",
"version": "0.1.0",
"type": "module",
"private": true,
"scripts": {
"build": "vite build",
"start": "tsx server.ts",
"start:stdio": "tsx server.ts --stdio"
},
"dependencies": {
"@json-render/core": "workspace:*",
"@json-render/mcp": "workspace:*",
"@json-render/vue": "workspace:*",
"@modelcontextprotocol/ext-apps": "^1.2.0",
"@modelcontextprotocol/sdk": "^1.27.1",
"vue": "^3.5.0",
"zod": "^4.3.6"
},
"devDependencies": {
"@tailwindcss/vite": "^4.2.1",
"@vitejs/plugin-vue": "^6.0.4",
"tailwindcss": "^4.2.1",
"tsx": "^4.21.0",
"typescript": "^5.9.3",
"vite": "^7.3.1",
"vite-plugin-singlefile": "^2.3.0"
}
}
+34
View File
@@ -0,0 +1,34 @@
import { createMcpApp } from "@json-render/mcp";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { catalog } from "./src/catalog.js";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
function loadHtml(): string {
const htmlPath = path.join(__dirname, "dist", "index.html");
if (!fs.existsSync(htmlPath)) {
throw new Error(
`Built HTML not found at ${htmlPath}. Run 'pnpm build' first.`,
);
}
return fs.readFileSync(htmlPath, "utf-8");
}
async function main() {
const html = loadHtml();
const server = await createMcpApp({
name: "json-render Vue Example",
version: "1.0.0",
catalog,
html,
});
await server.connect(new StdioServerTransport());
}
main().catch((e) => {
console.error(e);
process.exit(1);
});
+31
View File
@@ -0,0 +1,31 @@
<script setup lang="ts">
import { useJsonRenderApp } from "@json-render/mcp/app/vue";
import { Renderer, defineRegistry, JSONUIProvider } from "@json-render/vue";
import { catalog } from "./catalog";
import { components } from "./registry";
const { registry } = defineRegistry(catalog, { components });
const { spec, loading, connecting, error } = useJsonRenderApp({
name: "json-render-mcp-vue",
version: "1.0.0",
});
</script>
<template>
<div
v-if="error"
class="p-4 font-mono text-sm text-destructive"
>
{{ error.message }}
</div>
<div
v-else-if="!spec"
class="p-4 text-sm text-muted-foreground"
>
{{ connecting ? "Connecting to host..." : "Waiting for UI spec..." }}
</div>
<JSONUIProvider v-else :registry="registry" :initialState="spec.state ?? {}">
<Renderer :spec="spec" :registry="registry" :loading="loading" />
</JSONUIProvider>
</template>
+56
View File
@@ -0,0 +1,56 @@
import { schema } from "@json-render/vue/schema";
import { z } from "zod";
export const catalog = schema.createCatalog({
components: {
Stack: {
props: z.object({
gap: z.number().optional(),
padding: z.number().optional(),
direction: z.enum(["vertical", "horizontal"]).optional(),
align: z.enum(["start", "center", "end"]).optional(),
}),
slots: ["default"],
description:
"Layout container that stacks children vertically or horizontally",
},
Card: {
props: z.object({
title: z.string().optional(),
subtitle: z.string().optional(),
}),
slots: ["default"],
description: "A card container with optional title and subtitle",
},
Text: {
props: z.object({
content: z.string(),
size: z.enum(["sm", "md", "lg", "xl"]).optional(),
weight: z.enum(["normal", "medium", "bold"]).optional(),
color: z.string().optional(),
}),
slots: [],
description: "Displays a text string",
},
Button: {
props: z.object({
label: z.string(),
variant: z.enum(["primary", "secondary", "danger"]).optional(),
disabled: z.boolean().optional(),
}),
slots: [],
description: "A clickable button that emits a 'press' event",
},
Badge: {
props: z.object({
label: z.string(),
color: z.string().optional(),
}),
slots: [],
description: "A small badge/tag label",
},
},
actions: {},
});
export type AppCatalog = typeof catalog;
+98
View File
@@ -0,0 +1,98 @@
@import "tailwindcss";
@custom-variant dark (&:is([data-theme="dark"] *));
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--radius-2xl: calc(var(--radius) + 8px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
}
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
}
[data-theme="dark"] {
--background: var(--color-background-secondary, var(--vscode-editor-background, oklch(0.145 0 0)));
--foreground: var(--color-text-primary, var(--vscode-foreground, oklch(0.985 0 0)));
--card: var(--color-background-primary, var(--vscode-sideBar-background, oklch(0.205 0 0)));
--card-foreground: var(--color-text-primary, var(--vscode-foreground, oklch(0.985 0 0)));
--primary: var(--color-text-primary, var(--vscode-foreground, oklch(0.922 0 0)));
--primary-foreground: var(--color-background-primary, var(--vscode-sideBar-background, oklch(0.205 0 0)));
--secondary: var(--color-background-tertiary, var(--vscode-activityBar-background, oklch(0.269 0 0)));
--secondary-foreground: var(--color-text-primary, var(--vscode-foreground, oklch(0.985 0 0)));
--muted: var(--color-background-tertiary, var(--vscode-activityBar-background, oklch(0.269 0 0)));
--muted-foreground: var(--color-text-secondary, var(--vscode-descriptionForeground, oklch(0.708 0 0)));
--accent: var(--color-background-tertiary, var(--vscode-activityBar-background, oklch(0.269 0 0)));
--accent-foreground: var(--color-text-primary, var(--vscode-foreground, oklch(0.985 0 0)));
--destructive: var(--color-text-danger, var(--vscode-errorForeground, oklch(0.704 0.191 22.216)));
--border: var(--color-border-primary, var(--vscode-widget-border, oklch(1 0 0 / 10%)));
--input: var(--color-border-secondary, var(--vscode-editorWidget-border, oklch(1 0 0 / 15%)));
--ring: var(--color-ring-primary, var(--vscode-focusBorder, oklch(0.556 0 0)));
}
* {
box-sizing: border-box;
border-color: var(--color-border);
}
html, body, #app {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
body {
background-color: var(--color-background);
color: var(--color-foreground);
font-family: var(--vscode-font-family, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply text-foreground;
background-color: var(--color-background) !important;
}
}
button {
cursor: pointer;
}
+13
View File
@@ -0,0 +1,13 @@
import "./globals.css";
import { createApp } from "vue";
import App from "./App.vue";
// Fallback theme before MCP host provides context
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
document.documentElement.setAttribute(
"data-theme",
prefersDark ? "dark" : "light",
);
document.documentElement.style.colorScheme = prefersDark ? "dark" : "light";
createApp(App).mount("#app");
+116
View File
@@ -0,0 +1,116 @@
import { h } from "vue";
import type { Components } from "@json-render/vue";
import type { AppCatalog } from "./catalog";
export const components: Components<AppCatalog> = {
Stack: ({ props, children }) => {
const horizontal = props.direction === "horizontal";
return h(
"div",
{
class: "flex",
style: {
flexDirection: horizontal ? "row" : "column",
gap: props.gap ? `${props.gap}px` : undefined,
padding: props.padding ? `${props.padding}px` : undefined,
alignItems: props.align ?? (horizontal ? "center" : "stretch"),
},
},
children,
);
},
Card: ({ props, children }) =>
h(
"div",
{
class:
"rounded-xl border border-border bg-card text-card-foreground p-5 shadow-sm",
},
[
props.title &&
h(
"h2",
{
class: "text-base font-semibold text-card-foreground mb-1",
},
props.title,
),
props.subtitle &&
h(
"p",
{
class: "text-sm text-muted-foreground mb-3",
},
props.subtitle,
),
children,
],
),
Text: ({ props }) => {
const sizeClasses: Record<string, string> = {
sm: "text-xs",
md: "text-sm",
lg: "text-base",
xl: "text-2xl",
};
const weightClasses: Record<string, string> = {
normal: "font-normal",
medium: "font-medium",
bold: "font-bold",
};
return h(
"span",
{
class: [
sizeClasses[props.size ?? "md"] ?? "text-sm",
weightClasses[props.weight ?? "normal"] ?? "font-normal",
"text-foreground",
].join(" "),
style: props.color ? { color: props.color } : undefined,
},
String(props.content ?? ""),
);
},
Button: ({ props, emit }) => {
const variant = props.variant ?? "primary";
const variantClasses: Record<string, string> = {
primary: "bg-primary text-primary-foreground hover:bg-primary/90",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
danger:
"bg-destructive/10 text-destructive hover:bg-destructive/20 border border-destructive/20",
};
return h(
"button",
{
disabled: props.disabled,
onClick: () => emit("press"),
class: [
"inline-flex items-center justify-center rounded-lg px-4 py-2 text-sm font-medium transition-colors",
variantClasses[variant] ?? variantClasses.primary,
props.disabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer",
].join(" "),
},
props.label,
);
},
Badge: ({ props }) =>
h(
"span",
{
class:
"inline-flex items-center rounded-full px-3 py-1 text-xs font-medium bg-secondary text-secondary-foreground border border-border",
style: props.color
? {
backgroundColor: `${props.color}20`,
color: props.color,
borderColor: `${props.color}40`,
}
: undefined,
},
props.label,
),
};
+17
View File
@@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
"noEmit": true,
"strict": true,
"skipLibCheck": true,
"jsx": "preserve"
},
"include": ["src", "server.ts"]
}
+15
View File
@@ -0,0 +1,15 @@
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import { viteSingleFile } from "vite-plugin-singlefile";
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
plugins: [vue(), tailwindcss(), viteSingleFile()],
build: {
outDir: "dist",
emptyOutDir: false,
rollupOptions: {
input: "index.html",
},
},
});
-9
View File
@@ -1,14 +1,5 @@
# 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
+1 -1
View File
@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts";
import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-no-ai",
"version": "0.1.6",
"version": "0.1.5",
"type": "module",
"private": true,
"scripts": {
-8
View File
@@ -1,13 +1,5 @@
# 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
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-react-email",
"version": "0.1.2",
"version": "0.1.1",
"type": "module",
"private": true,
"scripts": {
-8
View File
@@ -1,13 +1,5 @@
# 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
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-react-native",
"version": "0.1.6",
"version": "0.1.5",
"private": true,
"main": "expo-router/entry",
"scripts": {
-8
View File
@@ -1,13 +1,5 @@
# 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
+1 -1
View File
@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts";
import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-react-pdf",
"version": "0.1.6",
"version": "0.1.5",
"type": "module",
"private": true,
"scripts": {
-8
View File
@@ -1,13 +1,5 @@
# 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
+1 -1
View File
@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts";
import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-remotion",
"version": "0.1.6",
"version": "0.1.5",
"type": "module",
"private": true,
"scripts": {
@@ -1,13 +1,5 @@
# 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
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "com.example.json-render-demo",
"version": "0.0.7",
"version": "0.0.6",
"description": "Test",
"private": true,
"license": "~~proprietary~~",
@@ -1,13 +1,5 @@
# 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
@@ -1,6 +1,6 @@
{
"name": "com.example.json-render-fullpage-demo",
"version": "0.0.7",
"version": "0.0.6",
"description": "Full-page Stripe App example (alpha)",
"private": true,
"license": "~~proprietary~~",
-8
View File
@@ -1,13 +1,5 @@
# 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
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "svelte-chat",
"private": true,
"version": "0.0.3",
"version": "0.0.2",
"type": "module",
"scripts": {
"dev": "vite dev",
+1 -1
View File
@@ -62,7 +62,7 @@ INPUT COMPONENTS:
- Button: Clickable button. Use on.press to trigger actions.
${explorerCatalog.prompt({
mode: "inline",
mode: "chat",
customRules: [
"NEVER use viewport height classes (min-h-screen, h-screen) — the UI renders inside a fixed-size container.",
"Prefer Grid with columns='2' or columns='3' for side-by-side layouts.",
-8
View File
@@ -1,13 +1,5 @@
# 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
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-svelte",
"version": "0.1.3",
"version": "0.1.2",
"private": true,
"type": "module",
"scripts": {
-10
View File
@@ -1,15 +1,5 @@
# 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
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "vite-renderers",
"version": "0.1.4",
"version": "0.1.3",
"private": true,
"type": "module",
"scripts": {
-8
View File
@@ -1,13 +1,5 @@
# 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
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-vue",
"version": "0.1.4",
"version": "0.1.3",
"private": true,
"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)",
-7
View File
@@ -1,12 +1,5 @@
# @json-render/codegen
## 0.12.1
### Patch Changes
- Updated dependencies [54a1ecf]
- @json-render/core@0.12.1
## 0.12.0
### Patch Changes
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@json-render/codegen",
"version": "0.12.1",
"version": "0.12.0",
"license": "Apache-2.0",
"description": "Utilities for generating code from json-render UI trees",
"keywords": [
@@ -15,7 +15,7 @@
"url": "git+https://github.com/vercel-labs/json-render.git",
"directory": "packages/codegen"
},
"homepage": "https://json-render.dev",
"homepage": "https://github.com/vercel-labs/json-render#readme",
"bugs": {
"url": "https://github.com/vercel-labs/json-render/issues"
},
-17
View File
@@ -1,22 +1,5 @@
# @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
+1 -1
View File
@@ -227,7 +227,7 @@ Schema options:
| `ActionBinding` | Action binding with `action`, `params`, `confirm`, `preventDefault`, etc. |
| `BuiltInAction` | Built-in action definition with `name` and `description` |
### Inline Mode (Mixed Streams)
### Chat Mode (Mixed Streams)
| Export | Purpose |
|--------|---------|
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@json-render/core",
"version": "0.12.1",
"version": "0.12.0",
"license": "Apache-2.0",
"description": "JSON becomes real things. Define your catalog, register your components, let AI generate.",
"keywords": [
@@ -19,7 +19,7 @@
"url": "git+https://github.com/vercel-labs/json-render.git",
"directory": "packages/core"
},
"homepage": "https://json-render.dev",
"homepage": "https://github.com/vercel-labs/json-render#readme",
"bugs": {
"url": "https://github.com/vercel-labs/json-render/issues"
},
+3 -35
View File
@@ -257,7 +257,7 @@ describe("catalog.prompt", () => {
expect(prompt).toContain("Keep UIs simple");
});
it("generates inline mode prompt when mode is inline", () => {
it("generates chat mode prompt when mode is chat", () => {
const catalog = defineCatalog(testSchema, {
components: {
Text: {
@@ -268,13 +268,13 @@ describe("catalog.prompt", () => {
},
actions: {},
});
const prompt = catalog.prompt({ mode: "inline" });
const prompt = catalog.prompt({ mode: "chat" });
expect(prompt).toContain("```spec");
expect(prompt).toContain("conversationally");
expect(prompt).toContain("text + JSONL");
});
it("generates standalone mode prompt by default", () => {
it("generates generate mode prompt by default", () => {
const catalog = defineCatalog(testSchema, {
components: {
Text: {
@@ -290,38 +290,6 @@ describe("catalog.prompt", () => {
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", () => {
const catalog = defineCatalog(testSchema, {
components: {
+6 -22
View File
@@ -112,14 +112,11 @@ export interface PromptOptions {
/**
* Output mode for the generated prompt.
*
* - `"standalone"` (default): The LLM should output only JSONL patches (no prose).
* - `"inline"`: The LLM should respond conversationally first, then output JSONL patches.
* - `"generate"` (default): The LLM should output only JSONL patches (no prose).
* - `"chat"`: The LLM should respond conversationally first, then output JSONL patches.
* Includes rules about interleaving text with JSONL and not wrapping in code fences.
*
* @deprecated `"generate"` — use `"standalone"` instead.
* @deprecated `"chat"` — use `"inline"` instead.
*/
mode?: "standalone" | "inline" | "generate" | "chat";
mode?: "generate" | "chat";
}
/**
@@ -566,28 +563,15 @@ function generatePrompt<TDef extends SchemaDefinition, TCatalog>(
const {
system = "You are a UI generator that outputs JSON.",
customRules = [],
mode: rawMode = "standalone",
mode = "generate",
} = 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[] = [];
lines.push(system);
lines.push("");
// Output format section - explain JSONL streaming patch format
if (mode === "inline") {
if (mode === "chat") {
lines.push("OUTPUT FORMAT (text + JSONL, RFC 6902 JSON Patch):");
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.",
@@ -1030,7 +1014,7 @@ Note: state patches appear right after the elements that use them, so the UI fil
// Rules
lines.push("RULES:");
const baseRules =
mode === "inline"
mode === "chat"
? [
"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",
-7
View File
@@ -1,12 +1,5 @@
# @json-render/image
## 0.12.1
### Patch Changes
- Updated dependencies [54a1ecf]
- @json-render/core@0.12.1
## 0.12.0
### Patch Changes
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@json-render/image",
"version": "0.12.1",
"version": "0.12.0",
"license": "Apache-2.0",
"description": "Image renderer for @json-render/core. JSON becomes SVG and PNG images via Satori.",
"keywords": [
@@ -20,7 +20,7 @@
"url": "git+https://github.com/vercel-labs/json-render.git",
"directory": "packages/image"
},
"homepage": "https://json-render.dev",
"homepage": "https://github.com/vercel-labs/json-render#readme",
"bugs": {
"url": "https://github.com/vercel-labs/json-render/issues"
},
-7
View File
@@ -1,12 +1,5 @@
# @json-render/jotai
## 0.12.1
### Patch Changes
- Updated dependencies [54a1ecf]
- @json-render/core@0.12.1
## 0.12.0
### Patch Changes
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@json-render/jotai",
"version": "0.12.1",
"version": "0.12.0",
"license": "Apache-2.0",
"description": "Jotai adapter for json-render StateStore",
"keywords": [
@@ -14,7 +14,7 @@
"url": "git+https://github.com/vercel-labs/json-render.git",
"directory": "packages/jotai"
},
"homepage": "https://json-render.dev",
"homepage": "https://github.com/vercel-labs/json-render#readme",
"bugs": {
"url": "https://github.com/vercel-labs/json-render/issues"
},
-20
View File
@@ -1,25 +1,5 @@
# @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
+58 -6
View File
@@ -46,10 +46,12 @@ await server.connect(new StdioServerTransport());
### 3. Build the UI (iframe)
Create a React app that uses `useJsonRenderApp` from `@json-render/mcp/app`:
Create an app that connects to the MCP host and renders specs. Framework-specific adapters are available for React, Vue, and Svelte.
#### React
```tsx
import { useJsonRenderApp } from "@json-render/mcp/app";
import { useJsonRenderApp } from "@json-render/mcp/app/react";
import { JSONUIProvider, Renderer } from "@json-render/react";
function McpAppView({ registry }) {
@@ -66,6 +68,44 @@ function McpAppView({ registry }) {
}
```
#### Vue
```vue
<script setup>
import { useJsonRenderApp } from "@json-render/mcp/app/vue";
import { Renderer } from "@json-render/vue";
const { spec, loading, error } = useJsonRenderApp();
</script>
<template>
<div v-if="error">Error: {{ error.message }}</div>
<div v-else-if="!spec">Waiting for spec...</div>
<Renderer v-else :spec="spec" :registry="registry" :loading="loading" />
</template>
```
#### Svelte
```svelte
<script>
import { createJsonRenderApp } from "@json-render/mcp/app/svelte";
import { Renderer } from "@json-render/svelte";
import { onDestroy } from "svelte";
const { spec, loading, error, destroy } = createJsonRenderApp();
onDestroy(destroy);
</script>
{#if $error}
<div>Error: {$error.message}</div>
{:else if !$spec}
<div>Waiting for spec...</div>
{:else}
<Renderer spec={$spec} {registry} loading={$loading} />
{/if}
```
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
@@ -107,17 +147,29 @@ Register a json-render tool on an existing `McpServer`.
Register a json-render UI resource on an existing `McpServer`.
### Client Side (`@json-render/mcp/app`)
### Client Side
#### `useJsonRenderApp(options?)`
#### `buildAppHtml(options)` — `@json-render/mcp/app`
Generate a self-contained HTML string from bundled JS/CSS for use as a UI resource.
#### `useJsonRenderApp(options?)` — `@json-render/mcp/app/react`
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)`
#### `useJsonRenderApp(options?)` — `@json-render/mcp/app/vue`
Generate a self-contained HTML string from bundled JS/CSS for use as a UI resource.
Vue composable with the same API shape. Values are Vue refs/computed properties.
Returns `{ spec, loading, connected, connecting, error, app, callServerTool }`.
#### `createJsonRenderApp(options?)` — `@json-render/mcp/app/svelte`
Creates a json-render MCP App client using Svelte stores. Values are readable stores.
Returns `{ spec, loading, connected, connecting, error, app, callServerTool, destroy }`.
## Client Support
+28 -8
View File
@@ -1,6 +1,6 @@
{
"name": "@json-render/mcp",
"version": "0.12.1",
"version": "0.12.0",
"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": [
@@ -22,7 +22,7 @@
"url": "git+https://github.com/vercel-labs/json-render.git",
"directory": "packages/mcp"
},
"homepage": "https://json-render.dev",
"homepage": "https://github.com/vercel-labs/json-render#readme",
"bugs": {
"url": "https://github.com/vercel-labs/json-render/issues"
},
@@ -43,10 +43,20 @@
"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"
"./app/react": {
"types": "./dist/app/react.d.ts",
"import": "./dist/app/react.mjs",
"require": "./dist/app/react.js"
},
"./app/vue": {
"types": "./dist/app/vue.d.ts",
"import": "./dist/app/vue.mjs",
"require": "./dist/app/vue.js"
},
"./app/svelte": {
"types": "./dist/app/svelte.d.ts",
"import": "./dist/app/svelte.mjs",
"require": "./dist/app/svelte.js"
}
},
"files": [
@@ -65,12 +75,16 @@
"devDependencies": {
"@internal/typescript-config": "workspace:*",
"@types/react": "19.2.3",
"svelte": "^5.0.0",
"tsup": "^8.0.2",
"typescript": "^5.4.5"
"typescript": "^5.4.5",
"vue": "^3.5.0"
},
"peerDependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
"react-dom": "^19.0.0",
"svelte": "^5.0.0",
"vue": "^3.5.0"
},
"peerDependenciesMeta": {
"react": {
@@ -78,6 +92,12 @@
},
"react-dom": {
"optional": true
},
"svelte": {
"optional": true
},
"vue": {
"optional": true
}
}
}

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