
I created an MCP Apps with Hono × React × Lambda, and made an app that can filter products using AI
This page has been translated by machine translation. View original
Hello, I'm Shunta Toda from the Retail App Co-Creation Department.
MCP Apps × Hono × React × AWS Lambda - I created a remote MCP server that displays custom UI within AI chat. This article introduces how MCP Apps works and the implementation steps for serverless deployment to AWS Lambda.
What I Built
"Show me a product list" "Filter to only electronics" - This application responds to such natural language instructions with AI determining the categories and conditions, displaying products with a rich UI.


The AI selects the category from the conversation context to call the MCP tool, and a product list UI built with React is displayed inline in the chat.
What is MCP Apps
MCP Apps is a mechanism that allows you to display custom HTML/React UI inline with MCP tool call results. Claude Desktop, ChatGPT, and others support this.
While regular MCP tools only return text, with MCP Apps you can display React UI in a sandboxed iframe.
For those who want to learn more about MCP Apps, please check this article.
Why MCP Apps?
AI Determines Conditions
In traditional e-commerce sites, users need to set categories and filter conditions themselves. With MCP Apps, AI determines conditions from natural language and calls the tool, so users can simply ask "Do you have any blue shoes from brand X?"
Displaying Information That Can't Be Fully Conveyed in Text
While MCP tool return values are basically text, for cases like product listings where you want to compare images, prices, and categories in a list, text alone is insufficient. With MCP Apps, you can display rich card UIs created with React inline in the chat.
Refine While Conversing
As you view results, you can continue the conversation with "How about something more casual?" or "Within a $100 budget," and the AI will adjust the conditions and call the tool again. This enables the experience of finding products while conversing with AI.
Architecture
As a technical point, Hono plays two roles:
- MCP Streamable HTTP Transport — Handling MCP protocol communication from Claude Desktop
- REST API Server — Responding to product data retrieval requests from React UI
Streamable HTTP Transport is the transport used for MCP remote communication. From client to server, it uses regular HTTP POST; from server to client, it returns streaming responses via SSE (Server-Sent Events).
All of these run on AWS Lambda + Function URL. The infrastructure is managed with AWS CDK and can be deployed with a single cdk deploy command.
Technology Stack
| Technology | Version | Purpose |
|---|---|---|
| Hono | 4.12.7 | HTTP framework (on Lambda) |
| @hono/mcp | 0.2.4 | Streamable HTTP Transport for Hono |
| @modelcontextprotocol/sdk | 1.27.1 | MCP Server SDK |
| @modelcontextprotocol/ext-apps | 1.2.2 | MCP Apps (React custom UI) |
| React | 19 | MCP Apps UI |
| Vite | 6 | React build (singlefile output) |
| Tailwind CSS | v4 | Styling |
| AWS CDK | 2.x | Infrastructure (Lambda + Function URL) |
| pnpm workspaces | - | Monorepo management |
How It Works
- Build: Vite +
vite-plugin-singlefileoutputs the React app as a single HTML file- Normally Vite separates JS and CSS into different files, but this
vite-plugin-singlefileinlines everything into HTML - MCP Apps requires single-file bundling since
registerAppResourcereturns bundled HTML as a string
- Normally Vite separates JS and CSS into different files, but this
- Tool Registration:
registerAppToollinks_meta.ui.resourceUrito the tool - Resource Registration:
registerAppResourceserves the built HTML viaui://URI - Display: The client (Claude Desktop, etc.) retrieves the resource when calling the tool and displays it in an iframe
- Communication: React inside the iframe communicates bidirectionally with the host using the
useApphook
Project Structure
Monorepo structure using pnpm workspaces.
shop-mcp/
├── pnpm-workspace.yaml
├── .env # Manage FUNCTION_URL in one place
├── packages/
│ ├── server/ Hono + MCP Server (Lambda)
│ │ └── src/
│ │ ├── index.ts Hono app + MCP tool registration
│ │ ├── lambda.ts Lambda handler
│ │ └── html.d.ts Type definition for HTML import
│ ├── app/ React (MCP Apps UI)
│ │ ├── vite.config.ts
│ │ ├── mcp-app.html Vite entry HTML
│ │ └── src/
│ │ ├── mcp-app.tsx Using useApp hook
│ │ └── index.css Tailwind CSS
│ └── infra/ AWS CDK
│ └── lib/
│ ├── app.ts CDK App entry
│ └── stack.ts Lambda + Function URL stack
Implementation
1. Hono + MCP Server (packages/server/src/index.ts)
We have both REST API and MCP endpoint coexisting in the Hono app. This combines Hono's AWS Lambda adapter with @hono/mcp.
The key point is import mcpAppHtml from "../../app/dist/mcp-app.html". Using esbuild's loader: { ".html": "text" }, we embed HTML as a string in the bundle at build time.
Gotcha: Can't read HTML files in Lambda environment
At first, I tried to read HTML with fs.readFile, but got an ENOENT error in the Lambda environment. This happens because the Lambda bundle doesn't include packages/app/dist/mcp-app.html and can't access the file system. I solved this by embedding it as an import statement using esbuild's loader: { ".html": "text" }. TypeScript type definitions are also necessary.
2. Registering MCP Tools and Resources
We register tools with registerAppTool and serve the React UI HTML as a resource with registerAppResource.
The tool's return value is just a confirmation message. React UI fetches product data directly from the Hono API. This architecture eliminates the need for parsing MCP tool results, and Hono functions as a pure API server.
Gotcha: External resources blocked by CSP
MCP Apps iframe is a sandbox environment, so access to external domains is restricted by default. This caused problems: product images (placehold.co) weren't displaying, and fetches to the Hono API were failing.
This was solved by allowing domains in _meta.ui.csp within registerAppResource's contents.
3. MCP Endpoint
Since Lambda is stateless, we generate a new McpServer and StreamableHTTPTransport for each request. We disable session management with sessionIdGenerator: undefined.
4. Lambda Handler (packages/server/src/lambda.ts)
Using streamHandle allows us to deliver MCP's SSE (Server-Sent Events) responses via Lambda response streaming.
5. React MCP Apps UI (packages/app/src/mcp-app.tsx)
Data Flow Key Points
- Register the
ontoolinputhandler inuseApp'sonAppCreated - When AI calls the tool,
ontoolinputreceives the tool arguments (category) useEffectfetches the Hono API/api/products- Render the fetched product data with React
The key point is using ontoolinput (tool arguments) instead of ontoolresult (tool execution result). Product data is fetched directly from the Hono API, not from the server's tool result.
Gotcha: Choosing between ontoolinput and ontoolresult
MCP Apps has two events:
| Event | Timing | Use case |
|---|---|---|
ontoolinput |
Right after AI calls the tool | Pass tool arguments to UI |
ontoolresult |
After server returns results | Display tool execution results in UI |
Initially, I tried including product data in the tool result and passing it to React via ontoolresult, but data passing became complex. I redesigned it to have React fetch directly from the Hono API. It receives only the category argument via ontoolinput and uses that as an API query parameter.
6. Vite Configuration (packages/app/vite.config.ts)
vite-plugin-singlefile inlines all JS and CSS into the HTML. MCP Apps serves this single HTML file as a resource, eliminating the need for external file references.
loadEnv(mode, "../..", "FUNCTION_URL") reads FUNCTION_URL from the root .env file, and define embeds the API base URL at build time.
7. CDK Stack (packages/infra/lib/stack.ts)
invokeMode: RESPONSE_STREAM is crucial. MCP's Streamable HTTP Transport uses SSE, requiring response streaming. CDK's NodejsFunction configures esbuild bundling.
Gotcha: CDK circular reference error
You might want to pass the Function URL to the Lambda function as an environment variable, but fn.addEnvironment("FUNCTION_URL", functionUrl.url) causes a circular reference (Lambda → Function URL → Lambda environment variables).
The solution was to define FUNCTION_URL in the root .env file and embed it at build time using esbuild's define. This design has both Vite and CDK referring to the same .env.
# .env
FUNCTION_URL=https://xxxxx.lambda-url.ap-northeast-1.on.aws
For the initial deployment, since the Function URL is not yet determined, the process is to deploy once, get the URL, write it to .env, then rebuild and redeploy.
Build Flow
- Vite builds React + Tailwind CSS into a single HTML (
dist/mcp-app.html) - CDK's esbuild bundles the code (embedding the HTML file as a string)
- Deployed to Lambda and published via Function URL
Connecting from Claude Desktop
To connect to the deployed MCP server from Claude Desktop, add the following to claude_desktop_config.json:
{
"mcpServers": {
"shop-mcp": {
"command": "npx",
"args": ["mcp-remote", "https://xxxxx.lambda-url.ap-northeast-1.on.aws/mcp"]
}
}
}
Summary
With the combination of MCP Apps × Hono × React × AWS Lambda, we achieved the following:
- MCP Apps: Inline display of React UI within AI chat
- Hono's dual role: Implementing both MCP Streamable HTTP Transport and REST API server in a single Hono app
- Serverless deployment: Scalable operation with AWS Lambda + Function URL
- Single HTML bundle: Condensing React + Tailwind CSS into one file with
vite-plugin-singlefile
MCP Apps is still a new mechanism, but being able to give AI tools rich UIs that can't be fully expressed with text alone shows great potential.
I hope this blog will be helpful to everyone.
References
- MCP Apps (ext-apps) Official Repository
- MCP Apps Official Documentation
- MCP Apps Quickstart
- @hono/mcp
- Hono - AWS Lambda Adapter
- MCP Streamable HTTP Transport Specification
- MCP TypeScript SDK - Server Documentation
- yusukebe/mcp-app-with-hono
- vite-plugin-singlefile
- esbuild - Content Types (text loader)
- AWS Lambda Response Streaming
- CDK NodejsFunction
- Tailwind CSS v4