Getting Started
Get started with h3-mcp.
Warning
h3-mcp is still evolving and may introduce breaking changes.
#Overview
h3-mcp turns any H3 app into a Model Context Protocol server. It implements MCP as a self-contained JSON-RPC 2.0 layer — there is no MCP SDK dependency, and the whole library is ~34kb min / ~11kb gzip (~19kb / ~6kb if you serve a single protocol era).
You describe your server declaratively — tools, resources, prompts — and defineMcpHandler gives you back an ordinary H3 event handler you can mount anywhere:
- Every JSON-RPC method of the current spec is implemented for you, on both protocol eras.
- All client-supplied input (tool arguments, prompt arguments, resource URIs, cursors, headers) is validated at the transport boundary before it reaches your handler.
- Security defaults —
Originchecks, body-size andContent-Typelimits, private cache scopes — are on before you configure anything.
#Quick Start
Install h3 and h3-mcp as dependencies:
npm i h3 h3-mcpCreate a server entry:
import { H3, serve } from "h3";
import { defineMcpHandler, defineMcpTool } from "h3-mcp";
const app = new H3();
app.all(
"/mcp",
defineMcpHandler({
name: "my-server",
version: "1.0.0",
tools: [
defineMcpTool({
name: "hello",
description: "Say hello",
inputSchema: {
type: "object",
properties: { name: { type: "string" } },
required: ["name"],
},
handler: async ({ name }) => ({
content: [{ type: "text", text: `Hello, ${name}!` }],
}),
}),
],
}),
);
serve(app, { port: 3000 });Then run it with your favorite runtime:
node --watch ./server.tsYour MCP endpoint is live on http://localhost:3000/mcp. Ask it what it can do:
curl -X POST http://localhost:3000/mcp \
-H 'content-type: application/json' \
-H 'accept: application/json, text/event-stream' \
-H 'mcp-protocol-version: 2026-07-28' \
-H 'mcp-method: tools/list' \
-d '{
"jsonrpc": "2.0", "id": 1,
"method": "tools/list",
"params": {
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}'#What Happened?
We mounted a single handler with app.all():
app.all("/mcp", defineMcpHandler({/* ... */}));all matters: MCP is POST-driven, but the legacy era also uses GET (for the SSE stream) and DELETE (to tear down a session) on the same path. defineMcpHandler answers 405 itself for methods it does not serve, so it is safe to hand it every method.
Inside, we declared one tool with defineMcpTool:
defineMcpTool({
name: "hello",
inputSchema: {/* JSON Schema, or a Zod / Valibot / ArkType schema */},
handler: async ({ name }) => ({ content: [{ type: "text", text: `Hello, ${name}!` }] }),
});defineMcpTool is an identity function — it returns the definition unchanged and exists purely so TypeScript can infer your handler's argument type from inputSchema. The same holds for defineMcpResource, defineMcpResourceTemplate, and defineMcpPrompt.
That's the whole server. tools/list, tools/call, server/discover, initialize, pagination, and input validation are all handled for you.
#Try the Playground
The repository ships a playground server that exercises every feature — tools, resources, templates, prompts, sessions, auth, streaming, caching, MRTR:
git clone https://github.com/h3js/mcp
cd mcp && pnpm install
pnpm playIt serves a small web UI at http://localhost:6274/ and three endpoints — /mcp (dual), /mcp/modern, and /mcp/legacy — one per entrypoint, so you can compare eras against the same definitions.
#Connect a Client
Any MCP client works. For a local stdio-only client, bridge with mcp-remote:
{
"mcpServers": {
"my-server": {
"command": "npx",
"args": ["-y", "mcp-remote", "http://localhost:3000/mcp"]
}
}
}