Handler Options

Every field of HandlerOptions.

defineMcpHandler takes this object, or a function returning it:

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

#Identity

OptionTypeDefaultNotes
namestring—Required. Machine name of the server
versionstring—Required. Server version
titlestring—Human-friendly display name
descriptionstring—Short description
iconsIcon[]—{ src, mimeType?, sizes?, theme? }
websiteUrlstring—Server homepage
instructionsstring—Guidance passed to the model — worth writing well

#Definitions

OptionTypeNotes
toolsMaybeLazy<ToolDefinition>[]Tools
resourcesMaybeLazy<ResourceDefinition>[]Resources
resourceTemplatesMaybeLazy<ResourceTemplateDefinition>[]Templates
promptsMaybeLazy<PromptDefinition>[]Prompts

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

Read more in Lazy definitions.

#Era

OptionTypeDefaultNotes
era"dual" | "modern" | "legacy""dual"Ignored on the h3-mcp/modern and h3-mcp/legacy entrypoints
Read more in Protocol eras.

#Modern Era

OptionTypeDefaultNotes
cacheCacheOptions{ ttlMs: 0, cacheScope: "private" }Per-method keys: discover, tools, prompts, resources, resourceTemplates, resourceRead
extensionsRecord<string, object>—Advertised in capabilities.extensions
loggingbooleanfalseTurn logging on — both eras. Required for ctx.log() to emit; also gates logging/setLevel
echoServerInfobooleantrueInclude _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 in Caching.
Read more in Tasks.
Read more in MCP Apps.
Read more in 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 }).

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 })],
});
FieldTypeNotes
extensionPluginsExtensionPlugin[]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

OptionTypeDefaultNotes
sessionboolean | SessionOptionsfalse{ 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 in Legacy era.

#Security

OptionTypeDefaultNotes
authfalse | AuthOptionsfalse (disabled){ enabled?, schemes?, tokens?, header?, validate?, resourceMetadataUrl?, scopes?, impliesScope? }
originfalse | OriginOptionsenabled: { allow: [], allowMissing: true }{ allow?, allowMissing?, validate? }
limitsfalse | 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 in Security.

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

Read more in Dynamic options.

h3-mcp  MCP servers, built on H3.