mirror of
https://github.com/vercel-labs/json-render.git
synced 2026-10-19 06:09:35 +08:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1cc87310c9 | ||
|
|
5b929dffa6 |
@@ -17,7 +17,8 @@
|
|||||||
"@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"
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"linked": [],
|
"linked": [],
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"json-render": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["tsx", "examples/mcp/server.ts", "--stdio"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+9
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"servers": {
|
||||||
|
"json-render": {
|
||||||
|
"type": "stdio",
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["tsx", "examples/mcp/server.ts", "--stdio"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -126,6 +126,7 @@ function Dashboard({ spec }) {
|
|||||||
| `@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
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -16,7 +16,7 @@ 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/react-email, @json-render/react-pdf, @json-render/image, @json-render/remotion, @json-render/codegen, @json-render/mcp
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
|
|||||||
@@ -95,6 +95,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 +133,7 @@ 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" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ 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",
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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.0",
|
||||||
|
"type": "module",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"build": "vite build",
|
||||||
|
"start": "tsx server.ts",
|
||||||
|
"start:stdio": "tsx server.ts --stdio",
|
||||||
|
"dev": "tsx watch server.ts"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@json-render/core": "workspace:*",
|
||||||
|
"@json-render/mcp": "workspace:*",
|
||||||
|
"@json-render/react": "workspace:*",
|
||||||
|
"@json-render/shadcn": "workspace:*",
|
||||||
|
"@modelcontextprotocol/ext-apps": "^1.2.0",
|
||||||
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
||||||
|
"react": "19.2.3",
|
||||||
|
"react-dom": "19.2.3",
|
||||||
|
"zod": "^4.3.6"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "19.2.3",
|
||||||
|
"@types/react-dom": "19.2.3",
|
||||||
|
"@tailwindcss/vite": "^4.2.1",
|
||||||
|
"@vitejs/plugin-react": "^5.1.4",
|
||||||
|
"tailwindcss": "^4.2.1",
|
||||||
|
"tsx": "^4.21.0",
|
||||||
|
"typescript": "^5.7.2",
|
||||||
|
"vite": "^6.3.5",
|
||||||
|
"vite-plugin-singlefile": "^2.3.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,15 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
import { viteSingleFile } from "vite-plugin-singlefile";
|
||||||
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react(), tailwindcss(), viteSingleFile()],
|
||||||
|
build: {
|
||||||
|
outDir: "dist",
|
||||||
|
emptyOutDir: false,
|
||||||
|
rollupOptions: {
|
||||||
|
input: "index.html",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -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,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,78 @@
|
|||||||
|
{
|
||||||
|
"name": "@json-render/mcp",
|
||||||
|
"version": "0.11.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": [
|
||||||
|
"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://github.com/vercel-labs/json-render#readme",
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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,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"],
|
||||||
|
format: ["cjs", "esm"],
|
||||||
|
dts: true,
|
||||||
|
sourcemap: true,
|
||||||
|
clean: true,
|
||||||
|
external: [
|
||||||
|
"react",
|
||||||
|
"react-dom",
|
||||||
|
"@json-render/core",
|
||||||
|
"@json-render/react",
|
||||||
|
"@modelcontextprotocol/sdk",
|
||||||
|
"@modelcontextprotocol/ext-apps",
|
||||||
|
],
|
||||||
|
});
|
||||||
@@ -65,6 +65,7 @@
|
|||||||
"typescript": "^5.4.5"
|
"typescript": "^5.4.5"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"svelte": "^5.0.0"
|
"svelte": "^5.0.0",
|
||||||
|
"zod": "^4.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
* Props for JSONUIProvider
|
* Props for JSONUIProvider
|
||||||
*/
|
*/
|
||||||
export interface JSONUIProviderProps {
|
export interface JSONUIProviderProps {
|
||||||
/** Component registry */
|
/** Component registry (passed through for convenience; not used internally) */
|
||||||
registry: ComponentRegistry;
|
registry?: ComponentRegistry;
|
||||||
/**
|
/**
|
||||||
* External store (controlled mode). When provided, `initialState` and
|
* External store (controlled mode). When provided, `initialState` and
|
||||||
* `onStateChange` are ignored.
|
* `onStateChange` are ignored.
|
||||||
|
|||||||
@@ -211,7 +211,7 @@ export interface CreateRendererProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component map type — maps component names to Vue components
|
* Component map type — maps component names to Svelte components
|
||||||
*/
|
*/
|
||||||
export type ComponentMap<
|
export type ComponentMap<
|
||||||
TComponents extends Record<string, { props: unknown }>,
|
TComponents extends Record<string, { props: unknown }>,
|
||||||
|
|||||||
Generated
+250
-4
@@ -520,6 +520,64 @@ importers:
|
|||||||
specifier: ^5.7.2
|
specifier: ^5.7.2
|
||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
|
|
||||||
|
examples/mcp:
|
||||||
|
dependencies:
|
||||||
|
'@json-render/core':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../packages/core
|
||||||
|
'@json-render/mcp':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../packages/mcp
|
||||||
|
'@json-render/react':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../packages/react
|
||||||
|
'@json-render/shadcn':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../packages/shadcn
|
||||||
|
'@modelcontextprotocol/ext-apps':
|
||||||
|
specifier: ^1.2.0
|
||||||
|
version: 1.2.0(@modelcontextprotocol/sdk@1.27.1(zod@4.3.6))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.6)
|
||||||
|
'@modelcontextprotocol/sdk':
|
||||||
|
specifier: ^1.27.1
|
||||||
|
version: 1.27.1(zod@4.3.6)
|
||||||
|
react:
|
||||||
|
specifier: 19.2.3
|
||||||
|
version: 19.2.3
|
||||||
|
react-dom:
|
||||||
|
specifier: 19.2.3
|
||||||
|
version: 19.2.3(react@19.2.3)
|
||||||
|
zod:
|
||||||
|
specifier: ^4.3.6
|
||||||
|
version: 4.3.6
|
||||||
|
devDependencies:
|
||||||
|
'@tailwindcss/vite':
|
||||||
|
specifier: ^4.2.1
|
||||||
|
version: 4.2.1(vite@6.4.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))
|
||||||
|
'@types/react':
|
||||||
|
specifier: 19.2.3
|
||||||
|
version: 19.2.3
|
||||||
|
'@types/react-dom':
|
||||||
|
specifier: 19.2.3
|
||||||
|
version: 19.2.3(@types/react@19.2.3)
|
||||||
|
'@vitejs/plugin-react':
|
||||||
|
specifier: ^5.1.4
|
||||||
|
version: 5.1.4(vite@6.4.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))
|
||||||
|
tailwindcss:
|
||||||
|
specifier: ^4.2.1
|
||||||
|
version: 4.2.1
|
||||||
|
tsx:
|
||||||
|
specifier: ^4.21.0
|
||||||
|
version: 4.21.0
|
||||||
|
typescript:
|
||||||
|
specifier: ^5.7.2
|
||||||
|
version: 5.9.3
|
||||||
|
vite:
|
||||||
|
specifier: ^6.3.5
|
||||||
|
version: 6.4.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||||
|
vite-plugin-singlefile:
|
||||||
|
specifier: ^2.3.0
|
||||||
|
version: 2.3.0(rollup@4.55.1)(vite@6.4.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))
|
||||||
|
|
||||||
examples/no-ai:
|
examples/no-ai:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@json-render/core':
|
'@json-render/core':
|
||||||
@@ -621,7 +679,7 @@ importers:
|
|||||||
version: 0.575.0(react@19.2.4)
|
version: 0.575.0(react@19.2.4)
|
||||||
next:
|
next:
|
||||||
specifier: 16.1.6
|
specifier: 16.1.6
|
||||||
version: 16.1.6(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
version: 16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
radix-ui:
|
radix-ui:
|
||||||
specifier: ^1.4.3
|
specifier: ^1.4.3
|
||||||
version: 1.4.3(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
version: 1.4.3(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
@@ -755,7 +813,7 @@ importers:
|
|||||||
version: 0.575.0(react@19.2.4)
|
version: 0.575.0(react@19.2.4)
|
||||||
next:
|
next:
|
||||||
specifier: 16.1.6
|
specifier: 16.1.6
|
||||||
version: 16.1.6(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
version: 16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
radix-ui:
|
radix-ui:
|
||||||
specifier: ^1.4.3
|
specifier: ^1.4.3
|
||||||
version: 1.4.3(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
version: 1.4.3(@types/react-dom@19.2.3(@types/react@19.2.3))(@types/react@19.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
@@ -1259,6 +1317,37 @@ importers:
|
|||||||
specifier: ^5.4.5
|
specifier: ^5.4.5
|
||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
|
|
||||||
|
packages/mcp:
|
||||||
|
dependencies:
|
||||||
|
'@json-render/core':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../core
|
||||||
|
'@modelcontextprotocol/ext-apps':
|
||||||
|
specifier: ^1.2.0
|
||||||
|
version: 1.2.0(@modelcontextprotocol/sdk@1.27.1(zod@4.3.6))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@4.3.6)
|
||||||
|
'@modelcontextprotocol/sdk':
|
||||||
|
specifier: ^1.27.1
|
||||||
|
version: 1.27.1(zod@4.3.6)
|
||||||
|
react:
|
||||||
|
specifier: ^19.0.0
|
||||||
|
version: 19.2.4
|
||||||
|
react-dom:
|
||||||
|
specifier: ^19.0.0
|
||||||
|
version: 19.2.4(react@19.2.4)
|
||||||
|
devDependencies:
|
||||||
|
'@internal/typescript-config':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../typescript-config
|
||||||
|
'@types/react':
|
||||||
|
specifier: 19.2.3
|
||||||
|
version: 19.2.3
|
||||||
|
tsup:
|
||||||
|
specifier: ^8.0.2
|
||||||
|
version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2)
|
||||||
|
typescript:
|
||||||
|
specifier: ^5.4.5
|
||||||
|
version: 5.9.3
|
||||||
|
|
||||||
packages/react:
|
packages/react:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@json-render/core':
|
'@json-render/core':
|
||||||
@@ -1515,6 +1604,9 @@ importers:
|
|||||||
'@json-render/core':
|
'@json-render/core':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../core
|
version: link:../core
|
||||||
|
zod:
|
||||||
|
specifier: ^4.0.0
|
||||||
|
version: 4.3.6
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@internal/typescript-config':
|
'@internal/typescript-config':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
@@ -3627,6 +3719,19 @@ packages:
|
|||||||
'@mixmark-io/domino@2.2.0':
|
'@mixmark-io/domino@2.2.0':
|
||||||
resolution: {integrity: sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==}
|
resolution: {integrity: sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==}
|
||||||
|
|
||||||
|
'@modelcontextprotocol/ext-apps@1.2.0':
|
||||||
|
resolution: {integrity: sha512-ijUQJX/FmNq8PWgOLzph/BAfy84sUZxoIRuHzr+F37wYtWjhdl8pliBJybapYolppY+XJ8oqjFZmTOuMqxwbWQ==}
|
||||||
|
peerDependencies:
|
||||||
|
'@modelcontextprotocol/sdk': ^1.24.0
|
||||||
|
react: ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||||
|
react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||||
|
zod: ^3.25.0 || ^4.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
react:
|
||||||
|
optional: true
|
||||||
|
react-dom:
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@modelcontextprotocol/sdk@1.26.0':
|
'@modelcontextprotocol/sdk@1.26.0':
|
||||||
resolution: {integrity: sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==}
|
resolution: {integrity: sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -3637,6 +3742,16 @@ packages:
|
|||||||
'@cfworker/json-schema':
|
'@cfworker/json-schema':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@modelcontextprotocol/sdk@1.27.1':
|
||||||
|
resolution: {integrity: sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
peerDependencies:
|
||||||
|
'@cfworker/json-schema': ^4.1.1
|
||||||
|
zod: ^3.25 || ^4.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@cfworker/json-schema':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@mongodb-js/zstd@7.0.0':
|
'@mongodb-js/zstd@7.0.0':
|
||||||
resolution: {integrity: sha512-mQ2s0pYYiav+tzCDR05Zptem8Ey2v8s11lri5RKGhTtL4COVCvVCk5vtyRYNT+9L8qSfyOqqefF9UtnW8mC5jA==}
|
resolution: {integrity: sha512-mQ2s0pYYiav+tzCDR05Zptem8Ey2v8s11lri5RKGhTtL4COVCvVCk5vtyRYNT+9L8qSfyOqqefF9UtnW8mC5jA==}
|
||||||
engines: {node: '>= 20.19.0'}
|
engines: {node: '>= 20.19.0'}
|
||||||
@@ -12103,6 +12218,53 @@ packages:
|
|||||||
resolution: {integrity: sha512-t20zYkrSf868+j/p31cRIGN28Phrjm3nRSLR2fyc2tiWi4cZGVdv68yNlwnIINTkMTmPoMiSlc0OadaO7DXZaQ==}
|
resolution: {integrity: sha512-t20zYkrSf868+j/p31cRIGN28Phrjm3nRSLR2fyc2tiWi4cZGVdv68yNlwnIINTkMTmPoMiSlc0OadaO7DXZaQ==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
|
|
||||||
|
vite-plugin-singlefile@2.3.0:
|
||||||
|
resolution: {integrity: sha512-DAcHzYypM0CasNLSz/WG0VdKOCxGHErfrjOoyIPiNxTPTGmO6rRD/te93n1YL/s+miXq66ipF1brMBikf99c6A==}
|
||||||
|
engines: {node: '>18.0.0'}
|
||||||
|
peerDependencies:
|
||||||
|
rollup: ^4.44.1
|
||||||
|
vite: ^5.4.11 || ^6.0.0 || ^7.0.0
|
||||||
|
|
||||||
|
vite@6.4.1:
|
||||||
|
resolution: {integrity: sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==}
|
||||||
|
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
|
||||||
|
hasBin: true
|
||||||
|
peerDependencies:
|
||||||
|
'@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0
|
||||||
|
jiti: '>=1.21.0'
|
||||||
|
less: '*'
|
||||||
|
lightningcss: ^1.21.0
|
||||||
|
sass: '*'
|
||||||
|
sass-embedded: '*'
|
||||||
|
stylus: '*'
|
||||||
|
sugarss: '*'
|
||||||
|
terser: ^5.16.0
|
||||||
|
tsx: ^4.8.1
|
||||||
|
yaml: ^2.4.2
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/node':
|
||||||
|
optional: true
|
||||||
|
jiti:
|
||||||
|
optional: true
|
||||||
|
less:
|
||||||
|
optional: true
|
||||||
|
lightningcss:
|
||||||
|
optional: true
|
||||||
|
sass:
|
||||||
|
optional: true
|
||||||
|
sass-embedded:
|
||||||
|
optional: true
|
||||||
|
stylus:
|
||||||
|
optional: true
|
||||||
|
sugarss:
|
||||||
|
optional: true
|
||||||
|
terser:
|
||||||
|
optional: true
|
||||||
|
tsx:
|
||||||
|
optional: true
|
||||||
|
yaml:
|
||||||
|
optional: true
|
||||||
|
|
||||||
vite@7.3.1:
|
vite@7.3.1:
|
||||||
resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==}
|
resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
@@ -14857,6 +15019,22 @@ snapshots:
|
|||||||
|
|
||||||
'@mixmark-io/domino@2.2.0': {}
|
'@mixmark-io/domino@2.2.0': {}
|
||||||
|
|
||||||
|
'@modelcontextprotocol/ext-apps@1.2.0(@modelcontextprotocol/sdk@1.27.1(zod@4.3.6))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.6)':
|
||||||
|
dependencies:
|
||||||
|
'@modelcontextprotocol/sdk': 1.27.1(zod@4.3.6)
|
||||||
|
zod: 4.3.6
|
||||||
|
optionalDependencies:
|
||||||
|
react: 19.2.3
|
||||||
|
react-dom: 19.2.3(react@19.2.3)
|
||||||
|
|
||||||
|
'@modelcontextprotocol/ext-apps@1.2.0(@modelcontextprotocol/sdk@1.27.1(zod@4.3.6))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@4.3.6)':
|
||||||
|
dependencies:
|
||||||
|
'@modelcontextprotocol/sdk': 1.27.1(zod@4.3.6)
|
||||||
|
zod: 4.3.6
|
||||||
|
optionalDependencies:
|
||||||
|
react: 19.2.4
|
||||||
|
react-dom: 19.2.4(react@19.2.4)
|
||||||
|
|
||||||
'@modelcontextprotocol/sdk@1.26.0(zod@3.25.76)':
|
'@modelcontextprotocol/sdk@1.26.0(zod@3.25.76)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@hono/node-server': 1.19.9(hono@4.12.0)
|
'@hono/node-server': 1.19.9(hono@4.12.0)
|
||||||
@@ -14879,6 +15057,28 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
'@modelcontextprotocol/sdk@1.27.1(zod@4.3.6)':
|
||||||
|
dependencies:
|
||||||
|
'@hono/node-server': 1.19.9(hono@4.12.0)
|
||||||
|
ajv: 8.17.1
|
||||||
|
ajv-formats: 3.0.1(ajv@8.17.1)
|
||||||
|
content-type: 1.0.5
|
||||||
|
cors: 2.8.6
|
||||||
|
cross-spawn: 7.0.6
|
||||||
|
eventsource: 3.0.7
|
||||||
|
eventsource-parser: 3.0.6
|
||||||
|
express: 5.2.1
|
||||||
|
express-rate-limit: 8.2.1(express@5.2.1)
|
||||||
|
hono: 4.12.0
|
||||||
|
jose: 6.1.3
|
||||||
|
json-schema-typed: 8.0.2
|
||||||
|
pkce-challenge: 5.0.1
|
||||||
|
raw-body: 3.0.2
|
||||||
|
zod: 4.3.6
|
||||||
|
zod-to-json-schema: 3.25.1(zod@4.3.6)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
'@mongodb-js/zstd@7.0.0':
|
'@mongodb-js/zstd@7.0.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
node-addon-api: 8.5.0
|
node-addon-api: 8.5.0
|
||||||
@@ -17741,7 +17941,7 @@ snapshots:
|
|||||||
'@stripe/ui-extension-tools@0.0.1(@babel/core@7.29.0)(babel-jest@27.5.1(@babel/core@7.29.0))':
|
'@stripe/ui-extension-tools@0.0.1(@babel/core@7.29.0)(babel-jest@27.5.1(@babel/core@7.29.0))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/jest': 28.1.8
|
'@types/jest': 28.1.8
|
||||||
'@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1)(typescript@4.9.5)
|
'@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0(eslint@9.39.2(jiti@2.6.1))(typescript@4.9.5))(eslint@8.57.1)(typescript@4.9.5)
|
||||||
'@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@4.9.5)
|
'@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@4.9.5)
|
||||||
eslint: 8.57.1
|
eslint: 8.57.1
|
||||||
eslint-plugin-react: 7.37.5(eslint@8.57.1)
|
eslint-plugin-react: 7.37.5(eslint@8.57.1)
|
||||||
@@ -17974,6 +18174,13 @@ snapshots:
|
|||||||
postcss: 8.5.6
|
postcss: 8.5.6
|
||||||
tailwindcss: 4.1.18
|
tailwindcss: 4.1.18
|
||||||
|
|
||||||
|
'@tailwindcss/vite@4.2.1(vite@6.4.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))':
|
||||||
|
dependencies:
|
||||||
|
'@tailwindcss/node': 4.2.1
|
||||||
|
'@tailwindcss/oxide': 4.2.1
|
||||||
|
tailwindcss: 4.2.1
|
||||||
|
vite: 6.4.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||||
|
|
||||||
'@tailwindcss/vite@4.2.1(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))':
|
'@tailwindcss/vite@4.2.1(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tailwindcss/node': 4.2.1
|
'@tailwindcss/node': 4.2.1
|
||||||
@@ -18260,7 +18467,7 @@ snapshots:
|
|||||||
'@types/node': 22.19.6
|
'@types/node': 22.19.6
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1)(typescript@4.9.5)':
|
'@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@9.39.2(jiti@2.6.1))(typescript@4.9.5))(eslint@8.57.1)(typescript@4.9.5)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint-community/regexpp': 4.12.2
|
'@eslint-community/regexpp': 4.12.2
|
||||||
'@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@4.9.5)
|
'@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@4.9.5)
|
||||||
@@ -18494,6 +18701,18 @@ snapshots:
|
|||||||
'@visual-json/core': 0.1.1
|
'@visual-json/core': 0.1.1
|
||||||
react: 19.2.3
|
react: 19.2.3
|
||||||
|
|
||||||
|
'@vitejs/plugin-react@5.1.4(vite@6.4.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))':
|
||||||
|
dependencies:
|
||||||
|
'@babel/core': 7.29.0
|
||||||
|
'@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0)
|
||||||
|
'@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0)
|
||||||
|
'@rolldown/pluginutils': 1.0.0-rc.3
|
||||||
|
'@types/babel__core': 7.20.5
|
||||||
|
react-refresh: 0.18.0
|
||||||
|
vite: 6.4.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
'@vitejs/plugin-react@5.1.4(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))':
|
'@vitejs/plugin-react@5.1.4(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.29.0
|
'@babel/core': 7.29.0
|
||||||
@@ -26445,6 +26664,29 @@ snapshots:
|
|||||||
string_decoder: 1.3.0
|
string_decoder: 1.3.0
|
||||||
util-deprecate: 1.0.2
|
util-deprecate: 1.0.2
|
||||||
|
|
||||||
|
vite-plugin-singlefile@2.3.0(rollup@4.55.1)(vite@6.4.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)):
|
||||||
|
dependencies:
|
||||||
|
micromatch: 4.0.8
|
||||||
|
rollup: 4.55.1
|
||||||
|
vite: 6.4.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||||
|
|
||||||
|
vite@6.4.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2):
|
||||||
|
dependencies:
|
||||||
|
esbuild: 0.25.12
|
||||||
|
fdir: 6.5.0(picomatch@4.0.3)
|
||||||
|
picomatch: 4.0.3
|
||||||
|
postcss: 8.5.6
|
||||||
|
rollup: 4.55.1
|
||||||
|
tinyglobby: 0.2.15
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/node': 22.19.6
|
||||||
|
fsevents: 2.3.3
|
||||||
|
jiti: 2.6.1
|
||||||
|
lightningcss: 1.31.1
|
||||||
|
terser: 5.46.0
|
||||||
|
tsx: 4.21.0
|
||||||
|
yaml: 2.8.2
|
||||||
|
|
||||||
vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2):
|
vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2):
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.27.2
|
esbuild: 0.27.2
|
||||||
@@ -26900,6 +27142,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
zod: 3.25.76
|
zod: 3.25.76
|
||||||
|
|
||||||
|
zod-to-json-schema@3.25.1(zod@4.3.6):
|
||||||
|
dependencies:
|
||||||
|
zod: 4.3.6
|
||||||
|
|
||||||
zod@3.22.3: {}
|
zod@3.22.3: {}
|
||||||
|
|
||||||
zod@3.25.76: {}
|
zod@3.25.76: {}
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
---
|
||||||
|
name: json-render-mcp
|
||||||
|
description: MCP Apps integration for json-render. Use when building MCP servers that render interactive UIs in Claude, ChatGPT, Cursor, or VS Code, or when integrating json-render with the Model Context Protocol.
|
||||||
|
---
|
||||||
|
|
||||||
|
# @json-render/mcp
|
||||||
|
|
||||||
|
MCP Apps integration that serves json-render UIs as interactive MCP Apps inside Claude, ChatGPT, Cursor, VS Code, and other MCP-capable clients.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Server (Node.js)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createMcpApp } from "@json-render/mcp";
|
||||||
|
import { defineCatalog } from "@json-render/core";
|
||||||
|
import { schema } from "@json-render/react/schema";
|
||||||
|
import { shadcnComponentDefinitions } from "@json-render/shadcn/catalog";
|
||||||
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||||
|
import fs from "node:fs";
|
||||||
|
|
||||||
|
const catalog = defineCatalog(schema, {
|
||||||
|
components: { ...shadcnComponentDefinitions },
|
||||||
|
actions: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const server = createMcpApp({
|
||||||
|
name: "My App",
|
||||||
|
version: "1.0.0",
|
||||||
|
catalog,
|
||||||
|
html: fs.readFileSync("dist/index.html", "utf-8"),
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.connect(new StdioServerTransport());
|
||||||
|
```
|
||||||
|
|
||||||
|
### Client (React, inside iframe)
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { useJsonRenderApp } from "@json-render/mcp/app";
|
||||||
|
import { JSONUIProvider, Renderer } from "@json-render/react";
|
||||||
|
|
||||||
|
function McpAppView({ registry }) {
|
||||||
|
const { spec, loading, error } = useJsonRenderApp();
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
1. `createMcpApp()` creates an `McpServer` that registers a `render-ui` tool and a `ui://` HTML resource
|
||||||
|
2. The tool description includes the catalog prompt so the LLM knows how to generate valid specs
|
||||||
|
3. The HTML resource is a Vite-bundled single-file React app with json-render renderers
|
||||||
|
4. Inside the iframe, `useJsonRenderApp()` connects to the host via `postMessage` and renders specs
|
||||||
|
|
||||||
|
## Server API
|
||||||
|
|
||||||
|
- `createMcpApp(options)` - main entry, creates a full MCP server
|
||||||
|
- `registerJsonRenderTool(server, options)` - register a json-render tool on an existing server
|
||||||
|
- `registerJsonRenderResource(server, options)` - register the UI resource
|
||||||
|
|
||||||
|
## Client API (`@json-render/mcp/app`)
|
||||||
|
|
||||||
|
- `useJsonRenderApp(options?)` - React hook, returns `{ spec, loading, connected, error, callServerTool }`
|
||||||
|
- `buildAppHtml(options)` - generate HTML from bundled JS/CSS
|
||||||
|
|
||||||
|
## Building the iframe HTML
|
||||||
|
|
||||||
|
Bundle the React app into a single self-contained HTML file using Vite + `vite-plugin-singlefile`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// vite.config.ts
|
||||||
|
import { defineConfig } from "vite";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
import { viteSingleFile } from "vite-plugin-singlefile";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react(), viteSingleFile()],
|
||||||
|
build: { outDir: "dist" },
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Client Configuration
|
||||||
|
|
||||||
|
### Cursor (`.cursor/mcp.json`)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"my-app": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["tsx", "server.ts", "--stdio"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Claude Desktop
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"my-app": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["tsx", "/path/to/server.ts", "--stdio"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Server
|
||||||
|
npm install @json-render/mcp @json-render/core @modelcontextprotocol/sdk
|
||||||
|
|
||||||
|
# Client (iframe)
|
||||||
|
npm install @json-render/react @json-render/shadcn react react-dom
|
||||||
|
|
||||||
|
# Build tools
|
||||||
|
npm install -D vite @vitejs/plugin-react vite-plugin-singlefile
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user