Handler Options
Every field of HandlerOptions.
defineMcpHandler takes this object, or a function returning it:
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 |
resources | MaybeLazy<ResourceDefinition>[] | Resources |
resourceTemplates | MaybeLazy<ResourceTemplateDefinition>[] | Templates |
prompts | MaybeLazy<PromptDefinition>[] | Prompts |
MaybeLazy<T> is T | (() => T | Promise<T>), so any entry may be a function resolved once on first use.
#Era
| Option | Type | Default | Notes |
|---|---|---|---|
era | "dual" | "modern" | "legacy" | "dual" | Ignored on the h3-mcp/modern and h3-mcp/legacy entrypoints |
#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 |
#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 }).
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.
#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.
#Dynamic Resolution
Every option above can be computed per request:
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.