---
title: Project Structure
description: Understand the full-stack TypeScript architecture of MCP Apps on deco
icon: Folder
---
import Callout from "../../../../components/ui/Callout.astro";

## Directory Structure

```
├── api/                        # MCP server (Bun)
│   ├── main.ts                 # Server entry point with middleware
│   ├── tools/
│   │   ├── index.ts            # Tool registry
│   │   └── hello.ts            # Example tool (hello_world)
│   ├── resources/
│   │   └── hello.ts            # MCP App resource (serves HTML)
│   └── types/
│       └── env.ts              # StateSchema + Env type
├── web/                        # React UI (MCP App)
│   ├── app.tsx                 # React entry point
│   ├── context.tsx             # MCP state management
│   ├── router.tsx              # TanStack Router (hash-based)
│   ├── types.ts                # McpState, McpStatus types
│   ├── tools/                  # One folder per tool UI
│   │   └── hello/
│   │       └── index.tsx       # hello_world tool UI
│   ├── components/ui/          # shadcn/ui components
│   ├── lib/utils.ts            # cn() helper
│   └── globals.css             # Tailwind base styles
├── index.html                  # Vite entry HTML
├── package.json
├── tsconfig.json
├── biome.json
├── vite.config.ts              # Vite + React Compiler + singlefile
├── components.json             # shadcn/ui config
├── app.json                    # Deco platform configuration
└── .mcp.json                   # Local MCP server config
```

## Backend (`api/`)

### `main.ts`

The server entry point sets up the MCP runtime and starts a Bun HTTP server:

```typescript
import { withRuntime } from "@decocms/runtime";
import { helloAppResource } from "./resources/hello.ts";
import { tools } from "./tools/index.ts";
import { type Env, StateSchema } from "./types/env.ts";

const runtime = withRuntime<Env, typeof StateSchema>({
  configuration: {
    state: StateSchema,
  },
  tools,
  resources: [helloAppResource],
});

Bun.serve({
  port: 3001,
  fetch: withLogging(withMcpApiRoute(runtime.fetch)),
});
```

Key parts:
- **`withRuntime()`** — creates the MCP server from tools, resources, and configuration
- **`withMcpApiRoute()`** — rewrites `/api/mcp` to `/mcp` so the MCP endpoint lives at a clean API path
- **`withLogging()`** — colored request/response logging middleware
- **`Bun.serve()`** — starts the HTTP server

### `tools/`

Tool definitions organized by domain. Each file exports a tool creator function that receives the environment.

- `tools/index.ts` exports the `tools` array — all tool creators registered here
- See [Building Tools](/en/full-code-guides/building-tools) for details

### `resources/`

MCP App resources that serve single-file HTML bundles as rich UIs for tool results.

- Each resource maps a URI (e.g., `ui://mcp-app/hello`) to the built HTML file
- See [Resources](/en/full-code-guides/resources) for details

### `types/`

```typescript
import type { DefaultEnv } from "@decocms/runtime";
import { z } from "zod";

export const StateSchema = z.object({});

export type Env = DefaultEnv<typeof StateSchema>;
```

- **`StateSchema`** — Zod schema defining your app's configuration (shown to installers)
- **`Env`** — typed environment extending `DefaultEnv`, available in tool handlers

## Frontend (`web/`)

### Entry point (`web/app.tsx`)

```typescript
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>,
);
```

The app wraps everything in `McpProvider` (connects to the MCP host) and `AppRouter` (routes to the correct tool UI).

### MCP Context (`web/context.tsx`)

Manages the connection to the MCP host using `@modelcontextprotocol/ext-apps`:

- **`McpProvider`** — connects via `useApp()`, handles events (`ontoolinput`, `ontoolresult`, `ontoolcancelled`, `onerror`, `onhostcontextchanged`), applies host styles
- **`useMcpState<TInput, TResult>()`** — access current MCP state (status, tool input/result)
- **`useMcpApp()`** — access the MCP App instance (e.g., for `app.sendMessage()`)
- **`useMcpHostContext()`** — access host context (safe area insets, tool info)

### Router (`web/router.tsx`)

Uses TanStack Router with hash-based history:

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

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

### Tool UIs (`web/tools/<name>/`)

Each tool's UI lives in its own folder. The component uses `useMcpState<TInput, TResult>()` to handle all lifecycle states: `initializing`, `connected`, `tool-input`, `tool-result`, `tool-cancelled`, and `error`.

### Build system

- **Vite** with `vite-plugin-singlefile` — builds into a single `dist/client/index.html`
- **React 19** with React Compiler (`babel-plugin-react-compiler`)
- All CSS and JS inlined into one HTML file for serving as an MCP App resource

## Configuration

### `app.json`

```json
{
  "scopeName": "deco",
  "name": "mcp-app",
  "friendlyName": "MCP App Template",
  "description": "Starter template for building MCP Apps on deco.",
  "connection": {
    "type": "HTTP",
    "url": "https://your-app.deco.site/api/mcp",
    "configSchema": {
      "type": "object",
      "properties": {},
      "required": []
    }
  }
}
```

- **`scopeName`** + **`name`** — unique identifier for your app in the deco platform
- **`friendlyName`** — display name shown to users
- **`connection.url`** — your deployed MCP endpoint
- **`configSchema`** — JSON Schema defining configuration options shown to installers

### `.mcp.json`

Local MCP server config for testing with editors:

```json
{
  "mcpServers": {
    "mcp-app": {
      "type": "sse",
      "url": "http://localhost:3001/mcp"
    }
  }
}
```

## Development Flow

1. Create a tool in `api/tools/my-tool.ts`
2. Register it in `api/tools/index.ts`
3. Create a UI component in `web/tools/my-tool/index.tsx`
4. Register the page in `web/router.tsx` (`TOOL_PAGES` map)
5. Create a resource in `api/resources/my-tool.ts`
6. Register the resource in `api/main.ts` (`resources` array)
7. Run `bun run dev` and test
