mirror of
https://github.com/vercel-labs/json-render.git
synced 2026-10-03 20:38:21 +08:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b2020fad6f | ||
|
|
5b51204960 | ||
|
|
3418d42a43 |
+11
-3
@@ -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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
node_modules
|
||||
.pnp
|
||||
.pnp.js
|
||||
.pnpm-store/
|
||||
|
||||
# Local env files
|
||||
.env*
|
||||
|
||||
Vendored
+1
-1
@@ -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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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`
|
||||
|
||||
|
||||
@@ -11,13 +11,20 @@ MCP Apps integration for json-render. Serve json-render UIs as interactive [MCP
|
||||
npm install @json-render/mcp @json-render/core @modelcontextprotocol/sdk
|
||||
```
|
||||
|
||||
For the iframe-side React UI, also install:
|
||||
For the iframe-side UI, install the renderer for your framework:
|
||||
|
||||
```bash
|
||||
# React
|
||||
npm install @json-render/react react react-dom
|
||||
|
||||
# Vue
|
||||
npm install @json-render/vue vue
|
||||
|
||||
# Svelte
|
||||
npm install @json-render/svelte svelte
|
||||
```
|
||||
|
||||
See the [MCP example](https://github.com/vercel-labs/json-render/tree/main/examples/mcp) for a full working example.
|
||||
See the [MCP React example](https://github.com/vercel-labs/json-render/tree/main/examples/mcp-react) for a full working example.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -117,16 +124,22 @@ registerJsonRenderResource(server, {
|
||||
});
|
||||
```
|
||||
|
||||
## Client API (`@json-render/mcp/app`)
|
||||
## Client API
|
||||
|
||||
These exports run inside the sandboxed iframe rendered by the MCP host.
|
||||
These exports run inside the sandboxed iframe rendered by the MCP host. Framework-specific adapters are available at:
|
||||
|
||||
### useJsonRenderApp
|
||||
- `@json-render/mcp/app/react` -- React hook
|
||||
- `@json-render/mcp/app/vue` -- Vue composable
|
||||
- `@json-render/mcp/app/svelte` -- Svelte stores
|
||||
|
||||
The React hook is also re-exported from `@json-render/mcp/app` for backward compatibility.
|
||||
|
||||
### React -- useJsonRenderApp (`@json-render/mcp/app/react`)
|
||||
|
||||
React hook that connects to the MCP host, listens for tool results, and maintains the current json-render spec.
|
||||
|
||||
```tsx
|
||||
import { useJsonRenderApp } from "@json-render/mcp/app";
|
||||
import { useJsonRenderApp } from "@json-render/mcp/app/react";
|
||||
import { JSONUIProvider, Renderer } from "@json-render/react";
|
||||
|
||||
function McpAppView({ registry }) {
|
||||
@@ -146,7 +159,51 @@ function McpAppView({ registry }) {
|
||||
}
|
||||
```
|
||||
|
||||
#### UseJsonRenderAppReturn
|
||||
### Vue -- useJsonRenderApp (`@json-render/mcp/app/vue`)
|
||||
|
||||
Vue composable with the same API shape. Values are Vue refs and computed properties.
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
import { useJsonRenderApp } from "@json-render/mcp/app/vue";
|
||||
import { Renderer } from "@json-render/vue";
|
||||
|
||||
const { spec, loading, error } = useJsonRenderApp({ name: "my-app" });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="error">Error: {{ error.message }}</div>
|
||||
<div v-else-if="!spec">Waiting...</div>
|
||||
<Renderer v-else :spec="spec" :registry="registry" :loading="loading" />
|
||||
</template>
|
||||
```
|
||||
|
||||
### Svelte -- createJsonRenderApp (`@json-render/mcp/app/svelte`)
|
||||
|
||||
Creates a json-render MCP App client using Svelte stores. Call `destroy()` in `onDestroy` to clean up.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { createJsonRenderApp } from "@json-render/mcp/app/svelte";
|
||||
import { Renderer } from "@json-render/svelte";
|
||||
import { onDestroy } from "svelte";
|
||||
|
||||
const { spec, loading, error, destroy } = createJsonRenderApp({ name: "my-app" });
|
||||
onDestroy(destroy);
|
||||
</script>
|
||||
|
||||
{#if $error}
|
||||
<div>Error: {$error.message}</div>
|
||||
{:else if !$spec}
|
||||
<div>Waiting...</div>
|
||||
{:else}
|
||||
<Renderer spec={$spec} {registry} loading={$loading} />
|
||||
{/if}
|
||||
```
|
||||
|
||||
### Return values
|
||||
|
||||
All three adapters return the same logical state:
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
@@ -195,9 +252,11 @@ function McpAppView({ registry }) {
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
### buildAppHtml
|
||||
In React, values are plain primitives. In Vue, they are `Ref`/`ShallowRef`/`ComputedRef`. In Svelte, they are readable stores (use `$` prefix to subscribe). The Svelte adapter also returns a `destroy` function for cleanup.
|
||||
|
||||
Generate a self-contained HTML page from bundled JavaScript and CSS.
|
||||
### buildAppHtml (`@json-render/mcp/app`)
|
||||
|
||||
Generate a self-contained HTML page from bundled JavaScript and CSS. Framework-agnostic.
|
||||
|
||||
```typescript
|
||||
import { buildAppHtml } from "@json-render/mcp/app";
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
|
||||
Vendored
+1
-1
@@ -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.
|
||||
|
||||
Vendored
+1
-1
@@ -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.
|
||||
|
||||
@@ -20,7 +20,7 @@ Add to `.cursor/mcp.json`:
|
||||
"mcpServers": {
|
||||
"json-render": {
|
||||
"command": "npx",
|
||||
"args": ["tsx", "examples/mcp/server.ts", "--stdio"]
|
||||
"args": ["tsx", "examples/mcp-react/server.ts", "--stdio"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,7 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
||||
"mcpServers": {
|
||||
"json-render": {
|
||||
"command": "npx",
|
||||
"args": ["tsx", "/absolute/path/to/examples/mcp/server.ts", "--stdio"]
|
||||
"args": ["tsx", "/absolute/path/to/examples/mcp-react/server.ts", "--stdio"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "example-mcp",
|
||||
"name": "example-mcp-react",
|
||||
"version": "0.1.1",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { schema } from "@json-render/svelte/schema";
|
||||
import { z } from "zod";
|
||||
|
||||
export const catalog = schema.createCatalog({
|
||||
components: {
|
||||
Stack: {
|
||||
props: z.object({
|
||||
gap: z.number().optional(),
|
||||
padding: z.number().optional(),
|
||||
direction: z.enum(["vertical", "horizontal"]).optional(),
|
||||
align: z.enum(["start", "center", "end"]).optional(),
|
||||
}),
|
||||
slots: ["default"],
|
||||
description:
|
||||
"Layout container that stacks children vertically or horizontally",
|
||||
},
|
||||
Card: {
|
||||
props: z.object({
|
||||
title: z.string().optional(),
|
||||
subtitle: z.string().optional(),
|
||||
}),
|
||||
slots: ["default"],
|
||||
description: "A card container with optional title and subtitle",
|
||||
},
|
||||
Text: {
|
||||
props: z.object({
|
||||
content: z.string(),
|
||||
size: z.enum(["sm", "md", "lg", "xl"]).optional(),
|
||||
weight: z.enum(["normal", "medium", "bold"]).optional(),
|
||||
color: z.string().optional(),
|
||||
}),
|
||||
slots: [],
|
||||
description: "Displays a text string",
|
||||
},
|
||||
Button: {
|
||||
props: z.object({
|
||||
label: z.string(),
|
||||
variant: z.enum(["primary", "secondary", "danger"]).optional(),
|
||||
disabled: z.boolean().optional(),
|
||||
}),
|
||||
slots: [],
|
||||
description: "A clickable button that emits a 'press' event",
|
||||
},
|
||||
Badge: {
|
||||
props: z.object({
|
||||
label: z.string(),
|
||||
color: z.string().optional(),
|
||||
}),
|
||||
slots: [],
|
||||
description: "A small badge/tag label",
|
||||
},
|
||||
},
|
||||
actions: {},
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
<script lang="ts">
|
||||
import type { BaseComponentProps } from "@json-render/svelte";
|
||||
|
||||
interface Props extends BaseComponentProps<{
|
||||
label: string;
|
||||
color?: string;
|
||||
}> {}
|
||||
|
||||
let { props }: Props = $props();
|
||||
</script>
|
||||
|
||||
<span
|
||||
class="inline-flex items-center rounded-full px-3 py-1 text-xs font-medium bg-secondary text-secondary-foreground border border-border"
|
||||
style:background-color={props.color ? `${props.color}20` : undefined}
|
||||
style:color={props.color ?? undefined}
|
||||
style:border-color={props.color ? `${props.color}40` : undefined}>
|
||||
{props.label}
|
||||
</span>
|
||||
@@ -0,0 +1,26 @@
|
||||
<script lang="ts">
|
||||
import type { BaseComponentProps } from "@json-render/svelte";
|
||||
|
||||
interface Props extends BaseComponentProps<{
|
||||
label: string;
|
||||
variant?: "primary" | "secondary" | "danger";
|
||||
disabled?: boolean;
|
||||
}> {}
|
||||
|
||||
let { props, emit }: Props = $props();
|
||||
|
||||
const variantClasses: Record<string, string> = {
|
||||
primary: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
danger: "bg-destructive/10 text-destructive hover:bg-destructive/20 border border-destructive/20",
|
||||
};
|
||||
|
||||
let variant = $derived(props.variant ?? "primary");
|
||||
</script>
|
||||
|
||||
<button
|
||||
disabled={props.disabled}
|
||||
onclick={() => emit("press")}
|
||||
class="inline-flex items-center justify-center rounded-lg px-4 py-2 text-sm font-medium transition-colors {variantClasses[variant] ?? variantClasses.primary} {props.disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}">
|
||||
{props.label}
|
||||
</button>
|
||||
@@ -0,0 +1,29 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from "svelte";
|
||||
import type { BaseComponentProps } from "@json-render/svelte";
|
||||
|
||||
interface Props extends BaseComponentProps<{
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
}> {
|
||||
children?: Snippet;
|
||||
}
|
||||
|
||||
let { props, children }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="rounded-xl border border-border bg-card text-card-foreground p-5 shadow-sm">
|
||||
{#if props.title}
|
||||
<h2 class="text-base font-semibold text-card-foreground mb-1">
|
||||
{props.title}
|
||||
</h2>
|
||||
{/if}
|
||||
{#if props.subtitle}
|
||||
<p class="text-sm text-muted-foreground mb-3">
|
||||
{props.subtitle}
|
||||
</p>
|
||||
{/if}
|
||||
{#if children}
|
||||
{@render children()}
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,33 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from "svelte";
|
||||
import type { BaseComponentProps } from "@json-render/svelte";
|
||||
|
||||
interface Props extends BaseComponentProps<{
|
||||
gap?: number;
|
||||
padding?: number;
|
||||
direction?: "vertical" | "horizontal";
|
||||
align?: "start" | "center" | "end";
|
||||
}> {
|
||||
children?: Snippet;
|
||||
}
|
||||
|
||||
let { props, children }: Props = $props();
|
||||
let horizontal = $derived(props.direction === "horizontal");
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="flex"
|
||||
style:flex-direction={horizontal ? "row" : "column"}
|
||||
style:gap="{props.gap ?? 0}px"
|
||||
style:padding="{props.padding ?? 0}px"
|
||||
style:align-items={props.align === "start"
|
||||
? "flex-start"
|
||||
: props.align === "end"
|
||||
? "flex-end"
|
||||
: horizontal
|
||||
? "center"
|
||||
: "stretch"}>
|
||||
{#if children}
|
||||
{@render children()}
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,31 @@
|
||||
<script lang="ts">
|
||||
import type { BaseComponentProps } from "@json-render/svelte";
|
||||
|
||||
interface Props extends BaseComponentProps<{
|
||||
content: string;
|
||||
size?: "sm" | "md" | "lg" | "xl";
|
||||
weight?: "normal" | "medium" | "bold";
|
||||
color?: string;
|
||||
}> {}
|
||||
|
||||
let { props }: Props = $props();
|
||||
|
||||
const sizeClasses: Record<string, string> = {
|
||||
sm: "text-xs",
|
||||
md: "text-sm",
|
||||
lg: "text-base",
|
||||
xl: "text-2xl",
|
||||
};
|
||||
|
||||
const weightClasses: Record<string, string> = {
|
||||
normal: "font-normal",
|
||||
medium: "font-medium",
|
||||
bold: "font-bold",
|
||||
};
|
||||
</script>
|
||||
|
||||
<span
|
||||
class="{sizeClasses[props.size ?? 'md'] ?? 'text-sm'} {weightClasses[props.weight ?? 'normal'] ?? 'font-normal'} text-foreground"
|
||||
style:color={props.color ?? undefined}>
|
||||
{String(props.content ?? "")}
|
||||
</span>
|
||||
@@ -0,0 +1,98 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@custom-variant dark (&:is([data-theme="dark"] *));
|
||||
|
||||
@theme inline {
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--radius-2xl: calc(var(--radius) + 8px);
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
}
|
||||
|
||||
:root {
|
||||
--radius: 0.625rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.205 0 0);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
--background: var(--color-background-secondary, var(--vscode-editor-background, oklch(0.145 0 0)));
|
||||
--foreground: var(--color-text-primary, var(--vscode-foreground, oklch(0.985 0 0)));
|
||||
--card: var(--color-background-primary, var(--vscode-sideBar-background, oklch(0.205 0 0)));
|
||||
--card-foreground: var(--color-text-primary, var(--vscode-foreground, oklch(0.985 0 0)));
|
||||
--primary: var(--color-text-primary, var(--vscode-foreground, oklch(0.922 0 0)));
|
||||
--primary-foreground: var(--color-background-primary, var(--vscode-sideBar-background, oklch(0.205 0 0)));
|
||||
--secondary: var(--color-background-tertiary, var(--vscode-activityBar-background, oklch(0.269 0 0)));
|
||||
--secondary-foreground: var(--color-text-primary, var(--vscode-foreground, oklch(0.985 0 0)));
|
||||
--muted: var(--color-background-tertiary, var(--vscode-activityBar-background, oklch(0.269 0 0)));
|
||||
--muted-foreground: var(--color-text-secondary, var(--vscode-descriptionForeground, oklch(0.708 0 0)));
|
||||
--accent: var(--color-background-tertiary, var(--vscode-activityBar-background, oklch(0.269 0 0)));
|
||||
--accent-foreground: var(--color-text-primary, var(--vscode-foreground, oklch(0.985 0 0)));
|
||||
--destructive: var(--color-text-danger, var(--vscode-errorForeground, oklch(0.704 0.191 22.216)));
|
||||
--border: var(--color-border-primary, var(--vscode-widget-border, oklch(1 0 0 / 10%)));
|
||||
--input: var(--color-border-secondary, var(--vscode-editorWidget-border, oklch(1 0 0 / 15%)));
|
||||
--ring: var(--color-ring-primary, var(--vscode-focusBorder, oklch(0.556 0 0)));
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
border-color: var(--color-border);
|
||||
}
|
||||
|
||||
html, body, #app {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--color-background);
|
||||
color: var(--color-foreground);
|
||||
font-family: var(--vscode-font-family, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply text-foreground;
|
||||
background-color: var(--color-background) !important;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import "./globals.css";
|
||||
import { mount } from "svelte";
|
||||
import App from "./App.svelte";
|
||||
|
||||
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||
document.documentElement.setAttribute(
|
||||
"data-theme",
|
||||
prefersDark ? "dark" : "light",
|
||||
);
|
||||
document.documentElement.style.colorScheme = prefersDark ? "dark" : "light";
|
||||
|
||||
mount(App, { target: document.getElementById("app")! });
|
||||
@@ -0,0 +1,18 @@
|
||||
import { type Components, defineRegistry } from "@json-render/svelte";
|
||||
import { catalog } from "./catalog";
|
||||
|
||||
import Stack from "./components/Stack.svelte";
|
||||
import Card from "./components/Card.svelte";
|
||||
import Text from "./components/Text.svelte";
|
||||
import Button from "./components/Button.svelte";
|
||||
import Badge from "./components/Badge.svelte";
|
||||
|
||||
const components: Components<typeof catalog> = {
|
||||
Stack,
|
||||
Card,
|
||||
Text,
|
||||
Button,
|
||||
Badge,
|
||||
};
|
||||
|
||||
export const { registry } = defineRegistry(catalog, { components });
|
||||
@@ -0,0 +1 @@
|
||||
export default {};
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src", "server.ts"]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { defineConfig } from "vite";
|
||||
import { svelte } from "@sveltejs/vite-plugin-svelte";
|
||||
import { viteSingleFile } from "vite-plugin-singlefile";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [svelte(), viteSingleFile()],
|
||||
build: {
|
||||
outDir: "dist",
|
||||
emptyOutDir: false,
|
||||
rollupOptions: {
|
||||
input: "index.html",
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline' 'unsafe-eval' data: blob:; img-src * data: blob:; font-src * data:; connect-src *; media-src *;" />
|
||||
<title>json-render MCP App (Vue)</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "example-mcp-vue",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"start": "tsx server.ts",
|
||||
"start:stdio": "tsx server.ts --stdio"
|
||||
},
|
||||
"dependencies": {
|
||||
"@json-render/core": "workspace:*",
|
||||
"@json-render/mcp": "workspace:*",
|
||||
"@json-render/vue": "workspace:*",
|
||||
"@modelcontextprotocol/ext-apps": "^1.2.0",
|
||||
"@modelcontextprotocol/sdk": "^1.27.1",
|
||||
"vue": "^3.5.0",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.2.1",
|
||||
"@vitejs/plugin-vue": "^6.0.4",
|
||||
"tailwindcss": "^4.2.1",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.3.1",
|
||||
"vite-plugin-singlefile": "^2.3.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { createMcpApp } from "@json-render/mcp";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { catalog } from "./src/catalog.js";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
function loadHtml(): string {
|
||||
const htmlPath = path.join(__dirname, "dist", "index.html");
|
||||
if (!fs.existsSync(htmlPath)) {
|
||||
throw new Error(
|
||||
`Built HTML not found at ${htmlPath}. Run 'pnpm build' first.`,
|
||||
);
|
||||
}
|
||||
return fs.readFileSync(htmlPath, "utf-8");
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const html = loadHtml();
|
||||
const server = await createMcpApp({
|
||||
name: "json-render Vue Example",
|
||||
version: "1.0.0",
|
||||
catalog,
|
||||
html,
|
||||
});
|
||||
await server.connect(new StdioServerTransport());
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
import { useJsonRenderApp } from "@json-render/mcp/app/vue";
|
||||
import { Renderer, defineRegistry, JSONUIProvider } from "@json-render/vue";
|
||||
import { catalog } from "./catalog";
|
||||
import { components } from "./registry";
|
||||
|
||||
const { registry } = defineRegistry(catalog, { components });
|
||||
|
||||
const { spec, loading, connecting, error } = useJsonRenderApp({
|
||||
name: "json-render-mcp-vue",
|
||||
version: "1.0.0",
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="error"
|
||||
class="p-4 font-mono text-sm text-destructive"
|
||||
>
|
||||
{{ error.message }}
|
||||
</div>
|
||||
<div
|
||||
v-else-if="!spec"
|
||||
class="p-4 text-sm text-muted-foreground"
|
||||
>
|
||||
{{ connecting ? "Connecting to host..." : "Waiting for UI spec..." }}
|
||||
</div>
|
||||
<JSONUIProvider v-else :registry="registry" :initialState="spec.state ?? {}">
|
||||
<Renderer :spec="spec" :registry="registry" :loading="loading" />
|
||||
</JSONUIProvider>
|
||||
</template>
|
||||
@@ -0,0 +1,56 @@
|
||||
import { schema } from "@json-render/vue/schema";
|
||||
import { z } from "zod";
|
||||
|
||||
export const catalog = schema.createCatalog({
|
||||
components: {
|
||||
Stack: {
|
||||
props: z.object({
|
||||
gap: z.number().optional(),
|
||||
padding: z.number().optional(),
|
||||
direction: z.enum(["vertical", "horizontal"]).optional(),
|
||||
align: z.enum(["start", "center", "end"]).optional(),
|
||||
}),
|
||||
slots: ["default"],
|
||||
description:
|
||||
"Layout container that stacks children vertically or horizontally",
|
||||
},
|
||||
Card: {
|
||||
props: z.object({
|
||||
title: z.string().optional(),
|
||||
subtitle: z.string().optional(),
|
||||
}),
|
||||
slots: ["default"],
|
||||
description: "A card container with optional title and subtitle",
|
||||
},
|
||||
Text: {
|
||||
props: z.object({
|
||||
content: z.string(),
|
||||
size: z.enum(["sm", "md", "lg", "xl"]).optional(),
|
||||
weight: z.enum(["normal", "medium", "bold"]).optional(),
|
||||
color: z.string().optional(),
|
||||
}),
|
||||
slots: [],
|
||||
description: "Displays a text string",
|
||||
},
|
||||
Button: {
|
||||
props: z.object({
|
||||
label: z.string(),
|
||||
variant: z.enum(["primary", "secondary", "danger"]).optional(),
|
||||
disabled: z.boolean().optional(),
|
||||
}),
|
||||
slots: [],
|
||||
description: "A clickable button that emits a 'press' event",
|
||||
},
|
||||
Badge: {
|
||||
props: z.object({
|
||||
label: z.string(),
|
||||
color: z.string().optional(),
|
||||
}),
|
||||
slots: [],
|
||||
description: "A small badge/tag label",
|
||||
},
|
||||
},
|
||||
actions: {},
|
||||
});
|
||||
|
||||
export type AppCatalog = typeof catalog;
|
||||
@@ -0,0 +1,98 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@custom-variant dark (&:is([data-theme="dark"] *));
|
||||
|
||||
@theme inline {
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--radius-2xl: calc(var(--radius) + 8px);
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
}
|
||||
|
||||
:root {
|
||||
--radius: 0.625rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.205 0 0);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
--background: var(--color-background-secondary, var(--vscode-editor-background, oklch(0.145 0 0)));
|
||||
--foreground: var(--color-text-primary, var(--vscode-foreground, oklch(0.985 0 0)));
|
||||
--card: var(--color-background-primary, var(--vscode-sideBar-background, oklch(0.205 0 0)));
|
||||
--card-foreground: var(--color-text-primary, var(--vscode-foreground, oklch(0.985 0 0)));
|
||||
--primary: var(--color-text-primary, var(--vscode-foreground, oklch(0.922 0 0)));
|
||||
--primary-foreground: var(--color-background-primary, var(--vscode-sideBar-background, oklch(0.205 0 0)));
|
||||
--secondary: var(--color-background-tertiary, var(--vscode-activityBar-background, oklch(0.269 0 0)));
|
||||
--secondary-foreground: var(--color-text-primary, var(--vscode-foreground, oklch(0.985 0 0)));
|
||||
--muted: var(--color-background-tertiary, var(--vscode-activityBar-background, oklch(0.269 0 0)));
|
||||
--muted-foreground: var(--color-text-secondary, var(--vscode-descriptionForeground, oklch(0.708 0 0)));
|
||||
--accent: var(--color-background-tertiary, var(--vscode-activityBar-background, oklch(0.269 0 0)));
|
||||
--accent-foreground: var(--color-text-primary, var(--vscode-foreground, oklch(0.985 0 0)));
|
||||
--destructive: var(--color-text-danger, var(--vscode-errorForeground, oklch(0.704 0.191 22.216)));
|
||||
--border: var(--color-border-primary, var(--vscode-widget-border, oklch(1 0 0 / 10%)));
|
||||
--input: var(--color-border-secondary, var(--vscode-editorWidget-border, oklch(1 0 0 / 15%)));
|
||||
--ring: var(--color-ring-primary, var(--vscode-focusBorder, oklch(0.556 0 0)));
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
border-color: var(--color-border);
|
||||
}
|
||||
|
||||
html, body, #app {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--color-background);
|
||||
color: var(--color-foreground);
|
||||
font-family: var(--vscode-font-family, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply text-foreground;
|
||||
background-color: var(--color-background) !important;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import "./globals.css";
|
||||
import { createApp } from "vue";
|
||||
import App from "./App.vue";
|
||||
|
||||
// Fallback theme before MCP host provides context
|
||||
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||
document.documentElement.setAttribute(
|
||||
"data-theme",
|
||||
prefersDark ? "dark" : "light",
|
||||
);
|
||||
document.documentElement.style.colorScheme = prefersDark ? "dark" : "light";
|
||||
|
||||
createApp(App).mount("#app");
|
||||
@@ -0,0 +1,116 @@
|
||||
import { h } from "vue";
|
||||
import type { Components } from "@json-render/vue";
|
||||
import type { AppCatalog } from "./catalog";
|
||||
|
||||
export const components: Components<AppCatalog> = {
|
||||
Stack: ({ props, children }) => {
|
||||
const horizontal = props.direction === "horizontal";
|
||||
return h(
|
||||
"div",
|
||||
{
|
||||
class: "flex",
|
||||
style: {
|
||||
flexDirection: horizontal ? "row" : "column",
|
||||
gap: props.gap ? `${props.gap}px` : undefined,
|
||||
padding: props.padding ? `${props.padding}px` : undefined,
|
||||
alignItems: props.align ?? (horizontal ? "center" : "stretch"),
|
||||
},
|
||||
},
|
||||
children,
|
||||
);
|
||||
},
|
||||
|
||||
Card: ({ props, children }) =>
|
||||
h(
|
||||
"div",
|
||||
{
|
||||
class:
|
||||
"rounded-xl border border-border bg-card text-card-foreground p-5 shadow-sm",
|
||||
},
|
||||
[
|
||||
props.title &&
|
||||
h(
|
||||
"h2",
|
||||
{
|
||||
class: "text-base font-semibold text-card-foreground mb-1",
|
||||
},
|
||||
props.title,
|
||||
),
|
||||
props.subtitle &&
|
||||
h(
|
||||
"p",
|
||||
{
|
||||
class: "text-sm text-muted-foreground mb-3",
|
||||
},
|
||||
props.subtitle,
|
||||
),
|
||||
children,
|
||||
],
|
||||
),
|
||||
|
||||
Text: ({ props }) => {
|
||||
const sizeClasses: Record<string, string> = {
|
||||
sm: "text-xs",
|
||||
md: "text-sm",
|
||||
lg: "text-base",
|
||||
xl: "text-2xl",
|
||||
};
|
||||
const weightClasses: Record<string, string> = {
|
||||
normal: "font-normal",
|
||||
medium: "font-medium",
|
||||
bold: "font-bold",
|
||||
};
|
||||
return h(
|
||||
"span",
|
||||
{
|
||||
class: [
|
||||
sizeClasses[props.size ?? "md"] ?? "text-sm",
|
||||
weightClasses[props.weight ?? "normal"] ?? "font-normal",
|
||||
"text-foreground",
|
||||
].join(" "),
|
||||
style: props.color ? { color: props.color } : undefined,
|
||||
},
|
||||
String(props.content ?? ""),
|
||||
);
|
||||
},
|
||||
|
||||
Button: ({ props, emit }) => {
|
||||
const variant = props.variant ?? "primary";
|
||||
const variantClasses: Record<string, string> = {
|
||||
primary: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
danger:
|
||||
"bg-destructive/10 text-destructive hover:bg-destructive/20 border border-destructive/20",
|
||||
};
|
||||
return h(
|
||||
"button",
|
||||
{
|
||||
disabled: props.disabled,
|
||||
onClick: () => emit("press"),
|
||||
class: [
|
||||
"inline-flex items-center justify-center rounded-lg px-4 py-2 text-sm font-medium transition-colors",
|
||||
variantClasses[variant] ?? variantClasses.primary,
|
||||
props.disabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer",
|
||||
].join(" "),
|
||||
},
|
||||
props.label,
|
||||
);
|
||||
},
|
||||
|
||||
Badge: ({ props }) =>
|
||||
h(
|
||||
"span",
|
||||
{
|
||||
class:
|
||||
"inline-flex items-center rounded-full px-3 py-1 text-xs font-medium bg-secondary text-secondary-foreground border border-border",
|
||||
style: props.color
|
||||
? {
|
||||
backgroundColor: `${props.color}20`,
|
||||
color: props.color,
|
||||
borderColor: `${props.color}40`,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
props.label,
|
||||
),
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"jsx": "preserve"
|
||||
},
|
||||
"include": ["src", "server.ts"]
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { defineConfig } from "vite";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
import { viteSingleFile } from "vite-plugin-singlefile";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue(), tailwindcss(), viteSingleFile()],
|
||||
build: {
|
||||
outDir: "dist",
|
||||
emptyOutDir: false,
|
||||
rollupOptions: {
|
||||
input: "index.html",
|
||||
},
|
||||
},
|
||||
});
|
||||
Vendored
+1
-1
@@ -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.
|
||||
|
||||
Vendored
+1
-1
@@ -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.
|
||||
|
||||
Vendored
+1
-1
@@ -1,6 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
import "./.next/dev/types/routes.d.ts";
|
||||
import "./.next/types/routes.d.ts";
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
|
||||
+1
-1
@@ -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.
|
||||
|
||||
+58
-6
@@ -46,10 +46,12 @@ await server.connect(new StdioServerTransport());
|
||||
|
||||
### 3. Build the UI (iframe)
|
||||
|
||||
Create a React app that uses `useJsonRenderApp` from `@json-render/mcp/app`:
|
||||
Create an app that connects to the MCP host and renders specs. Framework-specific adapters are available for React, Vue, and Svelte.
|
||||
|
||||
#### React
|
||||
|
||||
```tsx
|
||||
import { useJsonRenderApp } from "@json-render/mcp/app";
|
||||
import { useJsonRenderApp } from "@json-render/mcp/app/react";
|
||||
import { JSONUIProvider, Renderer } from "@json-render/react";
|
||||
|
||||
function McpAppView({ registry }) {
|
||||
@@ -66,6 +68,44 @@ function McpAppView({ registry }) {
|
||||
}
|
||||
```
|
||||
|
||||
#### Vue
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
import { useJsonRenderApp } from "@json-render/mcp/app/vue";
|
||||
import { Renderer } from "@json-render/vue";
|
||||
|
||||
const { spec, loading, error } = useJsonRenderApp();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="error">Error: {{ error.message }}</div>
|
||||
<div v-else-if="!spec">Waiting for spec...</div>
|
||||
<Renderer v-else :spec="spec" :registry="registry" :loading="loading" />
|
||||
</template>
|
||||
```
|
||||
|
||||
#### Svelte
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { createJsonRenderApp } from "@json-render/mcp/app/svelte";
|
||||
import { Renderer } from "@json-render/svelte";
|
||||
import { onDestroy } from "svelte";
|
||||
|
||||
const { spec, loading, error, destroy } = createJsonRenderApp();
|
||||
onDestroy(destroy);
|
||||
</script>
|
||||
|
||||
{#if $error}
|
||||
<div>Error: {$error.message}</div>
|
||||
{:else if !$spec}
|
||||
<div>Waiting for spec...</div>
|
||||
{:else}
|
||||
<Renderer spec={$spec} {registry} loading={$loading} />
|
||||
{/if}
|
||||
```
|
||||
|
||||
Bundle with Vite + `vite-plugin-singlefile` into a single HTML file, then pass it to `createMcpApp` as the `html` option.
|
||||
|
||||
### 4. Connect to a client
|
||||
@@ -107,17 +147,29 @@ Register a json-render tool on an existing `McpServer`.
|
||||
|
||||
Register a json-render UI resource on an existing `McpServer`.
|
||||
|
||||
### Client Side (`@json-render/mcp/app`)
|
||||
### Client Side
|
||||
|
||||
#### `useJsonRenderApp(options?)`
|
||||
#### `buildAppHtml(options)` — `@json-render/mcp/app`
|
||||
|
||||
Generate a self-contained HTML string from bundled JS/CSS for use as a UI resource.
|
||||
|
||||
#### `useJsonRenderApp(options?)` — `@json-render/mcp/app/react`
|
||||
|
||||
React hook for the iframe-side app. Connects to the MCP host, receives tool results, and maintains the current json-render spec.
|
||||
|
||||
Returns `{ spec, loading, connected, connecting, error, app, callServerTool }`.
|
||||
|
||||
#### `buildAppHtml(options)`
|
||||
#### `useJsonRenderApp(options?)` — `@json-render/mcp/app/vue`
|
||||
|
||||
Generate a self-contained HTML string from bundled JS/CSS for use as a UI resource.
|
||||
Vue composable with the same API shape. Values are Vue refs/computed properties.
|
||||
|
||||
Returns `{ spec, loading, connected, connecting, error, app, callServerTool }`.
|
||||
|
||||
#### `createJsonRenderApp(options?)` — `@json-render/mcp/app/svelte`
|
||||
|
||||
Creates a json-render MCP App client using Svelte stores. Values are readable stores.
|
||||
|
||||
Returns `{ spec, loading, connected, connecting, error, app, callServerTool, destroy }`.
|
||||
|
||||
## Client Support
|
||||
|
||||
|
||||
@@ -42,6 +42,21 @@
|
||||
"types": "./dist/app.d.ts",
|
||||
"import": "./dist/app.mjs",
|
||||
"require": "./dist/app.js"
|
||||
},
|
||||
"./app/react": {
|
||||
"types": "./dist/app/react.d.ts",
|
||||
"import": "./dist/app/react.mjs",
|
||||
"require": "./dist/app/react.js"
|
||||
},
|
||||
"./app/vue": {
|
||||
"types": "./dist/app/vue.d.ts",
|
||||
"import": "./dist/app/vue.mjs",
|
||||
"require": "./dist/app/vue.js"
|
||||
},
|
||||
"./app/svelte": {
|
||||
"types": "./dist/app/svelte.d.ts",
|
||||
"import": "./dist/app/svelte.mjs",
|
||||
"require": "./dist/app/svelte.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
@@ -60,12 +75,16 @@
|
||||
"devDependencies": {
|
||||
"@internal/typescript-config": "workspace:*",
|
||||
"@types/react": "19.2.3",
|
||||
"svelte": "^5.0.0",
|
||||
"tsup": "^8.0.2",
|
||||
"typescript": "^5.4.5"
|
||||
"typescript": "^5.4.5",
|
||||
"vue": "^3.5.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
"react-dom": "^19.0.0",
|
||||
"svelte": "^5.0.0",
|
||||
"vue": "^3.5.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react": {
|
||||
@@ -73,6 +92,12 @@
|
||||
},
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
},
|
||||
"svelte": {
|
||||
"optional": true
|
||||
},
|
||||
"vue": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+12
-4
@@ -5,11 +5,18 @@
|
||||
* 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.
|
||||
* framework-specific hooks / helpers to render them.
|
||||
*
|
||||
* Framework-specific adapters are available at:
|
||||
* - `@json-render/mcp/app/react` — React hook
|
||||
* - `@json-render/mcp/app/vue` — Vue composable
|
||||
* - `@json-render/mcp/app/svelte` — Svelte stores
|
||||
*
|
||||
* The React hook is also re-exported here for backward compatibility.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* import { useJsonRenderApp } from "@json-render/mcp/app";
|
||||
* import { useJsonRenderApp } from "@json-render/mcp/app/react";
|
||||
* import { Renderer } from "@json-render/react";
|
||||
*
|
||||
* function McpAppView({ registry }) {
|
||||
@@ -21,10 +28,11 @@
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
export { useJsonRenderApp } from "./use-json-render-app.js";
|
||||
export { useJsonRenderApp } from "./app/react.js";
|
||||
export type {
|
||||
UseJsonRenderAppOptions,
|
||||
UseJsonRenderAppReturn,
|
||||
} from "./use-json-render-app.js";
|
||||
} from "./app/react.js";
|
||||
export { buildAppHtml } from "./build-app-html.js";
|
||||
export type { BuildAppHtmlOptions } from "./build-app-html.js";
|
||||
export type { JsonRenderAppOptions, JsonRenderAppState } from "./app/shared.js";
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
import type { Spec } from "@json-render/core";
|
||||
import { App } from "@modelcontextprotocol/ext-apps";
|
||||
import {
|
||||
parseSpecFromToolResult,
|
||||
type JsonRenderAppOptions,
|
||||
type ToolResultContent,
|
||||
} from "./shared.js";
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
export type UseJsonRenderAppOptions = JsonRenderAppOptions;
|
||||
|
||||
/**
|
||||
* Return value of `useJsonRenderApp`.
|
||||
@@ -38,29 +38,6 @@ export interface UseJsonRenderAppReturn {
|
||||
) => 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.
|
||||
@@ -92,8 +69,6 @@ export function useJsonRenderApp(
|
||||
}
|
||||
};
|
||||
|
||||
// Let the App class handle transport creation internally,
|
||||
// matching the official MCP Apps quickstart pattern.
|
||||
app
|
||||
.connect()
|
||||
.then(() => {
|
||||
@@ -0,0 +1,61 @@
|
||||
import type { Spec } from "@json-render/core";
|
||||
import type { App } from "@modelcontextprotocol/ext-apps";
|
||||
|
||||
/**
|
||||
* Options for creating a json-render MCP App client.
|
||||
*/
|
||||
export interface JsonRenderAppOptions {
|
||||
/** App name shown during initialization. Defaults to `"json-render"`. */
|
||||
name?: string;
|
||||
/** App version. Defaults to `"1.0.0"`. */
|
||||
version?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* State returned by the json-render MCP App client across all frameworks.
|
||||
*/
|
||||
export interface JsonRenderAppState {
|
||||
/** 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>;
|
||||
}
|
||||
|
||||
export interface ToolResultContent {
|
||||
type: string;
|
||||
text?: string;
|
||||
}
|
||||
|
||||
export 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import { writable, derived, type Readable, type Writable } from "svelte/store";
|
||||
import type { Spec } from "@json-render/core";
|
||||
import { App } from "@modelcontextprotocol/ext-apps";
|
||||
import {
|
||||
parseSpecFromToolResult,
|
||||
type JsonRenderAppOptions,
|
||||
type ToolResultContent,
|
||||
} from "./shared.js";
|
||||
|
||||
/**
|
||||
* Options for `createJsonRenderApp`.
|
||||
*/
|
||||
export type CreateJsonRenderAppOptions = JsonRenderAppOptions;
|
||||
|
||||
/**
|
||||
* Return value of `createJsonRenderApp`.
|
||||
*/
|
||||
export interface CreateJsonRenderAppReturn {
|
||||
/** The current json-render spec (null until the first tool result). */
|
||||
spec: Readable<Spec | null>;
|
||||
/** Whether the app is still connecting to the host. */
|
||||
connecting: Readable<boolean>;
|
||||
/** Whether the app is connected to the host. */
|
||||
connected: Readable<boolean>;
|
||||
/** Connection error, if any. */
|
||||
error: Readable<Error | null>;
|
||||
/** Whether the spec is still being received / parsed. */
|
||||
loading: Readable<boolean>;
|
||||
/** The underlying MCP App instance. */
|
||||
app: Readable<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>;
|
||||
/** Clean up the MCP App connection. Call in `onDestroy`. */
|
||||
destroy: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a json-render MCP App client using Svelte stores.
|
||||
*
|
||||
* Connects to the MCP host, listens for tool results, and maintains
|
||||
* the current json-render spec as a readable store.
|
||||
*
|
||||
* Call `destroy()` in your component's `onDestroy` to clean up.
|
||||
*
|
||||
* @example
|
||||
* ```svelte
|
||||
* <script>
|
||||
* import { createJsonRenderApp } from "@json-render/mcp/app/svelte";
|
||||
* import { onDestroy } from "svelte";
|
||||
*
|
||||
* const { spec, loading, destroy } = createJsonRenderApp();
|
||||
* onDestroy(destroy);
|
||||
* </script>
|
||||
* ```
|
||||
*/
|
||||
export function createJsonRenderApp(
|
||||
options: CreateJsonRenderAppOptions = {},
|
||||
): CreateJsonRenderAppReturn {
|
||||
const { name = "json-render", version = "1.0.0" } = options;
|
||||
|
||||
const spec: Writable<Spec | null> = writable(null);
|
||||
const loading: Writable<boolean> = writable(true);
|
||||
const connected: Writable<boolean> = writable(false);
|
||||
const error: Writable<Error | null> = writable(null);
|
||||
const appStore: Writable<App | null> = writable(null);
|
||||
|
||||
const connecting: Readable<boolean> = derived(
|
||||
[connected, error],
|
||||
([$connected, $error]) => !$connected && !$error,
|
||||
);
|
||||
|
||||
const app = new App({ name, version });
|
||||
appStore.set(app);
|
||||
|
||||
app.ontoolresult = (result: { content?: ToolResultContent[] }) => {
|
||||
const parsed = parseSpecFromToolResult(result);
|
||||
if (parsed) {
|
||||
spec.set(parsed);
|
||||
loading.set(false);
|
||||
}
|
||||
};
|
||||
|
||||
app
|
||||
.connect()
|
||||
.then(() => {
|
||||
connected.set(true);
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
error.set(err instanceof Error ? err : new Error(String(err)));
|
||||
});
|
||||
|
||||
async function callServerTool(
|
||||
toolName: string,
|
||||
args: Record<string, unknown> = {},
|
||||
): Promise<void> {
|
||||
loading.set(true);
|
||||
try {
|
||||
const result = await app.callServerTool({
|
||||
name: toolName,
|
||||
arguments: args,
|
||||
});
|
||||
const parsed = parseSpecFromToolResult(result);
|
||||
if (parsed) spec.set(parsed);
|
||||
} finally {
|
||||
loading.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
function destroy(): void {
|
||||
app.close().catch(() => {});
|
||||
}
|
||||
|
||||
return {
|
||||
spec,
|
||||
connecting,
|
||||
connected,
|
||||
error,
|
||||
loading,
|
||||
app: appStore,
|
||||
callServerTool,
|
||||
destroy,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import {
|
||||
shallowRef,
|
||||
ref,
|
||||
computed,
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
type ShallowRef,
|
||||
type Ref,
|
||||
type ComputedRef,
|
||||
} from "vue";
|
||||
import type { Spec } from "@json-render/core";
|
||||
import { App } from "@modelcontextprotocol/ext-apps";
|
||||
import {
|
||||
parseSpecFromToolResult,
|
||||
type JsonRenderAppOptions,
|
||||
type ToolResultContent,
|
||||
} from "./shared.js";
|
||||
|
||||
/**
|
||||
* Options for the `useJsonRenderApp` composable.
|
||||
*/
|
||||
export type UseJsonRenderAppOptions = JsonRenderAppOptions;
|
||||
|
||||
/**
|
||||
* Return value of `useJsonRenderApp`.
|
||||
*/
|
||||
export interface UseJsonRenderAppReturn {
|
||||
/** The current json-render spec (null until the first tool result). */
|
||||
spec: ShallowRef<Spec | null>;
|
||||
/** Whether the app is still connecting to the host. */
|
||||
connecting: ComputedRef<boolean>;
|
||||
/** Whether the app is connected to the host. */
|
||||
connected: Ref<boolean>;
|
||||
/** Connection error, if any. */
|
||||
error: Ref<Error | null>;
|
||||
/** Whether the spec is still being received / parsed. */
|
||||
loading: Ref<boolean>;
|
||||
/** The underlying MCP App instance. */
|
||||
app: ShallowRef<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>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vue composable that connects to the MCP host, listens for tool results,
|
||||
* and maintains the current json-render spec.
|
||||
*
|
||||
* Must be called inside a component's `setup` function or `<script setup>`.
|
||||
*/
|
||||
export function useJsonRenderApp(
|
||||
options: UseJsonRenderAppOptions = {},
|
||||
): UseJsonRenderAppReturn {
|
||||
const { name = "json-render", version = "1.0.0" } = options;
|
||||
|
||||
const spec = shallowRef<Spec | null>(null);
|
||||
const loading = ref(true);
|
||||
const connected = ref(false);
|
||||
const error = ref<Error | null>(null);
|
||||
const appRef = shallowRef<App | null>(null);
|
||||
const connecting = computed(() => !connected.value && !error.value);
|
||||
|
||||
let appInstance: App | null = null;
|
||||
|
||||
onMounted(() => {
|
||||
const app = new App({ name, version });
|
||||
appInstance = app;
|
||||
appRef.value = app;
|
||||
|
||||
app.ontoolresult = (result: { content?: ToolResultContent[] }) => {
|
||||
const parsed = parseSpecFromToolResult(result);
|
||||
if (parsed) {
|
||||
spec.value = parsed;
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
app
|
||||
.connect()
|
||||
.then(() => {
|
||||
connected.value = true;
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
error.value = err instanceof Error ? err : new Error(String(err));
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
appInstance?.close().catch(() => {});
|
||||
});
|
||||
|
||||
async function callServerTool(
|
||||
toolName: string,
|
||||
args: Record<string, unknown> = {},
|
||||
): Promise<void> {
|
||||
if (!appInstance) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
const result = await appInstance.callServerTool({
|
||||
name: toolName,
|
||||
arguments: args,
|
||||
});
|
||||
const parsed = parseSpecFromToolResult(result);
|
||||
if (parsed) spec.value = parsed;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
spec,
|
||||
connecting,
|
||||
connected,
|
||||
error,
|
||||
loading,
|
||||
app: appRef,
|
||||
callServerTool,
|
||||
};
|
||||
}
|
||||
@@ -1,7 +1,13 @@
|
||||
import { defineConfig } from "tsup";
|
||||
|
||||
export default defineConfig({
|
||||
entry: ["src/index.ts", "src/app.ts"],
|
||||
entry: [
|
||||
"src/index.ts",
|
||||
"src/app.ts",
|
||||
"src/app/react.ts",
|
||||
"src/app/vue.ts",
|
||||
"src/app/svelte.ts",
|
||||
],
|
||||
format: ["cjs", "esm"],
|
||||
dts: true,
|
||||
sourcemap: true,
|
||||
@@ -9,6 +15,9 @@ export default defineConfig({
|
||||
external: [
|
||||
"react",
|
||||
"react-dom",
|
||||
"vue",
|
||||
"svelte",
|
||||
"svelte/store",
|
||||
"@json-render/core",
|
||||
"@json-render/react",
|
||||
"@modelcontextprotocol/sdk",
|
||||
|
||||
Generated
+107
-13
@@ -520,7 +520,7 @@ importers:
|
||||
specifier: ^5.7.2
|
||||
version: 5.9.3
|
||||
|
||||
examples/mcp:
|
||||
examples/mcp-react:
|
||||
dependencies:
|
||||
'@json-render/core':
|
||||
specifier: workspace:*
|
||||
@@ -578,6 +578,98 @@ importers:
|
||||
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/mcp-svelte:
|
||||
dependencies:
|
||||
'@json-render/core':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/core
|
||||
'@json-render/mcp':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/mcp
|
||||
'@json-render/svelte':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/svelte
|
||||
'@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)
|
||||
svelte:
|
||||
specifier: ^5.49.2
|
||||
version: 5.53.5
|
||||
zod:
|
||||
specifier: ^4.3.6
|
||||
version: 4.3.6
|
||||
devDependencies:
|
||||
'@sveltejs/vite-plugin-svelte':
|
||||
specifier: ^6.2.4
|
||||
version: 6.2.4(svelte@5.53.5)(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':
|
||||
specifier: ^4.2.1
|
||||
version: 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:
|
||||
specifier: ^4.2.1
|
||||
version: 4.2.1
|
||||
tsx:
|
||||
specifier: ^4.21.0
|
||||
version: 4.21.0
|
||||
typescript:
|
||||
specifier: ^5.9.3
|
||||
version: 5.9.3
|
||||
vite:
|
||||
specifier: ^7.3.1
|
||||
version: 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-plugin-singlefile:
|
||||
specifier: ^2.3.0
|
||||
version: 2.3.0(rollup@4.55.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))
|
||||
|
||||
examples/mcp-vue:
|
||||
dependencies:
|
||||
'@json-render/core':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/core
|
||||
'@json-render/mcp':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/mcp
|
||||
'@json-render/vue':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/vue
|
||||
'@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)
|
||||
vue:
|
||||
specifier: ^3.5.0
|
||||
version: 3.5.29(typescript@5.9.3)
|
||||
zod:
|
||||
specifier: ^4.3.6
|
||||
version: 4.3.6
|
||||
devDependencies:
|
||||
'@tailwindcss/vite':
|
||||
specifier: ^4.2.1
|
||||
version: 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))
|
||||
'@vitejs/plugin-vue':
|
||||
specifier: ^6.0.4
|
||||
version: 6.0.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))(vue@3.5.29(typescript@5.9.3))
|
||||
tailwindcss:
|
||||
specifier: ^4.2.1
|
||||
version: 4.2.1
|
||||
tsx:
|
||||
specifier: ^4.21.0
|
||||
version: 4.21.0
|
||||
typescript:
|
||||
specifier: ^5.9.3
|
||||
version: 5.9.3
|
||||
vite:
|
||||
specifier: ^7.3.1
|
||||
version: 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-plugin-singlefile:
|
||||
specifier: ^2.3.0
|
||||
version: 2.3.0(rollup@4.55.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))
|
||||
|
||||
examples/no-ai:
|
||||
dependencies:
|
||||
'@json-render/core':
|
||||
@@ -1341,12 +1433,18 @@ importers:
|
||||
'@types/react':
|
||||
specifier: 19.2.3
|
||||
version: 19.2.3
|
||||
svelte:
|
||||
specifier: ^5.0.0
|
||||
version: 5.53.5
|
||||
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
|
||||
vue:
|
||||
specifier: ^3.5.0
|
||||
version: 3.5.29(typescript@5.9.3)
|
||||
|
||||
packages/react:
|
||||
dependencies:
|
||||
@@ -3732,16 +3830,6 @@ packages:
|
||||
react-dom:
|
||||
optional: true
|
||||
|
||||
'@modelcontextprotocol/sdk@1.26.0':
|
||||
resolution: {integrity: sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@cfworker/json-schema': ^4.1.1
|
||||
zod: ^3.25 || ^4.0
|
||||
peerDependenciesMeta:
|
||||
'@cfworker/json-schema':
|
||||
optional: true
|
||||
|
||||
'@modelcontextprotocol/sdk@1.27.1':
|
||||
resolution: {integrity: sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -15035,7 +15123,7 @@ snapshots:
|
||||
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.27.1(zod@3.25.76)':
|
||||
dependencies:
|
||||
'@hono/node-server': 1.19.9(hono@4.12.0)
|
||||
ajv: 8.17.1
|
||||
@@ -25465,7 +25553,7 @@ snapshots:
|
||||
'@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0)
|
||||
'@babel/preset-typescript': 7.28.5(@babel/core@7.29.0)
|
||||
'@dotenvx/dotenvx': 1.52.0
|
||||
'@modelcontextprotocol/sdk': 1.26.0(zod@3.25.76)
|
||||
'@modelcontextprotocol/sdk': 1.27.1(zod@3.25.76)
|
||||
'@types/validate-npm-package-name': 4.0.2
|
||||
browserslist: 4.28.1
|
||||
commander: 14.0.2
|
||||
@@ -26670,6 +26758,12 @@ snapshots:
|
||||
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-plugin-singlefile@2.3.0(rollup@4.55.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:
|
||||
micromatch: 4.0.8
|
||||
rollup: 4.55.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)
|
||||
|
||||
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
|
||||
|
||||
+64
-12
@@ -34,10 +34,18 @@ const server = createMcpApp({
|
||||
await server.connect(new StdioServerTransport());
|
||||
```
|
||||
|
||||
### Client (React, inside iframe)
|
||||
### Client (inside iframe)
|
||||
|
||||
Framework-specific adapters connect to the MCP host and maintain the current spec:
|
||||
|
||||
- `@json-render/mcp/app/react` -- `useJsonRenderApp()` React hook
|
||||
- `@json-render/mcp/app/vue` -- `useJsonRenderApp()` Vue composable
|
||||
- `@json-render/mcp/app/svelte` -- `createJsonRenderApp()` Svelte stores
|
||||
|
||||
#### React
|
||||
|
||||
```tsx
|
||||
import { useJsonRenderApp } from "@json-render/mcp/app";
|
||||
import { useJsonRenderApp } from "@json-render/mcp/app/react";
|
||||
import { JSONUIProvider, Renderer } from "@json-render/react";
|
||||
|
||||
function McpAppView({ registry }) {
|
||||
@@ -52,12 +60,50 @@ function McpAppView({ registry }) {
|
||||
}
|
||||
```
|
||||
|
||||
#### Vue
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
import { useJsonRenderApp } from "@json-render/mcp/app/vue";
|
||||
import { Renderer } from "@json-render/vue";
|
||||
|
||||
const { spec, loading, error } = useJsonRenderApp();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="error">Error: {{ error.message }}</div>
|
||||
<div v-else-if="!spec">Waiting...</div>
|
||||
<Renderer v-else :spec="spec" :registry="registry" :loading="loading" />
|
||||
</template>
|
||||
```
|
||||
|
||||
#### Svelte
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { createJsonRenderApp } from "@json-render/mcp/app/svelte";
|
||||
import { Renderer } from "@json-render/svelte";
|
||||
import { onDestroy } from "svelte";
|
||||
|
||||
const { spec, loading, error, destroy } = createJsonRenderApp();
|
||||
onDestroy(destroy);
|
||||
</script>
|
||||
|
||||
{#if $error}
|
||||
<div>Error: {$error.message}</div>
|
||||
{:else if !$spec}
|
||||
<div>Waiting...</div>
|
||||
{:else}
|
||||
<Renderer spec={$spec} {registry} loading={$loading} />
|
||||
{/if}
|
||||
```
|
||||
|
||||
## 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
|
||||
3. The HTML resource is a Vite-bundled single-file app (React, Vue, or Svelte) with json-render renderers
|
||||
4. Inside the iframe, the framework adapter connects to the host via `postMessage` and renders specs
|
||||
|
||||
## Server API
|
||||
|
||||
@@ -65,17 +111,21 @@ function McpAppView({ registry }) {
|
||||
- `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`)
|
||||
## Client API
|
||||
|
||||
- `useJsonRenderApp(options?)` - React hook, returns `{ spec, loading, connected, error, callServerTool }`
|
||||
- `buildAppHtml(options)` - generate HTML from bundled JS/CSS
|
||||
- `buildAppHtml(options)` (`@json-render/mcp/app`) - generate HTML from bundled JS/CSS
|
||||
- `useJsonRenderApp(options?)` (`@json-render/mcp/app/react`) - React hook
|
||||
- `useJsonRenderApp(options?)` (`@json-render/mcp/app/vue`) - Vue composable
|
||||
- `createJsonRenderApp(options?)` (`@json-render/mcp/app/svelte`) - Svelte stores + `destroy()`
|
||||
|
||||
All adapters return `{ spec, loading, connected, connecting, error, app, callServerTool }`.
|
||||
|
||||
## Building the iframe HTML
|
||||
|
||||
Bundle the React app into a single self-contained HTML file using Vite + `vite-plugin-singlefile`:
|
||||
Bundle your app into a single self-contained HTML file using Vite + `vite-plugin-singlefile`. Add the appropriate framework plugin:
|
||||
|
||||
```typescript
|
||||
// vite.config.ts
|
||||
// vite.config.ts (React example)
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import { viteSingleFile } from "vite-plugin-singlefile";
|
||||
@@ -120,9 +170,11 @@ export default defineConfig({
|
||||
# Server
|
||||
npm install @json-render/mcp @json-render/core @modelcontextprotocol/sdk
|
||||
|
||||
# Client (iframe)
|
||||
npm install @json-render/react @json-render/shadcn react react-dom
|
||||
# Client (iframe) -- pick your framework
|
||||
npm install @json-render/react react react-dom # React
|
||||
npm install @json-render/vue vue # Vue
|
||||
npm install @json-render/svelte svelte # Svelte
|
||||
|
||||
# Build tools
|
||||
npm install -D vite @vitejs/plugin-react vite-plugin-singlefile
|
||||
npm install -D vite vite-plugin-singlefile
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user