MCP Appsにおける「単一HTML」の技術的背景
「MCP Apps(SEP-1865)」は、従来のテキストや構造化データによる応答を超え、AIチャット画面内にHTMLベースのインタラクティブなUI(View)を直接埋め込むための拡張仕様として、2026年1月26日に正式リリースされました。この仕様において、UIを提供するリソースは最終的に「単一のHTML」として配信される構成が極めて有効であり、実質的なシステム制限となっています。
このMCP AppsのUI開発において、なぜマルチファイル構成ではなくアセットを1つにまとめた「単一HTML」が求められるのか、その技術的背景と設計判断のトレードオフを解説します。
1. 配信プロトコルと厳格なサンドボックス制限
AIクライアント(ホスト)は、サーバーからのレスポンスに含まれる ui:// スキームを検出します。そこから、MIMEタイプが text/html;profile=mcp-app または text/html+mcp に指定されたUIリソースを取得します。
ホストは取得したHTMLを、セキュリティ確保のためにホストのDOM、Cookie、ローカルストレージへのアクセスが完全に遮断された**「サンドボックス化された iframe」**内にレンダリングします。もしUIが複数のCSSやJS、画像などの外部ファイルを相対パスで読み込む設計になっていると、この厳格に隔離されたサンドボックス環境下において依存アセットを正しく解決・ロードすることが極めて困難になります。これが「単一HTML」の構成を採るべき最大の理由です。
2. セキュリティとデータ窃取の防止
MCP Appsの大きな特徴は、**「UI(View)とサーバーは直接通信しない」**という基本設計思想にあります。iframe内のUIはサーバーと直接通信せず、すべての通信を仲介役であるホスト(AIクライアント)を介してプロキシする形で実行します。これにより、悪意ある操作や外部への意図しないデータ窃取(exfiltration)をホスト側で検閲・ブロックできるようになっています。
アセットがすべてインライン化された単一HTMLであれば、UI表示時における外部への追加アセットリクエストが発生しません。サーバー側があらかじめ宣言したCSP(Content Security Policy)に沿って、ホストが通信を厳格に制限する上でも、単一HTML構成は非常に親和性が高い設計と言えます。
Viteとプラグインによる単一HTMLのビルド構成
ReactやVueなどのコンポーネント指向フレームワークを使用する場合、通常はビルドプロセスにおいてパフォーマンス最適化のためにJSやCSSが分割(コードスプリッティング)されます。これを「単一HTML」に強制的にバンドルするために、Viteと vite-plugin-singlefile を組み合わせた開発環境が強力なソリューションとなります。
以下は、このビルド構成を実現するための最小限のセットアップ手順です。
1. 必要なパッケージの導入
Viteプロジェクトに vite-plugin-singlefile と、公式のフロントエンド用SDKである @modelcontextprotocol/ext-apps を導入します。
2. vite.config.tsの設定とインライン化の原理
Viteの設定ファイルにおいて、ビルド時にすべてのJS、CSS、アセットをHTML内にインライン化するようにプラグインを配置します。
import { defineConfig } from "vite";
import { viteSingleFile } from "vite-plugin-singlefile";
export default defineConfig({
plugins: [viteSingleFile()],
build: {
// インライン化を確実に行うためのアセットサイズ制限の事実上の解除設定
assetsInlineLimit: 100000000,
chunkSizeWarningLimit: 100000000,
},
});
この設定を適用して npm run build を実行することで、生成されるJSやCSS、その他の静的アセット(Base64エンコードされた画像など)がすべて dist/index.html の内部にインライン展開され、ファイル1つだけで完結する軽量なUIが完成します。
公式SDKを用いた双方向通信のミニマル実装
単一HTMLとしてビルドされたUIは、ホスト(AIクライアント)を仲介し、postMessage を介した JSON-RPC 通信によってサーバーとの双方向通信を実行します。公式提供されているSDKパッケージである @modelcontextprotocol/ext-apps を使用することで、低レイヤーの通信処理を記述することなく、直感的なコードで通信を制御できます。
1. UIからサーバーツールの呼び出し
以下は、UI側のボタンをクリックした際に、バックエンドのMCPサーバーが提供する特定のツールを呼び出すミニマルなフロントエンド側の実装例です。
import { createMcpClient } from "@modelcontextprotocol/ext-apps";
// MCPクライアントの初期化
const mcp = createMcpClient();
async function handleAction() {
try {
// サーバー側の特定のツールを引数付きで呼び出す
const response = await mcp.callServerTool("get-data", {
parameter: "example"
});
// 取得した結果をUIに反映
const resultArea = document.getElementById("result");
if (resultArea) {
resultArea.textContent = JSON.stringify(response);
}
} catch (error) {
console.error("サーバーツールの呼び出しに失敗しました:", error);
}
}
document.getElementById("action-btn")?.addEventListener("click", handleAction);
2. 双方向のコンテキスト更新
ユーザーがUI上で行った操作(フォーム入力や特定項目の選択など)をAI側に同期させたい場合、updateModelContext を使用します。これにより、AIは「今ユーザーがどのような画面操作を行っているか」という最新のコンテキストを動的に把握し、的確な次の対話を提供できます。
async function onUserInteract(stateData: any) {
// AI側のコンテキストを動的に更新する
await mcp.updateModelContext({
state: stateData,
description: "ユーザーがUI上の特定のデータをマークしました。"
});
}
段階的強化とセキュリティ運用の設計思想
MCP Apps UIを導入・設計するにあたっては、以下の2つの基本思想がベストプラクティスとされています。
1. 段階的強化(Progressive Enhancement)
すべてのAIクライアントが最新のMCP AppsによるUI表示に対応しているわけではありません。UIをサポートしない非対応クライアントで実行された場合でも、システムが破綻(クラッシュ)することなく、フォールバックとして従来の**「テキスト応答」を返す後方互換性**を維持する必要があります。そのため、バックエンド側にはあらかじめ代替となるテキスト応答を含める設計が不可欠です。
2. ガバナンスとユーザーの同意
UIから「外部ツールへの投稿」や「メッセージ送信」などの重要な操作をトリガーする場合、安全な運用の観点から、勝手に処理が実行される設計にしてはなりません。実行前に必ずプレビューをUIに提示し、**ユーザーによる明示的な確認・同意(オプトイン)**を必須とするプロセスをフローに組み込むことが重要です。