mcpField
C
create-mcp-app

create-mcp-app

github.com/sammcj開発16736 回インストール2 日前に更新
リポジトリ

create-mcp-app について

create-mcp-appは、MCP対応ホスト内で動くインタラクティブUIを開発するためのスキルです。MCPツールとHTMLリソースの連携方法を、複数のフレームワークの例とともに説明します。

主な機能

  1. 01ツールとHTMLリソースの連携
  2. 02React、Vue、Svelteのサンプル
  3. 03ライフサイクルとホストスタイルの処理
  4. 04入力の段階的な描画
  5. 05表示状態と全画面モードの管理

活用シーン

  1. 01既存のMCP サーバーへのUI追加
  2. 02フレームワークテンプレートからのMCP App作成
  3. 03生成中のツール入力のプレビュー
  4. 04非表示時のポーリングやアニメーション停止

MCP 対応

MCPホストがツールを呼び出してインタラクティブUIを表示できるよう、MCPツールとHTMLリソースを連携させる方法を案内します。

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/sammcj/agentic-coding --skill create-mcp-app

インストール後、Claude Code で/create-mcp-appを入力して読み込まれたか確認します。

詳細

MCP アプリの作成

Claude Desktop などの MCP 対応ホスト内で動作するインタラクティブな UI を構築します。MCP アプリは MCP ツールと HTML リソースを組み合わせて、リッチでインタラクティブなコンテンツを表示します。

基本概念:ツール + リソース

すべての MCP アプリには、相互に関連付けられた次の 2 つの要素が必要です。

  1. ツール - LLM/ホストによって呼び出され、データを返します
  2. リソース - データを表示するバンドル済み HTML UI を提供します
  3. リンク - ツールの _meta.ui.resourceUri がリソースを参照します
Host calls tool → Server returns result → Host renders resource UI → UI receives result

クイックスタートの判断ツリー

フレームワークの選択
フレームワークSDK のサポート最適な用途
ReactuseApp フックが提供されていますReact に慣れているチーム
Vanilla JSライフサイクルを手動で管理しますビルドの複雑さを避けたいシンプルなアプリ
Vue/Svelte/Preact/Solidライフサイクルを手動で管理します好みのフレームワーク

ユーザーが Golang または Rust で MCP サーバーを作成したい場合は、その言語で特定のフレームワークを希望しているか確認してください。希望がなければ、MCP アプリをサポートする適切なフレームワーク/ライブラリを探すのを支援してください。

プロジェクトの状況

既存の MCP サーバーに追加する場合:

  • SDK から registerAppTool と registerAppResource をインポートします
  • _meta.ui.resourceUri を指定してツール登録を追加します
  • バンドル済み HTML を提供するリソース登録を追加します

新しい MCP サーバーを作成する場合:

  • トランスポート(stdio または HTTP)を使用してサーバーをセットアップします
  • ツールとリソースを登録します
  • vite-plugin-singlefile を使用してビルドシステムを設定します

参考コードの取得

SDK リポジトリをクローンして、動作するサンプルと API ドキュメントを確認します。

git clone --branch "v$(npm view @modelcontextprotocol/ext-apps version)" --depth 1 https://github.com/modelcontextprotocol/ext-apps.git /tmp/mcp-ext-apps
フレームワークのテンプレート

/tmp/mcp-ext-apps/examples/basic-server-{framework}/ を参考にして、コードを学び、活用してください。

テンプレート主なファイル
basic-server-vanillajs/server.ts、src/mcp-app.ts、mcp-app.html
basic-server-react/server.ts、src/mcp-app.tsx(useApp フックを使用)
basic-server-vue/server.ts、src/App.vue
basic-server-svelte/server.ts、src/App.svelte
basic-server-preact/server.ts、src/mcp-app.tsx
basic-server-solid/server.ts、src/mcp-app.tsx

各テンプレートには次の内容が含まれています。

  • registerAppTool と registerAppResource を含む完全な server.ts
  • すべてのライフサイクルハンドラーを備えたクライアント側アプリ
  • vite-plugin-singlefile を使用する vite.config.ts
  • 必要な依存関係をすべて含む package.json
  • node_modules/ と dist/ を除外する .gitignore
API リファレンス(ソースファイル)

/tmp/mcp-ext-apps/src/ から JSDoc ドキュメントを直接確認してください。

ファイル内容
src/app.tsApp クラス、ハンドラー(ontoolinput、ontoolresult、onhostcontextchanged、onteardown)、ライフサイクル
src/server/index.tsregisterAppTool、registerAppResource、ツールの表示オプション
src/spec.types.tsすべての型定義:McpUiHostContext、CSS 変数のキー、表示モード
src/styles.tsapplyDocumentTheme、applyHostStyleVariables、applyHostFonts
src/react/useApp.tsxReact アプリ向けの useApp フック
src/react/useHostStyles.tsuseHostStyles、useHostStyleVariables、useHostFonts フック
高度な例
例実証するパターン
examples/shadertoy-server/ストリーミングによる部分入力 + 表示状態に応じた一時停止/再生(大きな入力に適したベストプラクティス)
examples/wiki-explorer-server/インタラクティブなデータ取得のための callServerTool
examples/system-monitor-server/インターバル管理を伴うポーリングパターン
examples/video-resource-server/バイナリ/Blob リソース
examples/sheet-music-server/ontoolinput - 実行完了前にツール引数を処理する
examples/threejs-server/ontoolinputpartial - ストリーミング/段階的なレンダリング
examples/map-server/updateModelContext - UI の状態をモデルに伝える
examples/transcript-server/updateModelContext + sendMessage - バックグラウンドでのコンテキスト更新 + ユーザーが開始するメッセージ
examples/basic-host/AppBridge を使用したホスト実装のリファレンス

実装に関する重要な注意事項

依存関係の追加

バージョン番号を手動で記述せず、npm install を使って依存関係を追加してください。

npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/sdk zod

これにより、npm が互換性のある最新バージョンを解決します。記憶に頼ってバージョン番号を指定しないでください。

TypeScript サーバーの実行

TypeScript サーバーファイルを実行するには、devDependency として tsx を使用してください。

npm install -D tsx
"scripts": {
  "serve": "tsx server.ts"
}

注:SDK の例では bun を使用していますが、生成されるプロジェクトでは、より幅広い互換性を確保するため tsx を使用してください。

ハンドラーの登録順序

app.connect() を呼び出す前に、すべてのハンドラーを登録してください。

const app = new App({ name: "My App", version: "1.0.0" });

// Register handlers first
app.ontoolinput = (params) => { /* handle input */ };
app.ontoolresult = (result) => { /* handle result */ };
app.onhostcontextchanged = (ctx) => { /* handle context */ };
app.onteardown = async () => { return {}; };

// Then connect
await app.connect();
ツールの表示設定

_meta.ui.visibility を使って、ツールにアクセスできるユーザーを制御します。

// Default: visible to both model and app
_meta: { ui: { resourceUri, visibility: ["model", "app"] } }

// UI-only (hidden from model) - for refresh buttons, form submissions
_meta: { ui: { resourceUri, visibility: ["app"] } }

// Model-only (app cannot call)
_meta: { ui: { resourceUri, visibility: ["model"] } }
ホストのスタイル設定との連携

Vanilla JS - ヘルパー関数を使用します。

import { applyDocumentTheme, applyHostStyleVariables, applyHostFonts } from "@modelcontextprotocol/ext-apps";

app.onhostcontextchanged = (ctx) => {
  if (ctx.theme) applyDocumentTheme(ctx.theme);
  if (ctx.styles?.variables) applyHostStyleVariables(ctx.styles.variables);
  if (ctx.styles?.css?.fonts) applyHostFonts(ctx.styles.css.fonts);
};

React - フックを使用します。

import { useApp, useHostStyles } from "@modelcontextprotocol/ext-apps/react";

const { app } = useApp({ appInfo, capabilities, onAppCreated });
useHostStyles(app); // Injects CSS variables to document, making var(--*) available

CSS での変数の使用 - 適用後、var() を使用します。

.container {
  background: var(--color-background-secondary);
  color: var(--color-text-primary);
  font-family: var(--font-sans);
  border-radius: var(--border-radius-md);
}
.code {
  font-family: var(--font-mono);
  font-size: var(--font-text-sm-size);
  line-height: var(--font-text-sm-line-height);
  color: var(--color-text-secondary);
}
.heading {
  font-size: var(--font-heading-lg-size);
  font-weight: var(--font-weight-semibold);
}

主な変数グループ:--color-background-*、--color-text-*、--color-border-*、--font-sans、--font-mono、--font-text-*-size、--font-heading-*-size、--border-radius-*。一覧は src/spec.types.ts を参照してください。

セーフエリアの処理

常に safeAreaInsets を考慮してください。

app.onhostcontextchanged = (ctx) => {
  if (ctx.safeAreaInsets) {
    const { top, right, bottom, left } = ctx.safeAreaInsets;
    document.body.style.padding = `${top}px ${right}px ${bottom}px ${left}px`;
  }
};
ストリーミングによる部分入力

大きなツール入力では、LLM による生成中に進捗を表示するため、ontoolinputpartial を使用してください。部分的な JSON は補完され(常に有効な形式になります)、段階的な UI 更新が可能になります。

仕様: ui/notifications/tool-input-partial

app.ontoolinputpartial = (params) => {
  const args = params.arguments; // Healed partial JSON - always valid, fields appear as generated
  // Use args directly for progressive rendering
};

app.ontoolinput = (params) => {
  // Final complete input - switch from preview to full render
};

使用例:

パターン例
コードプレビューストリーミング中のコードを <pre> に表示し、完了後にレンダリングする(examples/shadertoy-server/)
段階的なフォーム入力ストリーミングに合わせてフォームフィールドに入力する
ライブチャート配列が大きくなるたびにチャートへデータポイントを追加する
部分レンダリング未完成の構造化データ(テーブル、リスト、ツリー)をレンダリングする

シンプルなパターン(コードプレビュー):

app.ontoolinputpartial = (params) => {
  codePreview.textContent = params.arguments?.code ?? "";
  codePreview.style.display = "block";
  canvas.style.display = "none";
};
app.ontoolinput = (params) => {
  codePreview.style.display = "none";
  canvas.style.display = "block";
  render(params.arguments);
};
表示状態に応じたリソース管理

ビューがビューポートから外れたら、負荷の高い処理(アニメーション、WebGL、ポーリング)を一時停止します。

const observer = new IntersectionObserver((entries) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      animation.play(); // or: startPolling(), shaderToy.play()
    } else {
      animation.pause(); // or: stopPolling(), shaderToy.pause()
    }
  });
});
observer.observe(document.querySelector(".main"));
全画面モード

app.requestDisplayMode() で全画面表示をリクエストします。ホストのコンテキストで利用可能かどうかを確認してください。

let currentMode: "inline" | "fullscreen" = "inline";

app.onhostcontextchanged = (ctx) => {
  // Check if fullscreen available
  if (ctx.availableDisplayModes?.includes("fullscreen")) {
    fullscreenBtn.style.display = "block";
  }
  // Track current mode
  if (ctx.displayMode) {
    currentMode = ctx.displayMode;
    container.classList.toggle("fullscreen", currentMode === "fullscreen");
  }
};

async function toggleFullscreen() {
  const newMode = currentMode === "fullscreen" ? "inline" : "fullscreen";
  const result = await app.requestDisplayMode({ mode: newMode });
  currentMode = result.mode;
}

CSS パターン - 全画面表示では角丸をなくします。

.main { border-radius: var(--border-radius-lg); overflow: hidden; }
.main.fullscreen { border-radius: 0; }

実装全体については、examples/shadertoy-server/ を参照してください。

避けるべきよくある間違い

  1. connect() の後にハンドラーを登録する - app.connect() を呼び出す前に、すべてのハンドラーを登録してください
  2. 単一ファイルへのバンドルを忘れる - vite-plugin-singlefile を必ず使用してください
  3. リソースの登録を忘れる - ツールとリソースの両方を登録する必要があります
  4. resourceUri リンクがない - ツールに _meta.ui.resourceUri が必要です
  5. セーフエリアのインセットを無視する - 常に ctx.safeAreaInsets を処理してください
  6. テキストによるフォールバックがない - UI 非対応のホスト向けに、必ず content 配列を指定してください
  7. スタイルのハードコード - テーマとの連携にはホストの CSS 変数を使用してください
  8. 大きな入力でストリーミングを使わない - 生成中に進捗を表示するため、ontoolinputpartial を使用してください

テスト

basic-host の使用

basic-host の例を使って、MCP Apps をローカルでテストします。

# Terminal 1: Build and run your server
npm run build && npm run serve

# Terminal 2: Run basic-host (from cloned repo)
cd /tmp/mcp-ext-apps/examples/basic-host
npm install
SERVERS='["http://localhost:3001/mcp"]' npm run start
# Open http://localhost:8080

SERVERS にサーバー URL の JSON 配列を設定します(デフォルト:http://localhost:3001/mcp)。

sendLog によるデバッグ

デバッグログをホストアプリケーションに送信します(iframe の開発者コンソールだけに送信するのではありません)。

await app.sendLog({ level: "info", data: "Debug message" });
await app.sendLog({ level: "error", data: { error: err.message } });

コメント