Project Structure
Understand the full-stack TypeScript architecture of MCP Apps on deco
This guide follows the MCP App starter at commit 3fc9bd15. Its MCP server and React interface are separate from the Blocks website framework.
To use that reviewed starter:
git clone https://github.com/decocms/mcp-app.git my-mcp-app
cd my-mcp-app
git checkout 3fc9bd15cb28a66361d5c99730962307d3fd77e0
bun install --frozen-lockfile
bun run devDirectory Structure
├── api/ # MCP server (Bun)
│ ├── app.ts # Runtime, tools, resources, and middleware
│ ├── main.bun.ts # Bun HTTP server entry point
│ ├── 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 # App identity and deployed connection
Backend (api/)
app.ts
The application core configures the MCP runtime. Register tools and resources here:
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],
});
The existing app factory wraps runtime.fetch with logging and withMcpApiRoute(). Keep that routing adapter: it serves the public endpoint at /api/mcp and rejects /mcp.
main.bun.ts
The Bun entry imports the application core and starts the HTTP server:
import { app } from "./app.ts";
Bun.serve({
hostname: "0.0.0.0",
port: Number(process.env.PORT) || 3001,
fetch: app.fetch,
});Other hosting targets need their own entry and compatible resource storage; the pinned starter's entry runs on Bun.
tools/
Tool definitions organized by domain. Each file exports a tool creator function that receives the environment.
tools/index.tsexports thetoolsarray — all tool creators registered here- See 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 for details
types/
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 extendingDefaultEnv, available in tool handlers
Frontend (web/)
Entry point (web/app.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>,
);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 viauseApp(), handles events (ontoolinput,ontoolresult,ontoolcancelled,onerror,onhostcontextchanged), applies host stylesuseMcpState<TInput, TResult>()— access current MCP state (status, tool input/result)useMcpApp()— access the MCP App instance (e.g., forapp.sendMessage())useMcpHostContext()— access host context (safe area insets, tool info)
Router (web/router.tsx)
Uses TanStack Router with hash-based history:
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 singledist/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
{
"scopeName": "deco",
"name": "mcp-app",
"friendlyName": "MCP App Template",
"description": "Starter template for building MCP Apps on deco.",
"connection": {
"type": "HTTP",
"url": "https://mcp.example.com/api/mcp",
"configSchema": {
"type": "object",
"properties": {},
"required": []
}
}
}scopeName+name— unique identifier for your app in the deco platformfriendlyName— display name shown to usersconnection.url— your deployed MCP endpointconfigSchema— JSON Schema defining configuration options shown to installers
.mcp.json
For a local MCP client, create this configuration in the client that supports this format:
{
"mcpServers": {
"mcp-app": {
"type": "http",
"url": "http://localhost:3001/api/mcp"
}
}
}Hosted Studio needs a reachable HTTPS URL, rather than your machine's localhost. See Testing tools for the connection workflow.
Development Flow
- Create a tool in
api/tools/my-tool.ts - Register it in
api/tools/index.ts - Create a UI component in
web/tools/my-tool/index.tsx - Register the page in
web/router.tsx(TOOL_PAGESmap) - Create a resource in
api/resources/my-tool.ts - Register the resource in
api/app.ts(resourcesarray) - Run
bun run devand test