MCP Apps: ツール呼び出しの中のインタラクティブUI
MCPは最初の1年間ずっとテキストプロトコルだった。ツール呼び出しはJSONまたはMarkdownを返し、クライアントは好きなようにレンダリングした。MCP Apps — 2026年1月26日にファイナルとなり、2026-07-28コア仕様に組み込まれた最初の公式拡張 — がUIチャンネルを追加する。サーバは今やHTMLの塊を配信でき、クライアントはそれをサンドボックス化されたiframeでレンダリングし、iframeは**postMessage上のJSON-RPC 2.0**で応答する。すべての書き込みは、他のツール呼び出しと同じ監査パスを通る。「Claudeの中で完全なwebappを動かす」ではなく、インタラクティブなレポート + 実行前確認と考えるとよい。
- MCP Appsが実際に何であるか — そして意図的にそうではない4つのこと
- ワイヤ形状:ケイパビリティ・ネゴシエーション、ui://リソース、ツールの _meta.ui リンク、そしてpostMessageブリッジ
- セキュリティモデル — サンドボックス、CSP、Permission Policy、ホスト承認 — そしてそれでも残る露出面
- プレーンなツール結果ではなくAppsに手を伸ばすべきとき(稀ではあるが価値のあるパターン)
- これがあなたが既に話している2026-07-28ステートレス仕様の他の部分とどのように組み合わさるか
一段落バージョン
サーバは ui://<server>/<name> URIで1つ以上のUIリソースをMIMEタイプ text/html;profile=mcp-app で宣言する。ツールはスキーマに _meta.ui.resourceUri を付けることで「UIがある」と広告する。tools/call の際、サーバは通常のテキスト/データに加えてUIリソースへの参照を返せる。クライアントは resources/read でHTMLを取得し、サンドボックス化された <iframe> に入れ、iframeとホストはJSON-RPC 2.0を使って postMessage で会話する。UI起点のツール呼び出しも、他のツール呼び出しと同じユーザ承認を必要とする — Appはレンダリングでき、提案でき、しかしサイレントに実行することはできない。
MCP Appsがそうではないもの
- 完全なwebappランタイムではない。iframeはデフォルトでネットワークなし(`connect-src 'none'`)、トップレベルナビゲーションなし、サードパーティスクリプトなし。もしアイデアがCDNからReactをロードして自分のAPIを呼ぶ必要があるなら、これはそれではない。
- ツール呼び出しの承認をバイパスする方法ではない。何かを変更するUIアクションは、依然として通常のJSON-RPCツール呼び出しとして流れ、ホストがログ、スロットル、明示的なユーザ同意を要求できる。
- サニタイゼーションではない。サンドボックスはUIが何をできるかを制限するのであって、ユーザに何をタイプさせるように仕向けるかを制限しない。すべてのAppをサードパーティコードのように扱い、そもそもどのサーバがUIをレンダリングできるかを許可リスト化すること。
- 永続的ではない。セッションはなく、ツール呼び出しが終わればiframeは消える。呼び出し間で必要な状態は、サーバ側のハンドルとして返すことで保持する — ステートレス仕様の他の部分と全く同じ。
4つの可動部品
- クライアントはリクエストごとの _meta.capabilities の中で、リバースDNS名前空間 io.modelcontextprotocol/ui の下に拡張を広告し、レンダリング可能なMIMEタイプを列挙する。そのケイパビリティを見ないサーバは単にUIチャンネルをスキップしてプレーンな結果を返す — 拡張全体は両側でオプトインだ。
- サーバは ui://<server>/<name> URIで1つ以上のリソースをMIME text/html;profile=mcp-app で公開する。リソースは事前宣言されるので、ホストはツールが実行される前にプリフェッチ、ハッシュ、レビューできる。コンテンツは通常の resources/read 呼び出しで提供される — 新しいエンドポイントはない。
- UIをレンダリングしたい任意のツールは、スキーマに _meta.ui = { resourceUri: 'ui://…', visibility: ['model' | 'app'] } を付ける。visibilityはモデルがUI参照を「見る」ことができるか(UIアフォーダンスを推論するモデルに有用)、それともapp専用かを制御する。
- サンドボックス化されたiframeの中で、UIコードは window.parent に対して postMessage 経由でJSON-RPC 2.0を話す。最初のメッセージは通常 ui/initialize で、ツール引数とサーバが渡した任意のハンドルを含む。そこからUIは追加のリソースを要求したり、(ユーザ承認付きで)ツールを呼び出したり、通知を返送したりできる。
実際にタイプするワイヤ形状
クライアントリクエストでのケイパビリティ広告:
{
"jsonrpc": "2.0",
"method": "tools/call",
"params": { "name": "get_weather", "arguments": { "location": "Milan" } },
"_meta": {
"capabilities": {
"extensions": {
"io.modelcontextprotocol/ui": {
"mimeTypes": ["text/html;profile=mcp-app"]
}
}
}
}
}
サーバ側のツールスキーマ:
{
"name": "get_weather",
"description": "Get current weather and a 7-day forecast for a location.",
"inputSchema": {
"type": "object",
"properties": { "location": { "type": "string" } },
"required": ["location"]
},
"_meta": {
"ui": {
"resourceUri": "ui://weather-server/dashboard-template",
"visibility": ["model", "app"]
}
}
}
その隣に宣言されるUIリソース:
{
"uri": "ui://weather-server/dashboard-template",
"mimeType": "text/html;profile=mcp-app",
"_meta": {
"ui": {
"connectDomains": [],
"permissions": []
}
}
}
そしてiframeが返す最初のメッセージ:
UI → ホスト:postMessage上のJSON-RPC 2.0
window.parent.postMessage(
{
jsonrpc: "2.0",
id: 1,
method: "ui/initialize",
params: {
toolName: "get_weather",
toolArguments: { location: "Milan" },
toolResult: /* whatever the server returned alongside the UI */
}
},
"*"
);その後のすべて — ホストに別のツール呼び出しをさせるよう頼む、リソースを購読する、UI側のイベントを送信する — は、同じチャンネル上のさらなるJSON-RPC 2.0であり、サーバ側で既に話しているワイヤプロトコルと同一の構造だ。これが要点だ:UI開発者はカスタムシムを学ぶのではなく、標準の @modelcontextprotocol/sdk を使える。
1画面で見るセキュリティモデル
- サンドボックス:コンテンツはサンドボックス化された <iframe> でレンダリングされる。デフォルト:トップレベルナビゲーションなし、任意オリジンへのフォームなし、ポップアップなし、プラグインなし。
- デフォルトCSP:connect-src 'none'。UIはどこにも fetch() できない。特定のオリジンを許可するには、サーバがリソースの _meta.ui.connectDomains でそれらを宣言し、ホストがそのリストからCSPヘッダを構築する。
- Permission Policy:リソースの _meta.ui.permissions がiframeの allow 属性にマップされる — camera、microphone、geolocation、clipboard-write。何も暗黙的に付与されない。
- すべてのUI起点の書き込みは通常のツール呼び出しだ。ホストはそれを検証し、ユーザ承認を要求することがあり、モデル起点の呼び出しと同じ監査ストリームにログする。
- テンプレートはプリフェッチ可能でハッシュ可能。特定のハッシュにピン留めするホストは、下でテンプレートが差し替えられる攻撃を検出できる。しないホストは、その日サーバが配信するものを何でも受け取る。
実際にAppに手を伸ばすべきとき
意図的にハードルは高い — すべてのAppはあなたにとってクライアントの表面積であり、ユーザにとって攻撃面だ。UIがチャットのターンを明確に上回るときにだけ手を伸ばすこと:
- データグリッドでの確認。「これから更新する47行だ — 不要なものはチェックを外して。」チャット表示だと巨大か不誠実になる。チェックボックス付きグリッドは誠実で速い。
- チャート駆動の承認。「クエリプラン / コスト予測 / トレース・ウォーターフォールだ。承認または拒否。」チャートはHTMLでは安く、ASCIIでは酷い。
- 構造化ピッカー — 入力の形がテキストではないもの。ミニカレンダー付き日付範囲、ファイルシステムや組織図上のツリーセレクト、境界ボックス付きマップピッカー。
- インプレースエディタ — 全体の答えを再生成するよりdiff表示 + 承認/拒否が優れる小さなステップのため。
もしアイデアが「ダッシュボード全体を埋め込みたい」ならここには属さない — リンクアウトすること。もしアイデアが「サンドボックス内で任意のユーザコードを実行したい」なら、絶対にここには属さない。
2025-11-25互換性コントラクト
MCP Appsは2026-07-28コア仕様で導入された拡張フレームワークに乗るが、拡張自体は2026-01-26以来ファイナルだ。実務的な含意:古い2025-11-25リビジョン + この拡張だけを話すクライアントも既にAppsをレンダリングできる。ステートレスな2026-07-28クライアントは、リクエストごとのケイパビリティマップの1エントリとしてそれを拾う。拡張が両側でオプトインなので、それを追加するサーバは古いクライアントを決して壊さない — 単にUI分岐をスキップするだけだ。これはすべての将来のMCP拡張が取る形なので、このハンドシェイクを理解することは、来たるものへの負債を返済する。
MCP AppsがAILmanacマップのどこに位置するか
- MCP 2026-07-28: ステートレス仕様 — Appsが乗るコアプロトコル。特に拡張フレームワークのセクション(SEP-2133)。
- MCPとツール接続 — API側のコネクタ。iframeをレンダリングするのはクライアントであり、今日のコネクタはトランスポートを抽象化しているので、Appsは変更なしで流れる。
- Claude CodeにおけるMCP — Claude CodeのMCPサポートが向かう先。UIレンダリングはクライアントのケイパビリティであり、サーバのケイパビリティではないので、これはどのAppsが実際にレンダリングされるかを決める面だ。
- MCPサーバのセキュア化 — iframeの向こう側にあるサーバのハードニングパターン。
クイックチェック
Check yourself
0/4用語集
ソースとさらなる読み物
- SEP-1865: MCP Apps — Interactive User Interfaces for MCP — 仕様そのもの。
ext-appsリポジトリ — 凍結された2026-01-26仕様テキスト:ケイパビリティ形状、CSPデフォルト、postMessageセマンティクス。- MCP Apps: Extending servers with interactive user interfaces — 設計の根拠を辿るアナウンスメント投稿。
- The 2026-07-28 MCP Specification Release Candidate — Appsが一緒に出荷されるコアリリース。拡張フレームワークのセクションが、すべての拡張が今後従うネゴシエーションパターンを説明する。
- Anthropic and OpenAI Join Forces to Standardize Interactive AI Interfaces with MCP Apps — SEP-1865の背後にあるマルチベンダー協力の背景。
次に
- MCP 2026-07-28: ステートレス仕様 — すべてのMCP Appsホストとサーバもまた話すコアプロトコル。
- MCPサーバのセキュア化 — UI起点のツール呼び出しを受け入れる前に用意しておきたいサーバ側のハードニング。
- MCPとツール接続 — Appをレンダリングしているサーバに、API側のコネクタが実際にどのように到達するか。