Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions .cursor/mcp.json
Original file line number Diff line number Diff line change
@@ -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 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
node_modules
.pnp
.pnp.js
.pnpm-store/

# Local env files
.env*
Expand Down
2 changes: 1 addition & 1 deletion .vscode/mcp.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand Down
77 changes: 68 additions & 9 deletions apps/web/app/(main)/docs/api/mcp/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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. Framework-specific adapters are available at:

These exports run inside the sandboxed iframe rendered by the MCP host.
- `@json-render/mcp/app/react` -- React hook
- `@json-render/mcp/app/vue` -- Vue composable
- `@json-render/mcp/app/svelte` -- Svelte stores

### useJsonRenderApp
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 }) {
Expand All @@ -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>
Expand Down Expand Up @@ -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.

### buildAppHtml (`@json-render/mcp/app`)

Generate a self-contained HTML page from bundled JavaScript and CSS.
Generate a self-contained HTML page from bundled JavaScript and CSS. Framework-agnostic.

```typescript
import { buildAppHtml } from "@json-render/mcp/app";
Expand Down
2 changes: 1 addition & 1 deletion apps/web/lib/docs-navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
],
Expand Down
2 changes: 1 addition & 1 deletion examples/chat/next-env.d.ts
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 1 addition & 1 deletion examples/dashboard/next-env.d.ts
Original file line number Diff line number Diff line change
@@ -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.
File renamed without changes.
4 changes: 2 additions & 2 deletions examples/mcp/README.md → examples/mcp-react/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
}
}
}
Expand All @@ -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"]
}
}
}
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "example-mcp",
"name": "example-mcp-react",
"version": "0.1.1",
"type": "module",
"private": true,
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -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";

Expand Down
File renamed without changes.
File renamed without changes.
13 changes: 13 additions & 0 deletions examples/mcp-svelte/index.html
Original file line number Diff line number Diff line change
@@ -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 changes: 28 additions & 0 deletions examples/mcp-svelte/package.json
Original file line number Diff line number Diff line change
@@ -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 changes: 34 additions & 0 deletions examples/mcp-svelte/server.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 26 additions & 0 deletions examples/mcp-svelte/src/App.svelte
Original file line number Diff line number Diff line change
@@ -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 changes: 54 additions & 0 deletions examples/mcp-svelte/src/catalog.ts
Original file line number Diff line number Diff line change
@@ -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: {},
});
Loading