
MCP AppsをHono × React × Lambdaで作って、AIで商品を絞り込めるアプリを作ってみた
こんにちは、リテールアプリ共創部の戸田駿太です。
MCP Apps × Hono × React × AWS Lambda で、AIチャットの中にカスタムUIを表示するリモートMCPサーバーを作りました。この記事ではMCP Appsの仕組みと、AWS Lambdaへのサーバーレスデプロイまでの実装手順を紹介します。
作ったもの
「商品一覧を見せて」「エレクトロニクスだけに絞って」——こうした自然言語での指示に対して、AIがカテゴリや条件を判断し、リッチなUIで商品を表示してくれるアプリケーションです。


AIが会話の文脈からカテゴリを選択してMCPツールを呼び出し、チャット内にReactで作った商品一覧UIがインライン表示されます。
MCP Appsとは
MCP Apps は、MCPツールの呼び出し結果にカスタムHTML/React UIをインライン表示できる仕組みです。Claude Desktop、ChatGPT などが対応しています。
通常のMCPツールはテキストを返すだけですが、MCP Appsを使うとサンドボックスiframe内にReact UIを表示できます。
MCP Appsを詳しく知りたい方はこちらの記事をご覧ください。
なぜMCP Appsなのか
AIが条件を判断してくれる
従来のECサイトではユーザー自身がカテゴリやフィルタ条件を設定する必要がありました。MCP AppsではAIが自然言語から条件を判断してツールを呼び出すため、ユーザーは「〇〇ブランドの青い靴はある?」と聞くだけで済みます。
テキストでは伝えきれない情報をUIで表示できる
MCPツールの戻り値はテキストが基本ですが、商品一覧のように画像・価格・カテゴリを一覧で比較したい場合、テキストだけでは不十分です。MCP Appsを使えば、Reactで作ったリッチなカードUIをチャット内にインライン表示できます。
対話しながら絞り込める
結果を見ながら「もう少しカジュアルなものは?」「予算1万円以内で」と会話を続ければ、AIが条件を調整して再度ツールを呼びます。AIと壁打ちしながら商品を探す体験が実現できます。
アーキテクチャ
技術的なポイントとして、Honoが2つの役割を持っています。
- MCP Streamable HTTP Transport — Claude DesktopからのMCPプロトコル通信を処理
- 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 | - | モノレポ管理 |
仕組み
- ビルド: Vite +
vite-plugin-singlefileでReactアプリを単一HTMLファイルに出力- 通常ViteはJS・CSSを別ファイルに分離しますが、この
vite-plugin-singlefileですべてHTMLにインライン化します - MCP Appsでは
registerAppResourceでバンドル済みHTMLを文字列として返すため、単一ファイル化が必須です
- 通常ViteはJS・CSSを別ファイルに分離しますが、この
- ツール登録:
registerAppToolでツールに_meta.ui.resourceUriを紐付け - リソース登録:
registerAppResourceでビルド済みHTMLをui://URIで配信 - 表示: クライアント(Claude Desktop等)がツール呼び出し時にリソースを取得し、iframeで表示
- 通信: 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 APIとMCPエンドポイントを同居させます。HonoのAWS Lambdaアダプターと@hono/mcpを組み合わせています。
ポイントは 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の型定義も必要です。
2. MCP ツールとリソースの登録
registerAppToolでツールを登録し、registerAppResourceでReact UIのHTMLをリソースとして配信します。
ツールの戻り値は確認メッセージのみです。商品データはReact UIがHono APIを直接fetchします。このアーキテクチャにより、MCPツール結果のパース処理が不要になり、Honoが純粋なAPIサーバーとして機能します。
ハマりポイント: CSPで外部リソースがブロックされる
MCP Appsのiframeはサンドボックス環境のため、外部ドメインへのアクセスがデフォルトで制限されます。商品画像(placehold.co)が表示されない、Hono APIへのfetchが失敗する、という問題が発生しました。
registerAppResourceのcontents内の_meta.ui.cspでドメインを許可することで解決します。
3. MCPエンドポイント
Lambdaはステートレスなので、リクエストごとにMcpServerとStreamableHTTPTransportを新規生成します。sessionIdGenerator: undefinedでセッション管理を無効化しています。
4. Lambda ハンドラー (packages/server/src/lambda.ts)
streamHandleを使うことで、MCP の SSE(Server-Sent Events)レスポンスをLambdaレスポンスストリーミングで配信します。
5. React MCP Apps UI (packages/app/src/mcp-app.tsx)
データフローのポイント
useAppのonAppCreatedでontoolinputハンドラを登録- AIがツールを呼ぶと
ontoolinputでツール引数(カテゴリ)を受信 useEffectで Hono API/api/productsを fetch- 取得した商品データを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)
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)
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に書き込み、再ビルド・再デプロイする流れになります。
ビルドの流れ
- Viteが React + Tailwind CSSを単一HTMLにビルド (
dist/mcp-app.html) - CDKのesbuildがコードをバンドル(HTMLファイルも文字列として埋め込み)
- 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ツールに持たせられるのは大きな可能性を感じます。
このブログが皆さんの参考になれば幸いです。
参考
- MCP Apps (ext-apps) 公式リポジトリ
- MCP Apps 公式ドキュメント
- MCP Apps Quickstart
- @hono/mcp
- Hono - AWS Lambda アダプター
- MCP Streamable HTTP Transport 仕様
- MCP TypeScript SDK - サーバードキュメント
- yusukebe/mcp-app-with-hono
- vite-plugin-singlefile
- esbuild - Content Types (text loader)
- AWS Lambda レスポンスストリーミング
- CDK NodejsFunction
- Tailwind CSS v4









