Skip to content

MCP Servers

MCP Servers

@workkit/mcp is “Hono for MCP” — type-safe Model Context Protocol servers on Cloudflare Workers. One tool/resource/prompt definition gives you the MCP JSON-RPC endpoint, a REST surface, and an OpenAPI spec for free. Built on Standard Schema so you can validate with Zod, Valibot, ArkType, or any compliant vendor.

Install

Terminal window
bun add @workkit/mcp @workkit/errors hono zod

zod is only required if you use it as the validator in the examples below. Any Standard Schema implementation works (Valibot, ArkType, etc.).

Quick start

import { createMCPServer } from "@workkit/mcp";
import { z } from "zod";
const server = createMCPServer({
name: "weather-mcp",
version: "1.0.0",
basePath: "/api",
mcpPath: "/mcp",
openapi: { enabled: true, swaggerUI: true },
});
server.tool("get-weather", {
description: "Fetch current weather for a city",
input: z.object({ city: z.string() }),
output: z.object({ tempC: z.number(), conditions: z.string() }),
annotations: { readOnlyHint: true, openWorldHint: true },
handler: async (ctx) => {
const data = await fetchWeather(ctx.input.city);
return { tempC: data.temp, conditions: data.summary };
},
});
export default server.serve();

That single registration gives you these routes:

PathWhereWhat
POST {mcpPath} (default /mcp)configurableMCP JSON-RPC transport
POST {basePath}/tools/:toolName (default /api/tools/:toolName)configurableREST shortcut for one tool
GET /openapi.jsonrootOpenAPI 3.1 spec (when openapi.enabled !== false)
GET /docsrootSwagger UI shell (when openapi.swaggerUI: true)
GET /healthroot{"status":"ok"} (when health !== false)

Note that openapi.json, /docs, and /health mount at the root of the app — not under basePath. That keeps the meta-endpoints stable for tooling.

Resources and prompts

server.resource("doc://readme", {
description: "Project README",
mimeType: "text/markdown",
handler: async () => ({ contents: [{ uri: "doc://readme", text: "# Hello" }] }),
});
server.prompt("explain-code", {
description: "Explain a code snippet",
args: z.object({ language: z.string(), snippet: z.string() }),
handler: async (ctx) => ({
messages: [{ role: "user", content: { type: "text", text: `Explain this ${ctx.input.language}:\n${ctx.input.snippet}` } }],
}),
});

Tool annotations

Hints follow the MCP 2025-06 spec — clients use them to apply guardrails:

AnnotationMeaning
readOnlyHintTool does not modify state
destructiveHintTool may delete or overwrite data
idempotentHintRepeated calls are safe
openWorldHintTool reaches external services

Authentication

MCPAuthConfig.handler is a Hono-style middleware: (request, env, next) => Response | Promise<Response>. Use it to verify the token and short-circuit unauthenticated requests; otherwise call next().

const server = createMCPServer({
name: "secure-api",
version: "1.0.0",
auth: {
type: "bearer",
handler: async (request, env, next) => {
const token = request.headers.get("authorization")?.replace(/^Bearer\s+/i, "");
if (!token || !(await verifyJwt(token, env))) {
return new Response("unauthorized", { status: 401 });
}
return next();
},
exclude: ["/openapi.json", "/docs"],
},
});

type is informational metadata for clients; the actual verification lives in your handler. exclude lists paths the middleware skips.

Sessions (Durable Objects) — roadmap

Stateful tools will opt into DO-backed sessions via the session config:

const server = createMCPServer({
name: "stateful",
version: "1.0.0",
session: { storage: "durable-object", ttl: 3600, maxSessions: 1000 },
});

The config type is reserved on MCPServerConfig but the runtime wiring (DO class, session lookup header, sessionId propagation into handler context) lands in a follow-up release — track issue #46 for the design.

Middleware

Hono-style middleware composes per server, per tool, or globally:

server.tool("admin-action", {
description: "Admin-only",
input: z.object({ targetId: z.string() }),
middleware: [requireRole("admin"), rateLimit({ requests: 10, window: "1m" })],
handler: async (ctx) => { /* ... */ },
});

Validation

input and output are typed as StandardSchemaV1<T>. Runtime validation runs before the handler — invalid inputs return a JSON-RPC error with code: -32602 (Invalid params). Output validation runs after — schema mismatches surface as -32603 (Internal error) and never reach the client.

Mounting on an existing Hono app

createMCPServer().toHono() returns a Hono instance you can mount inside your own app:

import { Hono } from "hono";
const app = new Hono();
app.get("/health", (c) => c.text("ok"));
app.route("/", server.toHono());
export default app;

If you need raw fetch handlers instead of a Hono app, call server.mount() (no args) — it returns { mcpHandler, restHandler, openapi } you can wire into any router.

See also