
# Nitro

> An MCP endpoint as a Nitro route.

[Nitro](https://nitro.build) builds on H3, so an MCP handler is an ordinary file-based route — and it inherits everything Nitro gives you: filesystem routing, the storage layer, and a build that targets [any preset](https://nitro.build/deploy).

::read-more{to="https://github.com/h3js/mcp/tree/main/examples/nitro"}
[`examples/nitro/`](https://github.com/h3js/mcp/tree/main/examples/nitro) is this page as a runnable starter — copy the directory, `npm install`, `npm run dev`.
::

## The Route

:pm-install{name="h3-mcp zod"}

A route file with **no method suffix** answers every method, which is exactly what MCP needs: `POST` for JSON-RPC, plus `GET`/`DELETE` for the [legacy session transport](/protocols/legacy/sessions). Name it `mcp.post.ts` and the legacy era breaks.

```ts [server/routes/mcp.ts]
import { defineMcpHandler } from "h3-mcp";

import greetTool from "../mcp/tools/greet.ts";

export default defineMcpHandler({
  name: "my-server",
  version: "1.0.0",
  tools: [greetTool],
});
```

That serves `/mcp` when `serverDir` is `./server`. Definitions are plain objects, so where they live is your call — one file per tool under `server/mcp/` keeps the route readable:

```ts [server/mcp/tools/greet.ts]
import { z } from "zod";
import { defineTool } from "h3-mcp";

export default defineTool({
  name: "greet",
  description: "Greets someone by name",
  inputSchema: z.object({ name: z.string().describe("Who to greet") }),
  outputSchema: z.object({ greeting: z.string() }),
  handler: async ({ name }) => {
    const greeting = `Hello, ${name}!`;
    return { content: [{ type: "text", text: greeting }], structuredContent: { greeting } };
  },
});
```

## Resources From Server Assets

Nitro bundles everything in `assets/` into the server and mounts it on the `assets/server` storage key, which makes it a natural backing store for a [resource](/guide/resources) — the same code path works in `nitro dev` and in a built output, on every preset:

```ts [server/mcp/resources/notes.ts]
import { useStorage } from "nitro/storage";
import { defineResource } from "h3-mcp";

export default defineResource({
  name: "notes",
  uri: "file:///notes.md",
  mimeType: "text/markdown",
  handler: async (uri) => {
    const text = await useStorage("assets/server").getItem<string>("notes.md");
    return { contents: [{ uri: uri.toString(), mimeType: "text/markdown", text: text ?? "" }] };
  },
});
```

> [!NOTE]
> Server assets resolve from the **project root**, not from `serverDir` — `assets/notes.md`, not `server/assets/notes.md`. A file in the wrong place reads back as `undefined` rather than erroring.

## Deploying

`nitro build` bundles the handler with the rest of your app; nothing in `h3-mcp` needs a filesystem or a background timer beyond the per-stream keep-alive. Two preset-shaped caveats:

- **Serverless: prefer stateless.** [Legacy sessions](/protocols/legacy/sessions) live in process memory, so they break as soon as more than one instance serves the endpoint. Serve `era: "modern"`, or keep session state in a Nitro storage mount keyed by the session id.
- **Trim the bundle.** If you only serve one era, import from `h3-mcp/modern` or `h3-mcp/legacy` and drop the other from the output.

:read-more{to="/examples/deploy" title="Deploy Anywhere"}
