I created an MCP Apps with Hono × React × Lambda, and made an app that can filter products using AI

I created an MCP Apps with Hono × React × Lambda, and made an app that can filter products using AI

2026.03.12

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.

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

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.

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

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.

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

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:

  1. MCP Streamable HTTP Transport — Handling MCP protocol communication from Claude Desktop
  2. 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

  1. Build: Vite + vite-plugin-singlefile outputs the React app as a single HTML file
    • Normally Vite separates JS and CSS into different files, but this vite-plugin-singlefile inlines everything into HTML
    • MCP Apps requires single-file bundling since registerAppResource returns bundled HTML as a string
  2. Tool Registration: registerAppTool links _meta.ui.resourceUri to the tool
  3. Resource Registration: registerAppResource serves the built HTML via ui:// URI
  4. Display: The client (Claude Desktop, etc.) retrieves the resource when calling the tool and displays it in an iframe
  5. Communication: React inside the iframe communicates bidirectionally with the host using the useApp hook

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.

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

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.

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

2. Registering MCP Tools and Resources

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

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.

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

3. MCP Endpoint

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

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)

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

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)

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

Data Flow Key Points

  1. Register the ontoolinput handler in useApp's onAppCreated
  2. When AI calls the tool, ontoolinput receives the tool arguments (category)
  3. useEffect fetches the Hono API /api/products
  4. 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)

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

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)

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

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

  1. Vite builds React + Tailwind CSS into a single HTML (dist/mcp-app.html)
  2. CDK's esbuild bundles the code (embedding the HTML file as a string)
  3. 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

Share this article

FacebookHatena blogX