MCP Servers
MCP Servers
Section titled “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
Section titled “Install”bun add @workkit/mcp @workkit/errors hono zodzod is only required if you use it as the validator in the examples below. Any Standard Schema implementation works (Valibot, ArkType, etc.).
Quick start
Section titled “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:
| Path | Where | What |
|---|---|---|
POST {mcpPath} (default /mcp) |
configurable | MCP JSON-RPC transport |
POST {basePath}/tools/:toolName (default /api/tools/:toolName) |
configurable | REST shortcut for one tool |
GET /openapi.json |
root | OpenAPI 3.1 spec (when openapi.enabled !== false) |
GET /docs |
root | Swagger UI shell (when openapi.swaggerUI: true) |
GET /health |
root | {"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
Section titled “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
Section titled “Tool annotations”Hints follow the MCP 2025-06 spec — clients use them to apply guardrails:
| Annotation | Meaning |
|---|---|
readOnlyHint |
Tool does not modify state |
destructiveHint |
Tool may delete or overwrite data |
idempotentHint |
Repeated calls are safe |
openWorldHint |
Tool reaches external services |
Authentication
Section titled “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
Section titled “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
Section titled “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
Section titled “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
Section titled “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
Section titled “See also”- Agents —
@workkit/agentconsumes tools defined as Standard Schema. - Authentication — wire
@workkit/authinto theauth.handler. - MCP specification