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
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
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
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:
| 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
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
- Agents —
@workkit/agentconsumes tools defined as Standard Schema. - Authentication — wire
@workkit/authinto theauth.handler. - MCP specification