Exports
Everything h3-mcp exports at runtime.
The runtime surface is deliberately small: one handler factory, four identity helpers for type inference, one error class, and the multi-round-trip helpers. Everything else is types.
#defineMcpHandler(handlerOpts)
Define an H3 event handler that implements the Model Context Protocol (MCP) over HTTP using JSON-RPC 2.0 as the wire format.
The handler is dual-era: a POST whose params._meta["io.modelcontextprotocol/protocolVersion"] is set is served statelessly per 2026-07-28; anything else (including initialize and MCP-Session-Id traffic) takes the legacy ≤2025-11-25 path. Use era: "modern" or era: "legacy" to serve only one.
To also drop the unused era from your bundle, import from h3-mcp/modern or h3-mcp/legacy instead — those entrypoints never reference the other era's implementation.
Example:
app.all(
"/mcp",
defineMcpHandler({
name: "my-server",
version: "1.0.0",
tools: [echoTool],
resources: [readmeResource],
prompts: [greetPrompt],
}),
);Example:
// Dynamic options based on request context
app.all(
"/mcp",
defineMcpHandler((event) => ({
name: "my-server",
version: "1.0.0",
tools: getToolsForUser(event),
})),
);#definePrompt(definition)
Define an MCP prompt with optional arguments and a handler that returns messages.
arguments is either the declarative PromptArgument list — which can also carry per-argument complete callbacks — or a schema. A StandardJSONSchemaV1 implementor (Zod v4, ArkType, Valibot via toStandardJsonSchema()) is validated before the handler runs and its output type is inferred onto args.
Example:
// Prompt with declared arguments
const greetPrompt = definePrompt({
name: "greet",
description: "Generate a greeting",
arguments: [{ name: "name", required: true }],
handler: async (args, event) => ({
messages: [{ role: "user", content: { type: "text", text: `Hello ${args.name}!` } }],
}),
});Example:
// Prompt with schema-validated arguments
const reviewPrompt = definePrompt({
name: "review",
arguments: z.object({ path: z.string(), depth: z.enum(["quick", "deep"]) }),
handler: async (args, event) => ({
// args: { path: string; depth: "quick" | "deep" }
messages: [
{ role: "user", content: { type: "text", text: `${args.depth} review of ${args.path}` } },
],
}),
});Example:
// Prompt without arguments
const helpPrompt = definePrompt({
name: "help",
description: "Show help information",
handler: async (event) => ({
messages: [{ role: "user", content: { type: "text", text: "How can I help?" } }],
}),
});#defineResource(definition)
Define an MCP resource with a static URI and a handler that returns its contents.
Example:
const readmeResource = defineResource({
name: "readme",
uri: "file:///readme",
description: "Project README",
mimeType: "text/markdown",
handler: async (uri, event) => ({
contents: [{ uri: uri.toString(), text: "# My Project" }],
}),
});#defineResourceTemplate(definition)
Define an MCP resource template with a URI template (RFC 6570) and a handler.
Resource templates expose parameterized resources. Clients fill in template parameters and call resources/read with the expanded URI.
Example:
const fileTemplate = defineResourceTemplate({
name: "file",
uriTemplate: "file:///{path}",
description: "Read a file by path",
mimeType: "text/plain",
handler: async (uri, variables, event) => ({
contents: [{ uri: uri.toString(), text: `Contents of ${variables.path}` }],
}),
});#defineTool(definition)
Define an MCP tool with a name, optional JSON Schema input, and a handler function.
Example:
// Tool with validated input and output
const echoTool = defineTool({
name: "echo",
description: "Echo back a message",
inputSchema: z.object({ message: z.string() }),
outputSchema: z.object({ message: z.string() }),
handler: async ({ message }, event) => ({
content: [{ type: "text", text: message }],
structuredContent: { message },
}),
});Example:
// Tool without input parameters
const pingTool = defineTool({
name: "ping",
description: "Returns pong",
handler: async (event) => ({
content: [{ type: "text", text: "pong" }],
}),
});#McpJsonRpcError()
#McpJsonRpcError
A JSON-RPC error with an explicit code and HTTP status — the one error class whose message reaches the client verbatim on the modern era.
import { McpJsonRpcError } from "h3-mcp";
throw new McpJsonRpcError(-32602, "Widget id must be a UUID", { status: 400 });new McpJsonRpcError(code, message, { status?, data? }). When status is omitted it defaults per code: -32601 → 404, -32603 → 500, otherwise 400. McpJsonRpcError.isMcpJsonRpcError(value) is a type guard.
#MRTR Helpers
Multi round-trip requests are modern-only, so these are exported from h3-mcp and h3-mcp/modern — not from h3-mcp/legacy.
| Export | Purpose |
|---|---|
inputRequired(event, spec) | Build the resultType: "input_required" interim result to return from a handler |
mcpElicit(params) · mcpElicitUrl(params) | An elicitation/create request — a form, or an out-of-band URL |
createMessage(params) | A sampling/createMessage request |
listRoots() | A roots/list request |
getInputResponses(event, requests) | The answers that arrived, typed by the requests that asked for them |
getElicitedContent(event, requests, key) | The content of an accepted elicitation — key checked against the map, fields typed by its schema; undefined for missing/declined/cancelled |
defineRequestState(options) | An HMAC-SHA256 { seal, open } codec for requestState |
import { defineRequestState, getElicitedContent, mcpElicit, inputRequired } from "h3-mcp";inputRequired refuses to build a result the wire cannot carry: a legacy request, a capability the client never declared (-32021), an empty spec, or a requestState longer than the retry could echo back.
#Task Helpers
The tasks extension is a plugin, shipped from its own entrypoint. Nothing is exported from h3-mcp, h3-mcp/modern or h3-mcp/legacy — install it explicitly, and a server that does not serve it never bundles it.
| Export | Purpose |
|---|---|
mcpTasks(options?) | The plugin, for defineMcpHandler's plugins list — { enabled?, max?, ttlMs?, pollIntervalMs?, bind? } |
mcpTask(event, spec) | Start background work and return the resultType: "task" handle from a tools/call handler |
canCreateTask(event) | Whether this request may be answered with a task — check before falling back to synchronous |
TASKS_EXTENSION | The io.modelcontextprotocol/tasks identifier |
import { defineMcpHandler } from "h3-mcp/modern"; // or "h3-mcp"
import { canCreateTask, mcpTask, mcpTasks } from "h3-mcp/tasks";
export default defineMcpHandler(options, { extensionPlugins: [mcpTasks()] });The helpers work from any server entrypoint — importing mcpTask from h3-mcp/tasks while the handler comes from h3-mcp/modern is the normal case, not a hazard.
mcpTask refuses to mint a handle the wire cannot carry: a legacy request, a request the plugin's enabled gate turned the extension off for, or a client that never declared the extension (-32021). Installing the plugin is enabling the extension — there is no separate option to forget, and nothing to fail closed on.
#App Helpers
MCP Apps is a plugin too, from its own entrypoint. h3-mcp/ui is the smallest entry in the package (2.4kb minified) and pulls in no transport, so importing canRenderApp in a handler costs almost nothing.
| Export | Purpose |
|---|---|
mcpUI(options) | The plugin, for defineMcpHandler's extensionPlugins list — { apps, enabled? } |
defineApp(app) | Identity helper for an app: uri, name, html, tools, plus csp / permissions / domain / prefersBorder |
canRenderApp(event) | Whether this request's client can render an app — check before deciding what a tool returns |
UI_EXTENSION | The io.modelcontextprotocol/ui identifier |
APP_MIME_TYPE | text/html;profile=mcp-app |
import { defineMcpHandler } from "h3-mcp/modern"; // or "h3-mcp"
import { canRenderApp, defineApp, mcpUI } from "h3-mcp/ui";
export default defineMcpHandler(options, { extensionPlugins: [mcpUI({ apps: [dashboard] })] });Types: AppDefinition, AppCsp, AppPermissions, AppResourceMeta, AppTool, AppToolMeta, AppVisibility, UIOptions and UIState, from h3-mcp/ui only.
#Skill Helpers
Skills over MCP is the third plugin, from h3-mcp/skills. Like the other two it pulls in no transport — only the era-neutral request scope, params, pagination and error helpers — so it can be imported wherever the skill definitions live.
| Export | Purpose |
|---|---|
mcpSkills(options) | The plugin, for defineMcpHandler's extensionPlugins list — { skills, directoryRead?, cache?, enabled? } |
defineSkill(skill) | Identity helper for a skill: the root uri, its files, plus frontmatter / dynamic / cache |
parseFrontmatter(text) | The SKILL.md YAML frontmatter as the entry publishes it — the same parser the plugin runs, for checking a file |
SKILLS_EXTENSION | The io.modelcontextprotocol/skills identifier |
DIRECTORY_MIME_TYPE | inode/directory, the mimeType of a directory resource |
import { defineMcpHandler } from "h3-mcp/modern"; // or "h3-mcp"
import { defineSkill, mcpSkills } from "h3-mcp/skills";
export default defineMcpHandler(options, {
extensionPlugins: [mcpSkills({ skills: [refunds], directoryRead: true })],
});Types: Skill, SkillContent, SkillDefinition, SkillFile, SkillFrontmatter, SkillResource, SkillsOptions and SkillsState, from h3-mcp/skills only.
#OAuth Helpers
Import the OAuth metadata handler and token verifiers from h3-mcp/oauth. Every server entry already includes 401/403 challenges, scope checks, and event.context.mcp.auth. The verifiers return the identity used by those checks.
| Export | Purpose |
|---|---|
protectedResourceMetadata(options) | An h3 handler for the RFC 9728 document — { resource, authorizationServers, scopesSupported?, resourceName?, resourceDocumentation? } |
jwtValidator(options) | Verifies JWT access tokens (RFC 9068) with a JWKS and WebCrypto — { issuer, audience, jwksUri | keys, … } |
introspectionValidator(options) | Checks tokens with the authorization server (RFC 7662) — { endpoint, audience, clientId?, clientSecret?, … } |
dpopValidator(options) | Verifies a DPoP proof (RFC 9449) on top of either — { validate, resource, algorithms?, maxAgeMs?, clockToleranceMs?, replay?, replayMax?, nonce? } |
import { defineMcpHandler } from "h3-mcp"; // or "h3-mcp/modern", "h3-mcp/legacy"
import { jwtValidator, protectedResourceMetadata } from "h3-mcp/oauth";
app.get(
"/.well-known/oauth-protected-resource",
protectedResourceMetadata({
resource: "https://mcp.example.com/mcp",
authorizationServers: ["https://auth.example.com"],
scopesSupported: ["mcp:invoke"],
}),
);
app.all(
"/mcp",
defineMcpHandler({
name: "my-server",
version: "1.0.0",
auth: {
schemes: ["bearer"],
resourceMetadataUrl: "https://mcp.example.com/.well-known/oauth-protected-resource",
scopes: ["mcp:invoke"],
validate: jwtValidator({
issuer: "https://auth.example.com",
audience: "https://mcp.example.com/mcp",
jwksUri: "https://auth.example.com/.well-known/jwks.json",
}),
},
}),
);Mount the metadata handler at /.well-known/oauth-protected-resource or an endpoint-specific path such as /.well-known/oauth-protected-resource/mcp. Set auth.resourceMetadataUrl to that URL. The document is fixed and always includes bearer_methods_supported: ["header"].
All four helpers validate options when created. At request time, the verifiers return false or a reason (AuthFailure) instead of throwing if verification fails; the server responds with 401. They have no dependencies and use WebCrypto.
Types exported only from h3-mcp/oauth: ProtectedResourceMetadataOptions, JwtValidatorOptions, JwtAlgorithm, Jwk, IntrospectionValidatorOptions, ValidatorOptions, DpopValidatorOptions, and DpopReplayStore.
This entry also re-exports AuthIdentity, AuthValidator, AuthCredentials, AuthVerdict, AuthFailure, and AuthBinding.
#Plugin Toolkit
Writing an extension plugin? These are the helpers its methods need to behave like the library's own. Modern-only, so they are exported from h3-mcp and h3-mcp/modern — not from h3-mcp/legacy.
| Export | Purpose |
|---|---|
mcpMethod(ctx, req, event, handler) | Run a plugin method in the request scope — event.context.mcp.signal fires when the stream closes |
pluginState(event, plugin) | This plugin's per-request state, typed by the instance (what its initEvent returned) |
parseParams(params) | JSON-RPC params as a plain object, or -32602 |
requireParamString(params, field) | A required, non-empty string field |
protocolError(status, message, data?) | A validation failure whose message is safe to report verbatim |
invalidParamsError(message) | -32602 with a message |
isToolExecutionThrow(error) | Whether a throw is the model's problem rather than the client's |
toolErrorText(error) | The redacted text for an { isError: true } result |
import { mcpMethod, parseParams, requireParamString, type ExtensionPlugin } from "h3-mcp/modern";Two rules these exist to keep enforceable outside the library: validate client input at the boundary (never as string), and never echo untrusted input in an error message — protocolError(404, "Note not found"), not the id that was not found. isToolExecutionThrow / toolErrorText are the other half: a protocol error collapses its message for the client, a tool execution throw becomes isError content for the model.
Types: ExtensionPlugin, PluginContext, PluginDefinitions and PluginOptions are exported from h3-mcp, h3-mcp/modern, h3-mcp/tasks, h3-mcp/ui and h3-mcp/skills.
#Era Entrypoints
h3-mcp/modern and h3-mcp/legacy export the same helpers and error class, plus a single-era defineMcpHandler and the version it serves:
| Export | h3-mcp | h3-mcp/modern | h3-mcp/legacy |
|---|---|---|---|
defineMcpHandler | ✅ dual | ✅ modern-only | ✅ legacy-only |
defineTool | ✅ | ✅ | ✅ |
defineResource | ✅ | ✅ | ✅ |
defineResourceTemplate | ✅ | ✅ | ✅ |
definePrompt | ✅ | ✅ | ✅ |
McpJsonRpcError | ✅ | ✅ | ✅ |
| MRTR helpers (above) | ✅ | ✅ | — |
| Plugin toolkit (above) | ✅ | ✅ | — |
PROTOCOL_VERSION | — | ✅ 2026-07-28 | — |
LEGACY_PROTOCOL_VERSION | — | — | ✅ 2025-11-25 |
| all types | ✅ | ✅ | ✅ |
Each entrypoint is self-contained — never import from two of them in the same file, or you defeat the bundle split.
#Types
Every type is exported from each entrypoint. The ones you are most likely to name explicitly:
| Type | Use |
|---|---|
HandlerOptions | The options object |
ToolDefinition · ResourceDefinition · ResourceTemplateDefinition · PromptDefinition | Definition shapes |
ResourceDescriptor | A listed resource, as a template's list returns it |
CallToolResult · ReadResourceResult · GetPromptResult | Handler return values |
ContentBlock · Icon | Content and icons |
RequestContext | event.context.mcp |
InputRequiredResult · InputRequests · InputResponses | MRTR |
ElicitParams · ElicitResult · CreateMessageParams · ListRootsResult | MRTR request/response payloads |
RequestStateOptions · RequestStateCodec | Sealed requestState |
Subscription · SubscriptionFilter · SubscriptionOptions | Subscriptions |
CacheOptions · CacheHints · CacheScope | Caching |
AuthOptions · AuthIdentity · AuthValidator · AuthCredentials · AuthFailure | Auth |
OriginOptions · LimitsOptions · SessionOptions | Security |
Era · Implementation · ClientCapabilities · Extensions | Era and identity |
StandardSchemaV1 · StandardJSONSchemaV1 · StandardTypedV1 | Standard Schema (vendored) |
h3-mcp also augments h3's H3EventContext with mcp?: RequestContext, so event.context.mcp is typed everywhere once the package is imported.
#Deprecated but Functional
These are legacy-era features removed in 2026-07-28. They still work and are not going away — the JSDoc @deprecated marks are there to point you at the modern replacement:
| Deprecated | Modern replacement |
|---|---|
session | stateless requests; explicit handles in tool arguments |
onStream | onListen (subscriptions) |
onSubscribe / onUnsubscribe | onListen |
onLogLevelSet | _meta log level + mcp.log() |