Vercel の json-render で Claude の MCP Apps UI を作ってみた

Vercel の json-render で Claude の MCP Apps UI を作ってみた

json-renderはVercel Labsが公開したGenerative UIフレームワークで、Zodベースのコンポーネントカタログを定義するだけでMCP Apps上にリッチなUIを生成できます。createMcpApp()1行でサーバー構築が完了し、shadcn/uiの36コンポーネントがすぐ利用可能。従来のHTML手書き開発と比べ、型安全かつ大幅に効率化されたMCP Apps UI開発体験を実現します。
2026.03.23

はじめに

MCP Apps(Model Context Protocol Apps)は、AI チャットクライアント内にインタラクティブな UI を直接表示できる仕組みです。しかし実際に開発するとなると、HTML/CSS/JS をゼロから書いてシングルファイルにバンドルし、MCP サーバーに登録する……というステップが必要で、地味に手間がかかります。

そこで登場するのが json-render です。Vercel Labs が公開した Generative UI(生成的 UI)フレームワークで、「AI が自然言語から UI を生成し、あらかじめ定義したコンポーネントカタログの範囲内でレンダリングする」というコンセプトのライブラリです。Generative UI とは、LLM がテキストだけでなくリッチな UI コンポーネントを動的に生成・表示するアプローチのことです。

json-render には @json-render/mcp パッケージが用意されており、MCP Apps との統合がとてもスムーズにできます。今回はこの MCP サンプルを実際にビルドして Claude Desktop で動かすところまで試してみました。

対象読者: MCP Apps の基本は知っていて、UI 開発をもっと効率化したいエンジニア

json-render とは

json-render は、AI が生成する UI を「JSON スペック」として表現し、事前に定義したコンポーネントカタログの範囲内でレンダリングするフレームワークです。Generative UI(生成的 UI)という概念を Vercel が提唱しており、そのリファレンス実装にあたります。

特徴を3つに絞ると以下の通りです。

特徴 説明
ガードレール付き AI はカタログに定義されたコンポーネントしか使えない。意図しない HTML の注入を防止
クロスプラットフォーム 同じカタログ定義で React, Vue, Svelte, Solid, React Native に対応
プログレッシブレンダリング LLM のストリーミング出力に合わせて UI を段階的に描画

対応するレンダラーは非常に豊富です。Web フレームワーク(React / Vue / Svelte / Solid)だけでなく、React Native(モバイル)、React Three Fiber(3D)、Remotion(動画)、React PDF、React Email まで揃っています。

3ステップのアーキテクチャ

json-render は以下の3ステップで動作します。

1. カタログ定義 ──── どのコンポーネントが使えるか(Zodスキーマで定義)

2. JSON スペック生成 ── LLM がカタログに沿って UI 仕様を JSON で出力

3. レンダリング ──── Renderer がスペックをコンポーネントツリーに変換して描画

json-render × MCP Apps の仕組み

json-render の @json-render/mcp パッケージは、上記の3ステップを MCP Apps のアーキテクチャにきれいに乗せています。

┌──────────────┐  ①カタログをプロンプトに変換  ┌───────────────┐
│ json-render  │ ──────────────────────────►  │  LLM          │
│ カタログ定義  │                               │ (Claude等)    │
└──────────────┘                               └───────┬───────┘
                                                       │ ②JSONスペックを生成

┌──────────────┐  ③ render-ui ツール結果       ┌───────────────┐
│ MCP Apps     │ ◄──── postMessage ────────── │  Host         │
│ iframe内のUI  │                               │ (Claude Desktop) │
└──────────────┘                               └───────────────┘

       ▼ ④ Renderer がスペックを shadcn/ui コンポーネントに変換
  ┌──────────────────┐
  │ インタラクティブUI  │
  │ (テーブル, カード等) │
  └──────────────────┘

ポイントは createMcpApp() という関数です。カタログと HTML を渡すだけで、MCP サーバーのセットアップが完了します。

import { createMcpApp } from "@json-render/mcp";

const server = await createMcpApp({
  name: "json-render Example",
  version: "1.0.0",
  catalog,  // コンポーネントカタログ
  html,     // Vite でバンドルしたシングル HTML
});

内部では以下が自動的に行われます。

  1. catalog.prompt() でカタログ定義を LLM 向けのプロンプト文字列に変換
  2. render-ui という名前の MCP ツールを登録(description にカタログプロンプトを埋め込み)
  3. ui://render-ui/view.html というリソースを登録(バンドル済み HTML を返す)

つまり、LLM はツールの description からどんなコンポーネントが使えるかを理解し、JSON スペックを生成してツール結果として返します。ホストがそれを iframe 内の UI に渡し、json-render の Renderer がリッチな UI に変換する、という流れです。

実際にセットアップして動かしてみる

前提条件

  • Node.js >= 20
  • pnpm(monorepo のため)
  • Claude Desktop または Cursor 等の MCP Apps 対応クライアント

リポジトリのクローンとビルド

json-render は monorepo 構成なので、まず全体をクローンして必要なパッケージをビルドします。

git clone https://github.com/vercel-labs/json-render.git
cd json-render
pnpm install

MCP サンプルは examples/mcp/ にあります。依存する内部パッケージを順番にビルドする必要がありました。

# 依存パッケージを順にビルド
pnpm --filter @json-render/core build
pnpm --filter @internal/react-state build
pnpm --filter @json-render/react build
pnpm --filter @json-render/shadcn build
pnpm --filter @json-render/mcp build

# MCP サンプルをビルド
pnpm --filter example-mcp build

ビルドが成功すると、examples/mcp/dist/index.html に約983KBのシングル HTML ファイルが生成されます。Vite の vite-plugin-singlefile プラグインにより、CSS と JS がすべてインライン化された一枚の HTML になっています。

$ ls -la examples/mcp/dist/index.html
-rw-r--r-- 1 user user 983067 Mar 18 17:38 dist/index.html

Claude Desktop での設定

claude_desktop_config.json に以下を追加します。

{
  "mcpServers": {
    "json-render": {
      "command": "npx",
      "args": ["tsx", "/Users/yourname/json-render/examples/mcp/server.ts", "--stdio"]
    }
  }
}

/Users/yourname/json-render/ の部分は、実際にリポジトリをクローンしたパスに置き換えてください。

Cursor を使う場合は .cursor/mcp.json に同様の形式で追加します。

HTTP トランスポートで起動したい場合は --stdio を外して起動するだけです。

cd examples/mcp
pnpm start
# MCP server listening on http://localhost:3001/mcp

スクリーンショット 2026-03-21 23.06.33

試しに UI を生成してもらう

Claude Desktop で「売上ダッシュボードを表示して」のように依頼すると、render-ui ツールが呼び出されます。LLM はカタログの description を参照して、使用可能な shadcn/ui コンポーネント(Card, Table, Badge, Chart 等)の組み合わせで JSON スペックを生成します。

生成される JSON スペックはこのような構造です。

{
  "root": "card-1",
  "elements": {
    "card-1": {
      "type": "Card",
      "props": { "title": "売上ダッシュボード" },
      "children": ["metric-1", "metric-2", "table-1"]
    },
    "metric-1": {
      "type": "Metric",
      "props": { "label": "今月の売上", "value": "¥12,500,000", "format": "currency" },
      "children": []
    },
    "metric-2": {
      "type": "Metric",
      "props": { "label": "前月比", "value": "+15%", "format": "percent" },
      "children": []
    },
    "table-1": {
      "type": "Table",
      "props": {
        "columns": ["商品名", "売上", "数量"],
        "rows": [
          ["商品A", "¥5,000,000", "500"],
          ["商品B", "¥4,200,000", "380"],
          ["商品C", "¥3,300,000", "290"]
        ]
      },
      "children": []
    }
  }
}

この JSON が iframe 内の Renderer に渡され、shadcn/ui の美しいコンポーネントとしてレンダリングされます。

スクリーンショット 2026-03-21 23.10.10

コードの中身を読み解く

カタログ定義(catalog.ts)

カタログは非常にシンプルです。shadcn/ui の36コンポーネントをまとめてインポートしています。

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,  // 36個の shadcn/ui コンポーネント定義
  },
  actions: {},
});

defineCatalog() は Zod スキーマベースでコンポーネントの props を定義する関数です。shadcnComponentDefinitions には Card, Table, Badge, Button, Alert, Dialog など36種類のコンポーネントが含まれており、それぞれの props スキーマと description が定義されています。

catalog.prompt() を呼ぶと、これらの定義がLLM が理解できるプロンプト文字列に変換されます。このプロンプトが render-ui ツールの description にそのまま埋め込まれるため、LLM は「どのコンポーネントがどんな props を持つか」を正確に把握できる仕組みです。

サーバー実装(server.ts)

サーバーは stdio と HTTP の両方をサポートしています。

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 startStdio() {
  const html = loadHtml();
  const server = await createMcpApp({
    name: "json-render Example",
    version: "1.0.0",
    catalog,
    html,
  });
  await server.connect(new StdioServerTransport());
}

カタログとバンドル済み HTML を渡すだけで、MCP サーバーの登録(ツール定義、リソース登録)がすべて完了します。通常の MCP Apps 開発で必要な registerApp()server.tool() を個別に書く必要がありません。

UI 側の実装(main.tsx)

iframe 内で動作する React アプリのエントリーポイントです。

import { useState, useEffect } from "react";
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,  // shadcn/ui の実コンポーネント
  },
});

function McpAppView() {
  const [spec, setSpec] = useState<Spec | null>(null);

  useEffect(() => {
    const app = new McpApp({ name: "json-render", version: "1.0.0" });

    // ツール結果(JSONスペック)を受け取ってパースする
    app.ontoolresult = (result) => {
      const parsed = parseSpec(result);
      if (parsed) setSpec(parsed);
    };

    // ホストのテーマ(ダーク/ライト)に自動追従
    app.onhostcontextchanged = (ctx) => applyHostContext(ctx);

    app.connect();
  }, []);

  if (!spec) return <div>Loading...</div>;

  return (
    <JSONUIProvider registry={registry} initialState={spec.state ?? {}}>
      <Renderer spec={spec} registry={registry} />
    </JSONUIProvider>
  );
}

ここでのポイントは以下の3つです。

  1. McpApp(ext-apps SDK) でホストとの postMessage 通信を確立
  2. ontoolresult コールバックで LLM が生成した JSON スペックを受け取り
  3. Renderer にスペックと registry を渡して描画

さらに、onhostcontextchanged でホスト側のダークモード/ライトモード設定を取得し、CSS 変数を動的に切り替えている点も実用的です。

Vite のシングルファイル設定

vite.config.ts では vite-plugin-singlefile を使ってすべてを1つの HTML にバンドルしています。

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",
    rollupOptions: {
      input: "index.html",
    },
  },
});

MCP Apps では UI を ui:// リソースとして HTML 文字列で返す必要があるため、CSS / JS をすべてインライン化する viteSingleFile() が必須です。これは MCP Apps 開発でのベストプラクティスと言えます。

json-render を使うメリット・従来との比較

従来の MCP Apps UI 開発

HTML/CSS/JSを手書き → registerApp() でリソース登録 → ツール定義を手書き
  • UI のデザインを1からコーディング
  • ツールの description にUI の使い方を手動で記述
  • LLM がどんな UI を生成するかは予測しづらい

json-render を使った場合

カタログ定義(Zod) → createMcpApp() 1行 → LLM が自動で UI を生成
  • 36種の shadcn/ui コンポーネントがすぐ使える
  • カタログ定義がそのままツールの description になるため、LLM は正確にコンポーネントを選択できる
  • スキーマバリデーションにより、不正な JSON スペックはレンダリング前に弾かれる
  • ダークモード対応、プログレッシブレンダリングもビルトイン

カタログのカスタマイズ

デフォルトでは shadcn/ui の全36コンポーネントがカタログに含まれますが、LLM のプロンプトが長くなりすぎる場合があります。必要なコンポーネントだけに絞ることで、トークン使用量を削減し、生成精度を上げることができます。

import { defineCatalog } from "@json-render/core";
import { schema } from "@json-render/react/schema";
import { shadcnComponentDefinitions } from "@json-render/shadcn/catalog";

// 必要なコンポーネントだけ選択
const { Card, Table, Badge, Metric, Button } = shadcnComponentDefinitions;

export const catalog = defineCatalog(schema, {
  components: { Card, Table, Badge, Metric, Button },
  actions: {
    export_report: { description: "Export dashboard to PDF" },
  },
});

まとめ

json-render を使うと、MCP Apps の UI 開発が「コンポーネントのカタログを定義するだけ」に近い体験になります。

今回実際に試してみて感じたポイントをまとめます。

  • createMcpApp() 1行でサーバーセットアップが完了 する手軽さは魅力的。通常の MCP Apps 開発で必要な registerApp() + server.tool() の定型コードが不要
  • Zod ベースのカタログ定義 により、LLM が生成できる UI が型安全に制約される。「何が出てくるかわからない」不安がない
  • shadcn/ui の36コンポーネント がプリビルトで使えるため、UI デザインのスキルがなくても美しいダッシュボードやフォームが生成される
  • ダークモード対応、プログレッシブレンダリング がビルトインで、ホスト側の体験が洗練されている
  • monorepo のビルドには多少手間がかかるが、npm パッケージとして利用すれば問題ない

「MCP Apps の UI をもっと手軽に、もっと美しく」したい方にはぴったりのライブラリです。React 以外のフレームワーク(Vue / Svelte / Solid)にも対応しているので、技術スタックに関わらず検討する価値があります。

参考リンク

この記事をシェアする

FacebookHatena blogX

関連記事