
# MCP Handler

> Mount an MCP server on any H3 route.

`defineMcpHandler` returns a plain H3 [`EventHandler`](https://h3.dev/guide/basics/handler). It owns everything below the JSON-RPC layer — transport checks, era dispatch, parameter validation, pagination — and calls your definitions when a client asks for them.

```ts
import { H3 } from "h3";
import { defineMcpHandler } from "h3-mcp";

const app = new H3();

app.all(
  "/mcp",
  defineMcpHandler({
    name: "my-server",
    version: "1.0.0",
    title: "My Server",
    instructions: "Ask me about the weather.",
    tools: [/* ... */],
    resources: [/* ... */],
    resourceTemplates: [/* ... */],
    prompts: [/* ... */],
  }),
);
```

Only `name` and `version` are required. Everything else is opt-in, and capabilities are advertised from what you actually declared — define no prompts and the server does not claim the `prompts` capability.

:read-more{to="/guide/api/options" title="All handler options"}

## Mount with `app.all()`

MCP traffic is `POST`, but the legacy era also uses `GET` (SSE stream) and `DELETE` (session teardown) on the same path. Register every method and let the handler answer for itself:

```ts
app.all("/mcp", handler); // ✅ handler answers 405 for what it doesn't serve
app.post("/mcp", handler); // ⚠️ legacy GET/DELETE would 404 instead of 405
```

A modern-only server replies `405` with `allow: POST`; a dual or legacy server replies `405` with `allow: POST, GET, DELETE`.

## Server Identity

Identity fields flow into `initialize` (legacy) and `server/discover` plus `_meta["io.modelcontextprotocol/serverInfo"]` (modern):

```ts
defineMcpHandler({
  name: "my-server",
  version: "1.0.0",
  title: "Weather Server", // human-friendly display name
  description: "Forecasts and observations",
  websiteUrl: "https://example.com",
  icons: [{ src: "https://example.com/icon.png", mimeType: "image/png", sizes: ["48x48"] }],
  instructions: "Prefer `forecast` over `observations` for future dates.",
});
```

`instructions` is the one clients feed to the model, so it is worth writing carefully: describe when to use which tool, not what your server is.

## Dynamic Options

Pass a function instead of an object to resolve options per request — for example, to expose different tools per authenticated user:

```ts
defineMcpHandler((event) => ({
  name: "my-server",
  version: "1.0.0",
  tools: getToolsForUser(event),
  resources: getResourcesForTenant(event),
}));
```

The function runs at most once per request; the result is cached in a `WeakMap` keyed by the event, so it is garbage-collected with the request. Every option is dynamic, including `auth`, `origin`, `limits`, and `cache`.

> [!NOTE]
> Dynamic options are resolved before the era is dispatched, so the same function serves both eras. Use `event.req.headers` to branch — the request context is not populated yet.

## Lazy Definitions

`tools`, `resources`, `resourceTemplates`, and `prompts` also accept functions in the array. Each is called once on first use and cached, so an expensive definition never costs anything until a client lists or calls it:

```ts
defineMcpHandler({
  name: "my-server",
  version: "1.0.0",
  tools: [
    // Eager — defined inline
    defineMcpTool({
      name: "ping",
      handler: async () => ({ content: [{ type: "text", text: "pong" }] }),
    }),

    // Lazy — resolved once on first use
    async () => {
      const schema = await loadSchemaFromDB();
      return defineMcpTool({
        name: "query",
        description: "Run a query",
        inputSchema: schema,
        handler: async (args) => ({ content: [{ type: "text", text: "..." }] }),
      });
    },
  ],
});
```

With static options the resolved array lives for the handler's lifetime. With dynamic options it is cached per event, so a lazy entry runs once per request at most.

## Request Context

Inside any handler, `event.context.mcp` carries the state of the request in flight — negotiated protocol version, era, client info, the cancellation `signal`, and the notification helpers:

```ts
defineMcpTool({
  name: "whoami",
  handler: (event) => {
    const mcp = event.context.mcp!;
    return {
      content: [
        { type: "text", text: `${mcp.clientInfo?.name ?? "unknown"} on ${mcp.protocolVersion}` },
      ],
    };
  },
});
```

:read-more{to="/guide/api/request-context" title="Request context reference"}

## Multiple Endpoints

Nothing is global — mount as many handlers as you like, each with its own definitions, auth, and era:

```ts
app.all("/mcp", defineMcpHandler({ name: "public", version: "1.0.0", tools: publicTools }));
app.all(
  "/mcp/admin",
  defineMcpHandler({
    name: "admin",
    version: "1.0.0",
    tools: adminTools,
    auth: { tokens: [process.env.ADMIN_TOKEN!] },
  }),
);
```
