MCP AppsをHono × React × Lambdaで作って、AIで商品を絞り込めるアプリを作ってみた

MCP AppsをHono × React × Lambdaで作って、AIで商品を絞り込めるアプリを作ってみた

2026.03.12

こんにちは、リテールアプリ共創部の戸田駿太です。

MCP Apps × Hono × React × AWS Lambda で、AIチャットの中にカスタムUIを表示するリモートMCPサーバーを作りました。この記事ではMCP Appsの仕組みと、AWS Lambdaへのサーバーレスデプロイまでの実装手順を紹介します。

作ったもの

「商品一覧を見せて」「エレクトロニクスだけに絞って」——こうした自然言語での指示に対して、AIがカテゴリや条件を判断し、リッチなUIで商品を表示してくれるアプリケーションです。

CleanShot 2026-03-12 at 15.15.14.png
CleanShot 2026-03-12 at 15.15.24.png

AIが会話の文脈からカテゴリを選択してMCPツールを呼び出し、チャット内にReactで作った商品一覧UIがインライン表示されます。

GitHub - ShuntaToda/mcp-apps-react-hono-lambda: MCP Apps + React + Hono + AWS Lambda - Line chart MCP server with custom React UI · GitHub

MCP Appsとは

MCP Apps は、MCPツールの呼び出し結果にカスタムHTML/React UIをインライン表示できる仕組みです。Claude Desktop、ChatGPT などが対応しています。

通常のMCPツールはテキストを返すだけですが、MCP Appsを使うとサンドボックスiframe内にReact UIを表示できます。

MCP Appsを詳しく知りたい方はこちらの記事をご覧ください。

https://dev.classmethod.jp/articles/mcp-apps-introduction-overview/

なぜMCP Appsなのか

AIが条件を判断してくれる

従来のECサイトではユーザー自身がカテゴリやフィルタ条件を設定する必要がありました。MCP AppsではAIが自然言語から条件を判断してツールを呼び出すため、ユーザーは「〇〇ブランドの青い靴はある?」と聞くだけで済みます。

テキストでは伝えきれない情報をUIで表示できる

MCPツールの戻り値はテキストが基本ですが、商品一覧のように画像・価格・カテゴリを一覧で比較したい場合、テキストだけでは不十分です。MCP Appsを使えば、Reactで作ったリッチなカードUIをチャット内にインライン表示できます。

対話しながら絞り込める

結果を見ながら「もう少しカジュアルなものは?」「予算1万円以内で」と会話を続ければ、AIが条件を調整して再度ツールを呼びます。AIと壁打ちしながら商品を探す体験が実現できます。

アーキテクチャ

技術的なポイントとして、Honoが2つの役割を持っています。

  1. MCP Streamable HTTP Transport — Claude DesktopからのMCPプロトコル通信を処理
  2. REST API サーバー — React UIからの商品データ取得リクエストに応答

Streamable HTTP Transportは、MCPのリモート通信で使われるTransportです。クライアントからサーバーへは通常のHTTP POST、サーバーからクライアントへはSSE(Server-Sent Events)でストリーミングレスポンスを返します。

これらがすべてAWS Lambda + Function URL上で動きます。インフラはAWS CDKで管理し、cdk deploy一発でデプロイできます。

技術スタック

技術 バージョン 用途
Hono 4.12.7 HTTPフレームワーク(Lambda上)
@hono/mcp 0.2.4 Hono用 Streamable HTTP Transport
@modelcontextprotocol/sdk 1.27.1 MCP サーバーSDK
@modelcontextprotocol/ext-apps 1.2.2 MCP Apps(ReactカスタムUI)
React 19 MCP Apps UI
Vite 6 React ビルド(singlefile出力)
Tailwind CSS v4 スタイリング
AWS CDK 2.x インフラ (Lambda + Function URL)
pnpm workspaces - モノレポ管理

仕組み

  1. ビルド: Vite + vite-plugin-singlefile でReactアプリを単一HTMLファイルに出力
    • 通常ViteはJS・CSSを別ファイルに分離しますが、このvite-plugin-singlefileですべてHTMLにインライン化します
    • MCP AppsではregisterAppResourceバンドル済みHTMLを文字列として返すため、単一ファイル化が必須です
  2. ツール登録: registerAppTool でツールに _meta.ui.resourceUri を紐付け
  3. リソース登録: registerAppResource でビルド済みHTMLを ui:// URIで配信
  4. 表示: クライアント(Claude Desktop等)がツール呼び出し時にリソースを取得し、iframeで表示
  5. 通信: iframe内のReactが useApp フックでホストと双方向通信

プロジェクト構成

pnpm workspacesによるモノレポ構成です。

shop-mcp/
├── pnpm-workspace.yaml
├── .env                         # FUNCTION_URL を一元管理
├── packages/
│   ├── server/                  Hono + MCP サーバー (Lambda)
│   │   └── src/
│   │       ├── index.ts         Hono app + MCP ツール登録
│   │       ├── lambda.ts        Lambda ハンドラー
│   │       └── html.d.ts        HTML import の型定義
│   ├── app/                     React (MCP Apps UI)
│   │   ├── vite.config.ts
│   │   ├── mcp-app.html         Vite エントリHTML
│   │   └── src/
│   │       ├── mcp-app.tsx      useApp フック利用
│   │       └── index.css        Tailwind CSS
│   └── infra/                   AWS CDK
│       └── lib/
│           ├── app.ts           CDK App エントリ
│           └── stack.ts         Lambda + Function URL スタック

実装

1. Hono + MCP サーバー (packages/server/src/index.ts)

Honoアプリの中にREST APIMCPエンドポイントを同居させます。HonoのAWS Lambdaアダプター@hono/mcpを組み合わせています。

https://github.com/ShuntaToda/mcp-apps-react-hono-lambda/blob/1706e324c9627e48ab069d3ccd132f12d9c07a8e/packages/server/src/index.ts#L1-L77

ポイントは import mcpAppHtml from "../../app/dist/mcp-app.html" の部分です。esbuildのloader: { ".html": "text" }を使うことで、ビルド時にHTMLを文字列としてバンドルに埋め込みます。

ハマりポイント: Lambda環境でHTMLファイルが読めない

最初はfs.readFileでHTMLを読もうとしましたが、Lambda環境ではENOENTエラーになりました。Lambdaのバンドルにはpackages/app/dist/mcp-app.htmlが含まれず、ファイルシステムにアクセスできないためです。esbuildのloader: { ".html": "text" }import文として埋め込むことで解決しました。TypeScriptの型定義も必要です。

https://github.com/ShuntaToda/mcp-apps-react-hono-lambda/blob/1706e324c9627e48ab069d3ccd132f12d9c07a8e/packages/server/src/html.d.ts#L1-L5

2. MCP ツールとリソースの登録

https://github.com/ShuntaToda/mcp-apps-react-hono-lambda/blob/1706e324c9627e48ab069d3ccd132f12d9c07a8e/packages/server/src/index.ts#L85-L147

registerAppToolでツールを登録し、registerAppResourceでReact UIのHTMLをリソースとして配信します。

ツールの戻り値は確認メッセージのみです。商品データはReact UIがHono APIを直接fetchします。このアーキテクチャにより、MCPツール結果のパース処理が不要になり、Honoが純粋なAPIサーバーとして機能します。

ハマりポイント: CSPで外部リソースがブロックされる

MCP Appsのiframeはサンドボックス環境のため、外部ドメインへのアクセスがデフォルトで制限されます。商品画像(placehold.co)が表示されない、Hono APIへのfetchが失敗する、という問題が発生しました。

registerAppResourcecontents内の_meta.ui.cspでドメインを許可することで解決します。

https://github.com/ShuntaToda/mcp-apps-react-hono-lambda/blob/1706e324c9627e48ab069d3ccd132f12d9c07a8e/packages/server/src/index.ts#L131-L139

3. MCPエンドポイント

https://github.com/ShuntaToda/mcp-apps-react-hono-lambda/blob/1706e324c9627e48ab069d3ccd132f12d9c07a8e/packages/server/src/index.ts#L149-L158

Lambdaはステートレスなので、リクエストごとにMcpServerStreamableHTTPTransportを新規生成します。sessionIdGenerator: undefinedでセッション管理を無効化しています。

4. Lambda ハンドラー (packages/server/src/lambda.ts)

https://github.com/ShuntaToda/mcp-apps-react-hono-lambda/blob/1706e324c9627e48ab069d3ccd132f12d9c07a8e/packages/server/src/lambda.ts#L1-L6

streamHandleを使うことで、MCP の SSE(Server-Sent Events)レスポンスをLambdaレスポンスストリーミングで配信します。

5. React MCP Apps UI (packages/app/src/mcp-app.tsx)

https://github.com/ShuntaToda/mcp-apps-react-hono-lambda/blob/1706e324c9627e48ab069d3ccd132f12d9c07a8e/packages/app/src/mcp-app.tsx#L1-L127

データフローのポイント

  1. useApponAppCreatedontoolinput ハンドラを登録
  2. AIがツールを呼ぶと ontoolinput でツール引数(カテゴリ)を受信
  3. useEffect で Hono API /api/products を fetch
  4. 取得した商品データをReactで描画

ontoolresult(ツール実行結果)ではなくontoolinput(ツール引数)を使っているのがポイントです。商品データはサーバーのツール結果ではなく、Hono APIから直接取得します。

ハマりポイント: ontoolinput と ontoolresult の使い分け

MCP Appsには2つのイベントがあります。

イベント タイミング ユースケース
ontoolinput AIがツールを呼んだ直後 ツール引数をUIに渡す
ontoolresult サーバーが結果を返した後 ツール実行結果をUIに表示

最初はontoolresultでツール結果に商品データを含めてReactに渡す方式を試しましたが、データの受け渡しが複雑になりました。そこで設計を見直し、ReactがHono APIを直接fetchする方式に変更しました。ontoolinputでカテゴリ引数だけ受け取り、それをAPIのクエリパラメータとして使います。

6. Vite設定 (packages/app/vite.config.ts)

https://github.com/ShuntaToda/mcp-apps-react-hono-lambda/blob/1706e324c9627e48ab069d3ccd132f12d9c07a8e/packages/app/vite.config.ts#L1-L23

vite-plugin-singlefileがJS・CSSをすべてHTMLにインライン化します。MCP Appsはこの単一HTMLファイルをリソースとして配信する仕組みなので、外部ファイル参照が不要になります。

loadEnv(mode, "../..", "FUNCTION_URL")でルートの.envファイルからFUNCTION_URLを読み込み、defineでビルド時にAPIのベースURLを埋め込みます。

7. CDK スタック (packages/infra/lib/stack.ts)

https://github.com/ShuntaToda/mcp-apps-react-hono-lambda/blob/1706e324c9627e48ab069d3ccd132f12d9c07a8e/packages/infra/lib/stack.ts#L1-L58

invokeMode: RESPONSE_STREAM が重要です。MCPのStreamable HTTP TransportはSSEを使うため、レスポンスストリーミングが必須になります。CDKのNodejsFunctionでesbuildのバンドル設定を行います。

ハマりポイント: CDKの循環参照エラー

Lambda関数にFunction URLを環境変数として渡したくなりますが、fn.addEnvironment("FUNCTION_URL", functionUrl.url)とすると循環参照が発生します(Lambda → Function URL → Lambda環境変数)。

ルートの.envファイルにFUNCTION_URLを定義し、ビルド時にesbuildのdefineで埋め込むことで解決しました。ViteとCDKの両方が同じ.envを参照する設計です。

# .env
FUNCTION_URL=https://xxxxx.lambda-url.ap-northeast-1.on.aws

初回デプロイ時はFunction URLが未確定なので、一度デプロイしてURLを取得してから.envに書き込み、再ビルド・再デプロイする流れになります。

ビルドの流れ

  1. Viteが React + Tailwind CSSを単一HTMLにビルド (dist/mcp-app.html)
  2. CDKのesbuildがコードをバンドル(HTMLファイルも文字列として埋め込み)
  3. LambdaにデプロイされてFunction URLで公開

Claude Desktopからの接続

デプロイしたMCPサーバーにClaude Desktopから接続するには、claude_desktop_config.jsonに以下を追加します。

{
  "mcpServers": {
    "shop-mcp": {
      "command": "npx",
      "args": ["mcp-remote", "https://xxxxx.lambda-url.ap-northeast-1.on.aws/mcp"]
    }
  }
}

まとめ

MCP Apps × Hono × React × AWS Lambdaの組み合わせで、以下のことが実現できました。

  • MCP Apps: AIチャット内にReact UIをインライン表示
  • Honoの二役: MCP Streamable HTTP Transport + REST APIサーバーを1つのHonoアプリで実現
  • サーバーレスデプロイ: AWS Lambda + Function URLでスケーラブルに運用
  • 単一HTMLバンドル: vite-plugin-singlefileでReact + Tailwind CSSを1ファイルに凝縮

MCP Appsはまだ新しい仕組みですが、テキストだけでは表現しきれないリッチなUIをAIツールに持たせられるのは大きな可能性を感じます。

このブログが皆さんの参考になれば幸いです。

参考

この記事をシェアする

FacebookHatena blogX

関連記事