MCP Appsで宇宙のスライドショーを見れるMCPサーバーを実装して仕組みとユースケースを理解できたので、解説する。

なお、この記事には生成AIの出力は含まれない。

MCP Appsとは

MCP自体については以前の記事で解説したのでここでは端折る。

MCP AppsはMCPを拡張した仕様。 MCP Toolsで取得した結果をWeb UIで見たり、Web UIでMCPサーバーとやり取りできる。

このWeb UIはClaude Desktopとかのチャット画面内に埋め込まれるので、ユーザーはMCPツールの呼び出しからグラフィカルな結果の確認や操作まで、シームレスにできて便利。

MCP Appsのユースケース

MCP Appsの登場によりMCPは、ツールやリソースの機能による単なるテキストのやり取りから、AIがチャット内で専用の操作パネルやビューアを展開する形態へと進化した。

これにより、以下のようなユースケースが実現できるようになる。

  1. Visualization

    DatadogとかTableauとかPostgreSQLとか、データをためたり分析したりするサービスにMCPでクエリをかけるケースでは、その結果をテキストで返すより、MCP Appsをつかってグラフや表で見せた方が圧倒的にわかりやすい。

  2. Interactive Canvas

    CanvaとかDraw.ioとか、デザインしたり図を書いたりするサービスをMCPでAIにつないで、プロンプトで絵や図を生成する場合、自然言語で伝えきれないところがあったり気に入らないところが残ったりして、微調整や仕上げを人手でやりたくなることが多い。

    MCP Appsを使えば、チャットで指示してAIで生成されたものをチャット内に直接埋め込むことができるので、ウィンドウの切り替えなしで確認できるし、エディタを埋め込むような形にすればその場で直接調整できるので便利。

  3. Mini Console

    フライトの予約をしたり、GitHubのIssueやServiceNowのチケットを作ったり、サービスに対してなんらかの操作をするようなMCPサーバーでは、ユーザーのプロンプトでのリクエストを受けて、Elicitationとかでテキストベースのやりとりで情報を補完したりして処理を完遂することもできる。 けど、やりとりが多いと煩雑だし、テキストベースだと、例えば座席の選択とか非常にやりにくいことが想像できる。

    これをMCP Appsでちょっとした画面を表示して操作できるようにしてあげるだけで、かなりユーザビリティが上がる。

    後述のようにMCP Appsはカメラ機能も使えるので、顔認証したり、パスポートのスキャンしたり、QRコードでモバイル連携したり、テキストベースだと不可能なことも可能になる。

  4. Entertainment

    MCP AppsはWeb UIが使えるので、スライドショー見せたり、音楽流したり、動画見せたり、ユーザーを単に楽しませるような用途も考えられる。 従来のMCPツールの実用的な用途を超えて、可能性が広がる。


この他にも、MCP Appsには後述のようにチャットにメッセージやコンテキストを追加する機能もあるので、Web UIで収集した情報を使ったRAGみたいなことも考えられる。

NASA Images MCP Server

MCP AppsのEntertainmentユースケースの一例として、宇宙の画像のスライドショーを見れるMCPサーバーを作った。

NASA Images MCP Server:

nasa-images-mcp.gif


これをGitHub CopilotとかClaude Desktopにつないで、「地球の画像が見たい」のようなプロンプトを入力すると、NASA Images APIで画像を検索してMCP AppsのWeb UIで表示する。

公式MCPレジストリにも登録した。

MCP Appsの仕組み

mcpApps.svg


MCP Appsは図にするとこんな感じ。 右のMCPホストは、ChatGPTとかClaude DesktopとかのAIチャットアプリ。 それに左のMCPサーバーがつないである構成。 このMCPサーバーは、MCP Apps機能を備えたMCPツールが実装されている。

チャットでそのツールが必要になるようなプロンプトを入力すると、MCPホストがMCPサーバーのツールを呼び出して、そのレスポンスを受け取る。 このレスポンスは普通のMCPツールのレスポンスのJSON-RPCオブジェクトとほぼ同じだけど、_metaフィールドに以下のようなUIリソース情報(UIツールメタデータ)が付く。

UIツールメタデータ:

{
  ui: {
    resourceUri?: string;
    visibility?: Array<"model" | "app">;
  }
}

visibilityは誰がツールにアクセスできるかという付加的なフラグで、重要なのはresourceUriの方。 resourceUriの値はui://というスキーマのURIで、上記レスポンスを返したMCPサーバー上のMCPリソースのURI。

MCP Apps対応のMCPホストは、ツールのレスポンスにUIツールメタデータを見つけると、続けてresourceUriのURIに対してresources/readするリクエストをMCPサーバーに送る。

その結果、以下のような UIリソースオブジェクト を取得する。

UIリソースオブジェクト:

{
  contents: [{
    uri: string;   // 上記resourceUriとおなじ値
    mimeType: "text/html;profile=mcp-app";
    text?: string; // UIのHTMLドキュメント
    blob?: string; // UIのHTMLドキュメントのbase64エンコード版。
    _meta?: {
      ui?: {
        csp?: {
          // UIからfetch/XHR/WebSocketでアクセスするドメイン
          connectDomains?: string[];

          // UIから取得するリソース(scripts, images, styles, fonts)のドメイン
          resourceDomains?: string[];

          // UIに埋め込むiframeのドメイン
          frameDomains?: string[];

          // base URI
          baseUriDomains?: string[];
        };
        permissions?: {
          camera?: {};         // カメラへのアクセス許可
          microphone?: {};     // マイクへのアクセス許可
          geolocation?: {};    // 地理位置情報へのアクセス許可
          clipboardWrite?: {}; // クリップボードへの書き込み許可
        };
        domain?: string;
        prefersBorder?: boolean;
      };
    };
  }];
}

UIリソースオブジェクトにはいろいろプロパティはあるけど、一番大事なのはtextblobtextblobのいずれかにUIのHTMLドキュメントが書かれているので、MCPホストはそれをiframeのsrcdocにセットするなどしてレンダリングする。

このiframeのなかでは以下のようなことが起こる。

  1. レンダリングされたHTMLドキュメントに含まれる<script>タグにより、JavaScriptアプリ(i.e. MCPアプリ)がロードされて実行される。
  2. MCPアプリは自身の初期化処理をしたあと、MCPホストとコネクションを張る。
  3. そのコネクションを通して、MCPホストが上記のMCPツール実行のインプットとレスポンスをMCPアプリに渡す。(これについては詳細を後述する。)
  4. MCPアプリは渡されたレスポンスをグラフィカルにユーザーに見せたりしつつ、ユーザーとのインタラクションを開始する。

MCPアプリはコンセプトとしてはMCPクライアントみたいなものとされていて、MCPホストとのコネクションを通してMCPサーバーのツールを実行したりリソースを取得したりできる。

また、上記のUIリソースオブジェクトの_meta.ui.csp.connectDomainsで指定されたドメインのWebサーバーのAPIを叩いたり、_meta.ui.permissionsで許可されていればカメラとかにもアクセスできるので、アイデア次第で幅広いことができる。

MCPホストとMCPアプリ間の通信

前節に書いたように、MCPアプリはMCPホストとコネクションを張り、それにより双方向通信をすることができる。 その通信を実現する仕組みが postMessageトランスポート と呼ばれるもので、MCPアプリとMCPホストとの間でpostMessageでJSON-RPCメッセージをやり取りできるようになっている。

例えばMCPアプリがtools/callリクエストを送ると、MCPホストがそれをMCPサーバーに送って、結果をMCPアプリに戻してくれる。 同様にMCPアプリからresources/readリクエストを送って、MCPホスト経由でMCPサーバーのリソースを読むこともできる。

MCP標準のJSON-RPCメッセージの他、以下のようなMCP Apps特有のメッセージもある。

  • ui/open-link: 指定したURLを開くことをMCPホストに要求する。MCPホストは、ブラウザで動いている場合は新タブで、そうでない場合はデフォルトブラウザを起動してそのURLを開く。
  • ui/message: MCPホストのチャットにメッセージを送る。ロールも指定できるので、ユーザーの代わりにチャットにメッセージを投稿できる。
  • ui/update-model-context: MCPホストのLLMのコンテクストにメッセージを送る。MCPホストはこのメッセージの中身は基本的にユーザーには見せなくて、ユーザーによる次のチャットメッセージと合わせてLLMに送信する。MCPアプリのUIで収集した情報でLLMのコンテクストを拡張するみたいなことができる。

ここまでに挙げたJSON-RPCメッセージはMCPアプリからMCPホストへの方向のリクエストだけど、逆方向のMCPホストからMCPアプリへのメッセージもある。 この方向のメッセージは今のところ通知だけで、以下のようなものがある。

  • MCP標準通知
    • notifications/resources/list_changed
  • MCP Apps特有の通知
    • ui/notifications/tool-input: MCPアプリとのコネクション確立後、MCPツール実行のインプットを通知。
    • ui/notifications/tool-result: MCPアプリとのコネクション確立後、MCPツール実行のレスポンスを通知。
    • ui/notifications/size-changed: MCPアプリの画面サイズが変更されたことを通知する。


以上が、MCPホストとMCPアプリ間の通信の仕組みと、通信内容。

このようなMCPホストとMCPアプリ間の通信を処理する、MCPホスト側のコンポーネントは、Appブリッジ と呼ばれる。

MCPホストとMCPアプリ間のコネクション確立時の通知の挙動

ui/notifications/tool-inputui/notifications/tool-resultについて、ちょっと仕様がわかりにくかったので調べた。

MCP Appsの2026-01-26版の仕様を読むと、これらはMCPアプリがMCPホストとのハンドシェイクを完了してコネクションを張ったあと、MCPホストが一回ずつだけ送ってくると書いてある。 役割としては、「MCP Appsの仕組み」の図にあるMCPツールの呼び出しのインプットとレスポンスをMCPアプリに渡すというもの。

だけど仕様書の別の個所のシーケンス図を見ると、MCPアプリからのツール呼び出しについてもui/notifications/tool-inputui/notifications/tool-resultを送るように見える。 インプットはMCPアプリ自身が送ったものなのでMCPアプリに通知しても意味ないし、レスポンスもツール呼び出しの戻り値として受け取れるので通知は不要。 MCP Appsの公式SDKのソースやそのもととなったMCP UIのソースを見ても、MCPアプリからのツール呼び出しに対してこれらの通知を送るような設計に見えない。

MCP Apps対応のAIエージェントであるGooseや、MCP Apps SDK同梱のサンプルホストの挙動をみてもMCPアプリからのツール呼び出しに対してはui/notifications/tool-inputui/notifications/tool-resultは送らないので、多分仕様書が間違ってる。

MCP Appsのセキュリティ

Sandbox Proxy

MCP Appsは上記のようにMCPホストのiframeで読み込まれるんだけど、iframeの中に悪意のあるコンテンツを表示されたり、iframeの中からホストのコンテンツを読み取られたり、悪意のあるツール呼び出しをされたりする危険性がある。

このような危険性を軽減するため、MCP Appsを実装するホストには基本的に double-iframe sandboxアーキテクチャ の実装が要求される。

mcpApps-sandbox.svg

このアーキテクチャでは、MCPホストはUIリソースを取得したら、まずSandbox Proxy(上図では、二つあるsandbox iframeの外側の方)をレンダリングする。

Sandbox ProxyはMCPホストのドメインと異なるURLをソースとするiframeで、sandbox属性によって中のHTMLやJavaScriptがアクセスできる機能を制限したもの。 HTMLドキュメント内ではJavaScriptの実行はできるけど、同一オリジンにしかアクセスできなくて、ポップアップを出したり、Cookieにアクセスしたり、フォームを送信したりすることはできない。

iframeのallow属性

Sandbox Proxyは内部に別のiframe(上図の内側のsandbox iframe)を配置して、UIリソースのHTMLドキュメントをレンダリングする。 以降の流れは上の「MCP Appsの仕組み」に書いた通りだけど、セキュリティの観点では、このiframeのallow属性にUIリソースオブジェクトのpermissionsの設定が反映されるというところが重要。

MCPアプリはallow属性で許可した機能にしかアクセスできないわけだけど、MCPホストはMCPサーバーが返してきたpermissionsを精査して、ユーザーの同意をとったりして、安全な機能だけをMCPアプリにアクセス許可することができる。

Content Security Policy (CSP)

iframeのallow属性に加えて、UIリソースのHTMLドキュメントにはContent Security Policy (CSP)も設定されて、読み込めるリソースを制限される。

このCSPには、UIリソースオブジェクトのcspの設定が反映され、そこで許可したドメインに対してしか、fetchでアクセスしたり、スクリプトなどのリソースをダウンロードしたりできない。

UIリソースのHTMLドキュメントにCSPを設定する手段はプロトコルでは特に定められてないけど、Gooseの実装では以下のようにSandbox ProxyのiframeのHTMLドキュメントの http-equiv属性で設定していた。 (https://images-assets.nasa.govcsp.resourceDomainsで許可したオリジン。)

<meta
  http-equiv="Content-Security-Policy"
  content="default-src 'none';
    script-src 'self' 'unsafe-inline' https://images-assets.nasa.gov;
    script-src-elem 'self' 'unsafe-inline' https://images-assets.nasa.gov;
    style-src 'self' 'unsafe-inline' https://images-assets.nasa.gov;
    style-src-elem 'self' 'unsafe-inline' https://images-assets.nasa.gov;
    connect-src 'self';
    img-src 'self' data: blob: https://images-assets.nasa.gov;
    font-src 'self' https://images-assets.nasa.gov;
    media-src 'self' data: blob: https://images-assets.nasa.gov;
    frame-src 'none';
    object-src 'none';
    base-uri 'self'"
>

Gooseの実装ではUIリソースのHTMLドキュメントをiframeのsrcdocでレンダリングするため、親のSandbox Proxyのドキュメントの一部(同一オリジン)という扱いになり、上記CSPが継承されて適用される、という寸法。

因みに、MCP公式SDKのサンプルでは、Sandbox ProxyのサーバーのレスポンスのHTTPヘッダーでCSPを設定して、内側のiframeにはsrcdocの代わりにdocument.writeでHTMLドキュメントを書き込む実装になっている。 これでもUIリソースのHTMLドキュメントにCSP設定が継承される。

iframeのcsp属性がexperimentalじゃなくなったらそっちで設定するようになるのかも。