Vポイントマーケティング|TECH LABの Tech Blog

TECH LABのエンジニアが技術情報を発信しています

ブログタイトル

ClaudeやChatGPTに独自のUIを表示できるMCP Appsについて。

こんにちは、CCCMKホールディングスAIエンジニアの三浦です。

Model Context Protocol (MCP)という、LLMにToolを提供できる仕組みが広く浸透してきました。そのMCPを拡張したMCP Appsという仕組みがあることを最近知って面白そうだなと感じていました。

MCPのToolは基本的にテキストや構造化されたデータを返します。MCP Appsはそれに加え、ユーザーが操作できるUIを提供することが出来、UIを通じてToolの実行結果を可視化したりUI上の操作から別のToolを実行させる、といったことが出来るようになります。

MCP Appsを使えば、たとえば「○○な本が読みたいな」といったユーザーの入力をトリガーにChatGPTやClaudeなどのAgentがサーバーから書籍リストを取得し、チャット画面に一覧表示、ユーザーが一覧からアイテムを選択するとそのアイテムの詳細を取得するToolを実行し詳細を表示する、といったことが普段使っているチャット画面から行えるようになります。

私自身、一日の中で一番触っている時間が長いのは(ブラウザを除けば)もしかしたらChatGPTやClaude, GeminiのようなAIアプリではないかなと思っています。そういった一番接触時間の長いアプリの中で、ふと「たまにはどっか旅行行きたいな」とつぶやくだけで色々な情報を教えてくれるようになるのって実は結構便利なんじゃないでしょうか。

そんなMCP Appsについて調べるために、簡単なアプリを自分で開発してみました。

冷蔵庫に余っている食材に応じて献立を考えてくれるアプリです。メニュー情報は全てダミーで固定なので、今のところ豆腐に関する献立しか出せませんが・・・。

ChatGPTとの会話の中でふと豆腐が余ってることを思い出し・・・

ChatGPTがToolを実行し献立を表示するUIを描画します。

違う方向で提案してほしい時はボタンをクリックするだけでOKです

MCP Appsを使えばこのようにチャット画面では表現できないような見せ方やインタラクティブにやり取りができるボタンを表示することが出来ます。

MCP Appsはどういう仕組みなのか

MCP Appsに対応したToolがどのように実行されるのか、MCP Appsのドキュメントの「How MCP Apps work」にフローが説明されています。

modelcontextprotocol.io

このドキュメントによると、MCP Appsに対応したToolにはui://で始まる値がToolのdescriptionのフィールド_meta.ui.resourceUriに設定されています。このURIはJavaScriptやCSSを含むHTMLのページデータに紐づいています。

MCP Appsに対応したクライアント(ChatGPTやClaudeなど)はこのHTMLを会話画面内のサンドボックス化されたiframeの中に描画します。その後Toolが実行した結果を受け取り、UIに反映するというフローになっています。

具体的な実装例

具体的な実装例をポイントを絞ってご紹介します。私は今回、MCP ServerをPythonのfastmcpで構築し、UIの実装はHTMLとTypeScriptのMCP AppsのSDK@modelcontextprotocol/ext-appsを使って実装しました。

MCP Serverの実装

まずresourceの登録をします。

BASE_DIR = Path(__file__).resolve().parent
UI_HTML_PATH = BASE_DIR / "web" / "dist" / "mcp-app.html"
RENDER_TEMPLATE_URI = "ui://meal-planner/mcp-app.html"

mcp = FastMCP("meal-planner")

...python
@mcp.resource(
    RENDER_TEMPLATE_URI,
    app=AppConfig(
        csp=ResourceCSP(
            resource_domains=[],
            connect_domains=[],
        ),
        prefers_border=True,
    ),
)
def meal_planner_ui() -> str:
    if not UI_HTML_PATH.exists():
        raise FileNotFoundError(
            f"UI file not found: {UI_HTML_PATH}. "
            "Build or place mcp-app.html under ./web first."
        )
    return UI_HTML_PATH.read_text(encoding="utf-8")

この例では_meta.ui.resourceUriui://meal-planner/mcp-app.htmlが設定されます。このUriにアクセスすると、meal_planner_ui()が返すHTMLファイルの内容が取得できるようになっています。

次にMCP Appsに対応したToolの登録です。AppConfigresource_uriui://meal-planner/mcp-app.htmlを設定します。visibilityはこのToolの公開範囲を指定することができ、["model", "app"]であればLLMとアプリ本体からこのToolが呼び出せることを示しています。もしアプリ内限定で呼び出したいToolなのであれば["app"]のようにします。

Toolの結果ですが、contentにはLLMが読む用の文字列("結果が表示された"、など)、structuredContentに画面描画に必要になるデータを格納しています。

# Toolの実行結果を整形するヘルパー関数
def _tool_result(content_text: str, structured_content: dict, meta: Optional[dict] = None) -> dict:
    result = {
        "content": [{"type": "text", "text": content_text}],
        "structuredContent": structured_content,
    }
    if meta:
        result["_meta"] = meta
    return result
...

@mcp.tool(
    name="render_meal_planner",
    description="献立プランナーのUI表示データを返します。view と payload を受け取り、widget を表示します。",
    app=AppConfig(
        resource_uri=RENDER_TEMPLATE_URI,
        visibility=["model", "app"],
    ),
)
def render_meal_planner(input: RenderMealPlannerInput) -> dict:
    text = "候補一覧を表示します。" if input.view == RenderView.candidates else "完成した献立を表示します。"
    return _tool_result(
        content_text=text,
        structured_content=input.model_dump(),
        meta={"ui:hint": input.view.value},
    )

UIの実装

TypeScriptの実装です。こちらもポイントを絞ってご紹介します。

app.ontoolresultはホスト(ChatGPTやClaudeなど)がこのToolを実行した結果をUIに渡したときに実行されます。renderFromToolResultでToolの実行結果に応じて画面描画処理を実行しています。

import { App } from "@modelcontextprotocol/ext-apps";

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

app.ontoolresult = (params) => {
  console.log("toolresult params:", params);
  renderFromToolResult(params as ToolResult);
};

app.connect();
...

renderFromToolResultは以下のようにしました。先ほどのToolの定義で触れたように、実行結果にはstructuredContentというキーに描画に必要になるデータが格納されており、そのデータを使って描画処理を実行します。同じHTMLに対し描画モード(candidates, meal_set)に応じて異なるレイアウトを表示するようにしています。

function renderFromToolResult(result: ToolResult) {
  ...

  const data = result.structuredContent as RenderMealPlannerStructuredContent | undefined;
  if (!data || !data.view || !data.payload) {
    appRoot.innerHTML = `<div class="panel">${escapeHtml(textContent || "表示できるデータがありません。")}</div>`;
    return;
  }

  if (data.view === "candidates") {
    renderCandidates(data.payload as RenderCandidatesPayload);
    return;
  }

  if (data.view === "meal_set") {
    console.log("branch: meal_set");
    renderMealSet(data.payload as RenderMealSetPayload);
    return;
  }

  appRoot.innerHTML = `<div class="panel">未知の view です。</div>`;
}

ChatGPTでローカルアプリのテストをする

OpenAIの開発者向けドキュメントではローカル環境で開発したアプリのChatGPTでのテストについて、ngrokを使う方法が紹介されています。

developers.openai.com

ngrokはローカルPCで動いているアプリを外部からアクセス可能にするサービスです。

ngrok.com

以下、ローカルで動いているアプリをChatGPTに接続するために私が今回試した手順です。

アプリを起動した後、たとえばportが8000で起動した場合は以下を実行します。

ngrok http 8000

ターミナルに外部からアクセス可能なURLが表示されるため、それを控えておきます。

次にChatGPTを開き、画面左下のアカウント名をクリックし、[設定]を選択。 [アプリ]をクリックし、[開発者モード]を"ON"にし、表示される[アプリを作成する]をクリックします。

[新しいアプリ]の画面では[名前], [説明]を入力し、MCPサーバーのURLに<ngrokのURL>/mcpを入力、[認証]を"認証なし"にして[理解したうえで、続行します]にチェックを入れ[作成する]をクリックします。

問題がなければメッセージが表示され、チャット画面で入力に応じてToolを実行してくれるようになります。

まとめ

ということで今回はClaudeやChatGPTに独自のUIを表示することができるMCP Appsについてまとめてみました。前回の私のブログの記事ではUCPというAgenticコマースの共通ルールについて触れましたが、今後Agentを通じてできることがどんどん増えていくように感じています。そうなると指示を出す手段がチャット形式だけではだんだん厳しくなってくるはずで、MCP AppsのようにUIを提供する仕組みは必要になってくると思います。引き続き調査していきたいと思います。