Nitro

An MCP endpoint as a Nitro route.

Nitro 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.

Read more in github.com/h3js/mcp/tree/main/examples/nitro.

#The Route

npm i 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. Name it mcp.post.ts and the legacy era breaks.

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:

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 — the same code path works in nitro dev and in a built output, on every preset:

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 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 in Deploy Anywhere.

h3-mcp  MCP servers, built on H3.