create-mcp-app について
create-mcp-appは、MCP対応ホスト内で動くインタラクティブUIを開発するためのスキルです。MCPツールとHTMLリソースの連携方法を、複数のフレームワークの例とともに説明します。
主な機能
- 01ツールとHTMLリソースの連携
- 02React、Vue、Svelteのサンプル
- 03ライフサイクルとホストスタイルの処理
- 04入力の段階的な描画
- 05表示状態と全画面モードの管理
活用シーン
- 01既存のMCP サーバーへのUI追加
- 02フレームワークテンプレートからのMCP App作成
- 03生成中のツール入力のプレビュー
- 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 つの要素が必要です。
- ツール - LLM/ホストによって呼び出され、データを返します
- リソース - データを表示するバンドル済み HTML UI を提供します
- リンク - ツールの
_meta.ui.resourceUriがリソースを参照します
Host calls tool → Server returns result → Host renders resource UI → UI receives result
クイックスタートの判断ツリー
フレームワークの選択
| フレームワーク | SDK のサポート | 最適な用途 |
|---|---|---|
| React | useApp フックが提供されています | 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.ts | App クラス、ハンドラー(ontoolinput、ontoolresult、onhostcontextchanged、onteardown)、ライフサイクル |
src/server/index.ts | registerAppTool、registerAppResource、ツールの表示オプション |
src/spec.types.ts | すべての型定義:McpUiHostContext、CSS 変数のキー、表示モード |
src/styles.ts | applyDocumentTheme、applyHostStyleVariables、applyHostFonts |
src/react/useApp.tsx | React アプリ向けの useApp フック |
src/react/useHostStyles.ts | useHostStyles、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/ を参照してください。
避けるべきよくある間違い
- connect() の後にハンドラーを登録する -
app.connect()を呼び出す前に、すべてのハンドラーを登録してください - 単一ファイルへのバンドルを忘れる -
vite-plugin-singlefileを必ず使用してください - リソースの登録を忘れる - ツールとリソースの両方を登録する必要があります
- resourceUri リンクがない - ツールに
_meta.ui.resourceUriが必要です - セーフエリアのインセットを無視する - 常に
ctx.safeAreaInsetsを処理してください - テキストによるフォールバックがない - UI 非対応のホスト向けに、必ず
content配列を指定してください - スタイルのハードコード - テーマとの連携にはホストの CSS 変数を使用してください
- 大きな入力でストリーミングを使わない - 生成中に進捗を表示するため、
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 } });