
# Handler Options

> Every field of `HandlerOptions`.

`defineMcpHandler` takes this object, or a function returning it:

```ts
defineMcpHandler(options: HandlerOptions | ((event: H3Event) => HandlerOptions));
```

## Identity

| Option         | Type     | Default | Notes                                             |
| -------------- | -------- | ------- | ------------------------------------------------- |
| `name`         | `string` | —       | **Required.** Machine name of the server          |
| `version`      | `string` | —       | **Required.** Server version                      |
| `title`        | `string` | —       | Human-friendly display name                       |
| `description`  | `string` | —       | Short description                                 |
| `icons`        | `Icon[]` | —       | `{ src, mimeType?, sizes?, theme? }`              |
| `websiteUrl`   | `string` | —       | Server homepage                                   |
| `instructions` | `string` | —       | Guidance passed to the model — worth writing well |

## Definitions

| Option              | Type                                      | Notes                                            |
| ------------------- | ----------------------------------------- | ------------------------------------------------ |
| `tools`             | `MaybeLazy<ToolDefinition>[]`             | [Tools](/guide/tools)                            |
| `resources`         | `MaybeLazy<ResourceDefinition>[]`         | [Resources](/guide/resources)                    |
| `resourceTemplates` | `MaybeLazy<ResourceTemplateDefinition>[]` | [Templates](/guide/resources#resource-templates) |
| `prompts`           | `MaybeLazy<PromptDefinition>[]`           | [Prompts](/guide/prompts)                        |

`MaybeLazy<T>` is `T | (() => T | Promise<T>)`, so any entry may be a function resolved once on first use.

:read-more{to="/guide/handler#lazy-definitions" title="Lazy definitions"}

## Era

| Option | Type                             | Default  | Notes                                                          |
| ------ | -------------------------------- | -------- | -------------------------------------------------------------- |
| `era`  | `"dual" \| "modern" \| "legacy"` | `"dual"` | Ignored on the `h3-mcp/modern` and `h3-mcp/legacy` entrypoints |

:read-more{to="/protocols" title="Protocol eras"}

## Modern Era

| Option           | Type                            | Default                               | Notes                                                                                                |
| ---------------- | ------------------------------- | ------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| `cache`          | `CacheOptions`                  | `{ ttlMs: 0, cacheScope: "private" }` | Per-method keys: `discover`, `tools`, `prompts`, `resources`, `resourceTemplates`, `resourceRead`    |
| `extensions`     | `Record<string, object>`        | —                                     | Advertised in `capabilities.extensions`                                                              |
| `logging`        | `boolean`                       | `false`                               | Turn logging on — **both eras**. **Required** for `ctx.log()` to emit; also gates `logging/setLevel` |
| `echoServerInfo` | `boolean`                       | `true`                                | Include `_meta` server info on modern results                                                        |
| `subscriptions`  | `{ max?, keepAliveMs? }`        | `{ max: 100, keepAliveMs: 15000 }`    | `subscriptions/listen` limits                                                                        |
| `onListen`       | `(subscription, event) => void` | —                                     | Called when a listen stream opens                                                                    |

:read-more{to="/protocols/modern/caching" title="Caching"}

:read-more{to="/protocols/modern/tasks" title="Tasks"}

:read-more{to="/protocols/modern/apps" title="MCP Apps"}

:read-more{to="/protocols/modern/skills" title="Skills"}

### Plugins

Spec **extensions** are installed at construction, through `defineMcpHandler`'s second argument. Nothing is pre-installed, on any entrypoint — an extension you do not install is not in your bundle. A plugin also carries **its own configuration**, so there is no handler option for an extension and nothing to keep in sync: `mcpTasks({ max, ttlMs, pollIntervalMs, bind, enabled })`, `mcpUI({ apps, enabled })`, `mcpSkills({ skills, directoryRead, cache, enabled })`.

```ts
import { defineMcpHandler } from "h3-mcp/modern"; // or "h3-mcp"
import { mcpTasks } from "h3-mcp/tasks";
import { mcpUI } from "h3-mcp/ui";
import { mcpSkills } from "h3-mcp/skills";

export default defineMcpHandler(options, {
  extensionPlugins: [mcpTasks({ max: 100 }), mcpUI({ apps: [dashboard] }), mcpSkills({ skills })],
});
```

| Field              | Type                | Notes                                                     |
| ------------------ | ------------------- | --------------------------------------------------------- |
| `extensionPlugins` | `ExtensionPlugin[]` | Modern-era only; `h3-mcp/legacy` takes no second argument |

A plugin instance belongs to one handler — call the factory again for a second `defineMcpHandler`.

## Legacy Era

| Option          | Type                            | Default | Notes                                     |
| --------------- | ------------------------------- | ------- | ----------------------------------------- |
| `session`       | `boolean \| SessionOptions`     | `false` | `{ enabled?, generateId?, maxSessions? }` |
| `onStream`      | `(stream, event) => void`       | —       | Enables the `GET`/SSE stream              |
| `onSubscribe`   | `(uri, event) => void`          | —       | `resources/subscribe`                     |
| `onUnsubscribe` | `(uri, event) => void`          | —       | `resources/unsubscribe`                   |
| `onLogLevelSet` | `(level, event) => void`        | —       | `logging/setLevel`                        |
| `onCancelled`   | `(notification, event) => void` | —       | `notifications/cancelled`                 |
| `onProgress`    | `(notification, event) => void` | —       | `notifications/progress`                  |

All of these are `@deprecated` in the sense that `2026-07-28` removed the feature — they remain fully functional on the legacy path.

:read-more{to="/protocols/legacy/handshake" title="Legacy era"}

## Security

| Option   | Type                     | Default                                                                       | Notes                                                                                               |
| -------- | ------------------------ | ----------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| `auth`   | `false \| AuthOptions`   | `false` (disabled)                                                            | `{ enabled?, schemes?, tokens?, header?, validate?, resourceMetadataUrl?, scopes?, impliesScope? }` |
| `origin` | `false \| OriginOptions` | enabled: `{ allow: [], allowMissing: true }`                                  | `{ allow?, allowMissing?, validate? }`                                                              |
| `limits` | `false \| LimitsOptions` | `{ maxBodySize: 4 MiB, contentType: ["application/json"], maxBatchSize: 50 }` | Applied to every POST; `maxBatchSize` is legacy-only                                                |

Auth defaults, once enabled: `schemes: ["bearer", "api-key"]` (`dpop` is opt-in and needs `validate`), `header: "x-api-key"`. Enabling auth requires at least one of `tokens` or `validate`. `auth.scopes` sets the OAuth scopes required for every request. A tool, resource, template, or prompt can require additional scopes through its own `scopes` field.

:read-more{to="/security" title="Security"}

## Dynamic Resolution

Every option above can be computed per request:

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

The function runs at most once per event and the result is cached in a `WeakMap`.

:read-more{to="/guide/handler#dynamic-options" title="Dynamic options"}
