---
title: Building MCP Apps
description: Build interactive UIs for MCP tools using the MCP Apps SDK
icon: Globe
---
import Callout from "../../../../components/ui/Callout.astro";

## What's an MCP App?

An MCP App is an interactive UI rendered inside MCP clients (Claude, Cursor, etc.). It connects to the host via the `@modelcontextprotocol/ext-apps` SDK, receives tool input and results, and renders a rich interactive display.

MCP Apps are:
- Built as single-file HTML bundles
- Served as MCP resources by your server
- Connected to tools via `_meta.ui.resourceUri`
- Styled automatically by the host theme

## Stack

- **React 19** with React Compiler (`babel-plugin-react-compiler`)
- **Tailwind v4** + **shadcn/ui** components
- **TanStack Router** (hash-based, works in embedded contexts)
- **@modelcontextprotocol/ext-apps** SDK for host communication
- **Vite** + **vite-plugin-singlefile** (builds to a single HTML file)

## How It Works

1. Your MCP server exposes tools with `_meta.ui.resourceUri` pointing to a resource
2. When a tool is called, the MCP client loads the resource (an HTML bundle)
3. The HTML bundle connects to the host via the MCP Apps SDK
4. The UI receives tool input and results through event handlers
5. The UI renders an interactive display and can send messages back to the conversation

## App Structure

### `web/app.tsx` — React entry point

```tsx
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { McpProvider } from "./context.tsx";
import { AppRouter } from "./router.tsx";

createRoot(document.getElementById("root")!).render(
  <StrictMode>
    <McpProvider>
      <AppRouter />
    </McpProvider>
  </StrictMode>,
);
```

### `web/context.tsx` — MCP state management

The `McpProvider` connects to the MCP host and manages state:

```tsx
import {
  type App,
  type McpUiHostContext,
  useApp,
  useHostStyles,
} from "@modelcontextprotocol/ext-apps/react";

export function McpProvider({ children }: { children: ReactNode }) {
  const [state, setState] = useState<McpState>(INITIAL_STATE);

  const onAppCreated = useCallback((app: App) => {
    app.ontoolinput = (params) => {
      setState((prev) => ({
        ...prev,
        status: "tool-input",
        toolInput: params.arguments,
      }));
    };

    app.ontoolresult = (result) => {
      setState((prev) => ({
        ...prev,
        status: "tool-result",
        toolResult: result.structuredContent,
      }));
    };

    app.ontoolcancelled = () => {
      setState((prev) => ({ ...prev, status: "tool-cancelled" }));
    };
  }, []);

  const { app, isConnected } = useApp({
    appInfo: { name: "MCP App", version: "1.0.0" },
    capabilities: {},
    onAppCreated,
  });

  useHostStyles(app, app?.getHostContext());

  return (
    <McpAppContext.Provider value={app}>
      <McpStateContext.Provider value={state}>
        {children}
      </McpStateContext.Provider>
    </McpAppContext.Provider>
  );
}
```

Exports:
- **`useMcpState<TInput, TResult>()`** — typed access to the current MCP state
- **`useMcpApp()`** — access the App instance (for `sendMessage()`, etc.)
- **`useMcpHostContext()`** — access host context (safe area insets, tool info)

### `web/router.tsx` — Tool routing

```tsx
const TOOL_PAGES: Record<string, React.ComponentType> = {
  hello_world: HelloPage,
};

function ToolRouter() {
  const { toolName } = useMcpState();
  const Page = TOOL_PAGES[toolName];
  if (!Page) return <p>Unknown tool: {toolName}</p>;
  return <Page />;
}
```

The router reads `toolName` from the MCP host context and renders the matching page. `RootLayout` applies safe area insets from the host.

### `web/types.ts` — State types

```typescript
export type McpStatus =
  | "initializing"
  | "connected"
  | "tool-input"
  | "tool-result"
  | "tool-cancelled"
  | "error";

export interface McpState<TInput = unknown, TResult = unknown> {
  status: McpStatus;
  toolName?: string;
  error?: string;
  toolInput?: TInput;
  toolResult?: TResult;
}
```

## Building a Tool UI

### Step-by-step: Add a new tool UI

1. **Create the tool** (`api/tools/my-tool.ts`) with `_meta.ui.resourceUri`
2. **Create UI component** (`web/tools/my-tool/index.tsx`)
3. **Register in router** — add to `TOOL_PAGES` in `web/router.tsx`
4. **Create resource** (`api/resources/my-tool.ts`) serving the HTML bundle
5. **Register resource** in `api/main.ts` (`resources` array)

### Example: Tool UI component

Here's the hello tool UI (`web/tools/hello/index.tsx`):

```tsx
import { Badge } from "@/components/ui/badge.tsx";
import { Button } from "@/components/ui/button.tsx";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card.tsx";
import { useMcpApp, useMcpState } from "@/context.tsx";
import type { HelloInput, HelloOutput } from "../../../api/tools/hello.ts";

export default function HelloPage() {
  const state = useMcpState<HelloInput, HelloOutput>();
  const app = useMcpApp();

  if (state.status === "initializing") {
    return <div>Connecting to host...</div>;
  }

  if (state.status === "connected") {
    return (
      <Card>
        <CardHeader>
          <CardTitle>Hello MCP App</CardTitle>
        </CardHeader>
        <CardContent>
          <p>
            Connected. Call the <Badge variant="secondary">hello_world</Badge>{" "}
            tool to see a greeting here.
          </p>
        </CardContent>
      </Card>
    );
  }

  if (state.status === "tool-input") {
    return <div>Greeting {state.toolInput?.name ?? "someone"}...</div>;
  }

  if (state.status === "error") {
    return <p className="text-destructive">{state.error}</p>;
  }

  if (state.status === "tool-cancelled") {
    return <p className="text-destructive">Tool call was cancelled.</p>;
  }

  // tool-result
  return (
    <Card>
      <CardHeader>
        <CardTitle>{state.toolResult?.greeting}</CardTitle>
      </CardHeader>
      <CardContent>
        <Button
          onClick={() => {
            app?.sendMessage({
              role: "user",
              content: [{ type: "text", text: "Tell me more!" }],
            });
          }}
        >
          Send Message
        </Button>
      </CardContent>
    </Card>
  );
}
```

Key patterns:
- Use `useMcpState<TInput, TResult>()` with your tool's input/output types
- Handle **all** status states (`initializing`, `connected`, `tool-input`, `tool-result`, `tool-cancelled`, `error`)
- Use `app.sendMessage()` to send messages back to the conversation

### Example: Resource definition

See [Resources](/en/full-code-guides/resources) for how to create the resource that serves this UI.

## Host Integration

- **`useHostStyles()`** — automatically inherit host theme (colors, fonts, spacing)
- **`useMcpHostContext()`** — access safe area insets, tool info from the host
- **`app.sendMessage()`** — send messages back to the AI conversation

## Styling

- **shadcn/ui** components available in `web/components/ui/`
- **Tailwind v4** for utility classes
- Host styles automatically applied via `useHostStyles()` — your UI matches the host's theme
- Design for embedded contexts: use safe area insets, compact layouts
