Guide · HTTP Frameworks
MCP server Hono — edge-deployable MCP servers with Hono.js
Hono.js is a lightweight, standards-based web framework built on the Fetch API, which makes it uniquely suited for deploying MCP servers at the edge — on Cloudflare Workers, Deno Deploy, Bun, and Node.js with zero framework-specific shims. This guide covers the complete integration between Hono and the MCP SDK's StreamableHTTPServerTransport, including stateless session patterns for edge environments and monitoring edge-distributed MCP deployments with AliveMCP.
TL;DR
Create a Hono app, extract c.req.raw (the native Request) and pass it to StreamableHTTPServerTransport.handleRequest(), then return the transport's Response via c.res. For Cloudflare Workers, use wrangler deploy with a wrangler.toml pointing at your Hono entry. Connect AliveMCP to probe from multiple regions so you catch regional edge node failures before users in those regions do.
Why Hono for MCP edge servers
Traditional MCP server frameworks like Express and Fastify are Node.js-specific: they rely on IncomingMessage, ServerResponse, and the Node.js event loop. Hono's architecture is built entirely on the WHATWG Fetch API (Request, Response, Headers), which is now natively available on Cloudflare Workers, Deno, and Bun — no compatibility layer required.
npm install hono @modelcontextprotocol/sdk
npm install -D wrangler typescript
For Node.js compatibility, add @hono/node-server:
npm install @hono/node-server
The MCP SDK's StreamableHTTPServerTransport works with Node.js IncomingMessage and ServerResponse. On edge runtimes, you need a thin adapter that converts Hono's native Fetch API types into the Node.js stream interfaces the SDK expects. On Cloudflare Workers, this means extracting c.req.raw (a native Request) and constructing a Node.js-compatible shim — or using the MCP SDK's upcoming Fetch-native transport when it stabilizes.
For production edge deployments today, the most reliable approach is to use Node.js compatibility mode on Cloudflare Workers, which is now Generally Available and supports the full Node.js stream API. This lets you use StreamableHTTPServerTransport directly.
Hono app with StreamableHTTPServerTransport on Node.js
The simplest starting point runs Hono on Node.js using @hono/node-server. This gives you Hono's ergonomic routing and middleware while using the full Node.js stream API that the MCP SDK requires. The pattern for extracting the raw streams from Hono's context is the same regardless of whether you later deploy on a Node.js server or a Workers-compatible edge runtime.
// src/index.ts
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { logger } from 'hono/logger';
import { serve } from '@hono/node-server';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
import { v4 as uuidv4 } from 'uuid';
import { z } from 'zod';
const app = new Hono();
const sessions = new Map<string, StreamableHTTPServerTransport>();
app.use('*', logger());
app.use('/mcp/*', cors({
origin: process.env.ALLOWED_ORIGINS?.split(',') ?? '*',
allowHeaders: ['Content-Type', 'Mcp-Session-Id', 'Authorization'],
exposeHeaders: ['Mcp-Session-Id'],
credentials: true,
}));
// Health endpoint — monitored by AliveMCP from multiple regions
app.get('/health', (c) =>
c.json({ status: 'ok', sessions: sessions.size, ts: Date.now() })
);
app.post('/mcp', async (c) => {
const sessionId = c.req.header('mcp-session-id');
const body = await c.req.json();
if (sessionId && sessions.has(sessionId)) {
return new Promise<Response>((resolve, reject) => {
const transport = sessions.get(sessionId)!;
// @hono/node-server exposes req.raw and res members
transport.handleRequest(c.env.incoming, c.env.outgoing, body)
.then(() => resolve(c.res))
.catch(reject);
});
}
if (!isInitializeRequest(body)) {
return c.json({ error: 'Expected initialize request' }, 400);
}
const newId = uuidv4();
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => newId,
onsessioninitialized: (id) => { sessions.set(id, transport); },
});
transport.onclose = () => { sessions.delete(newId); };
const server = createMcpServer();
await server.connect(transport);
await transport.handleRequest(c.env.incoming, c.env.outgoing, body);
return c.res;
});
app.get('/mcp', async (c) => {
const sessionId = c.req.header('mcp-session-id');
if (!sessionId || !sessions.has(sessionId)) {
return c.json({ error: 'Unknown session' }, 404);
}
await sessions.get(sessionId)!.handleRequest(c.env.incoming, c.env.outgoing);
return c.res;
});
app.delete('/mcp', async (c) => {
const sessionId = c.req.header('mcp-session-id');
if (sessionId && sessions.has(sessionId)) {
await sessions.get(sessionId)!.close();
sessions.delete(sessionId);
}
return new Response(null, { status: 204 });
});
function createMcpServer(): McpServer {
const server = new McpServer({ name: 'hono-mcp', version: '1.0.0' });
server.tool('ping', 'Ping the server', {}, async () => ({
content: [{ type: 'text', text: 'pong' }],
}));
server.tool(
'greet',
'Greet a user',
{ name: z.string() },
async ({ name }) => ({
content: [{ type: 'text', text: `Hello, ${name}!` }],
})
);
return server;
}
serve({ fetch: app.fetch, port: Number(process.env.PORT ?? 3000) });
The c.env.incoming and c.env.outgoing bindings are provided by @hono/node-server and give you access to the underlying Node.js IncomingMessage and ServerResponse. This is the key to making StreamableHTTPServerTransport work inside a Hono handler.
Deploying on Cloudflare Workers with wrangler
Cloudflare Workers support Node.js compatibility mode via the nodejs_compat flag, which enables the full Node.js stream API inside the Workers runtime. This lets you run StreamableHTTPServerTransport without modification on Workers' V8 isolates — providing sub-millisecond cold starts and global edge distribution.
# wrangler.toml
name = "my-mcp-server"
main = "src/worker.ts"
compatibility_date = "2026-01-01"
compatibility_flags = ["nodejs_compat"]
[vars]
ALLOWED_ORIGINS = "https://myapp.example.com"
[[routes]]
pattern = "mcp.example.com/*"
zone_name = "example.com"
The worker entry file exports a fetch handler that Cloudflare invokes for every incoming request:
// src/worker.ts
import { Hono } from 'hono';
import { cors } from 'hono/cors';
// On Workers, sessions cannot be stored in-process between requests
// because each isolate may be a different instance. Use stateless
// per-request MCP sessions with Durable Objects for state, or design
// your tools to be stateless.
const app = new Hono<{ Bindings: CloudflareBindings }>();
app.use('/mcp/*', cors());
app.get('/health', (c) => c.json({ status: 'ok', region: c.req.header('cf-ray') }));
app.post('/mcp', async (c) => {
const body = await c.req.json();
// Each Workers request gets a fresh McpServer — stateless pattern
const { McpServer } = await import('@modelcontextprotocol/sdk/server/mcp.js');
const { StreamableHTTPServerTransport } = await import('@modelcontextprotocol/sdk/server/streamableHttp.js');
const server = new McpServer({ name: 'cf-worker-mcp', version: '1.0.0' });
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => crypto.randomUUID(),
});
await server.connect(transport);
await transport.handleRequest(c.env.incoming, c.env.outgoing, body);
return c.res;
});
export default { fetch: app.fetch };
Deploy with:
npx wrangler deploy
Note the stateless session pattern for Workers: because Cloudflare distributes requests across many isolates, you cannot rely on an in-process Map to hold sessions. Either use Cloudflare Durable Objects to provide stateful session storage, or design your MCP server with stateless tools that don't require session continuity. See the Cloudflare Workers MCP guide for a full Durable Objects implementation.
Deploying on Deno Deploy and Bun
Hono's multi-runtime support means the same application code runs on Deno Deploy and Bun with only the entry point changing. Deno Deploy is particularly convenient for MCP servers because it provides automatic HTTPS, global CDN, and no cold-start latency for TypeScript workloads.
// src/deno-entry.ts (for Deno Deploy)
import { Hono } from 'npm:hono';
import { serve } from 'https://deno.land/std/http/server.ts';
const app = new Hono();
// Same routes as Node.js version — Deno supports Node.js streams via npm: compat
app.get('/health', (c) => c.json({ status: 'ok', runtime: 'deno' }));
// ... rest of route definitions
Deno.serve({ port: 8000 }, app.fetch);
# Deploy to Deno Deploy via deployctl
deployctl deploy --project=my-mcp-server src/deno-entry.ts
For Bun, Hono provides a native adapter:
// src/bun-entry.ts
import { Hono } from 'hono';
import { serve } from 'bun';
const app = new Hono();
app.get('/health', (c) => c.json({ status: 'ok', runtime: 'bun' }));
// ... routes
export default {
port: Number(Bun.env.PORT ?? 3000),
fetch: app.fetch,
};
bun run src/bun-entry.ts
The Deno MCP server guide covers Deno-specific patterns including permission flags and import maps. The Bun MCP server guide covers Bun's native test runner integration for MCP tool testing.
Regional monitoring for edge-deployed MCP servers
Edge deployments are the scenario where external uptime monitoring matters most. When your MCP server runs on Cloudflare Workers or Deno Deploy, it's distributed across dozens of geographic regions simultaneously. A routing misconfiguration, a bad deploy pushed to only some PoPs, or a Workers KV outage can affect users in specific regions while leaving others unaffected — and you'll never see it in your error rates because the affected region's requests simply stop arriving.
# Check current Workers deployment status
npx wrangler deployments list
# Tail real-time logs from all regions
npx wrangler tail --format=pretty
# Test from a specific region via curl with Cloudflare's anycast
curl -H "Mcp-Session-Id: test" https://mcp.example.com/health
AliveMCP probes your MCP endpoint from multiple geographic regions — North America, Europe, Asia-Pacific — every minute. If users in Frankfurt experience failures while users in Virginia are fine, AliveMCP's regional probes will detect the discrepancy and alert you with region-specific failure details. You can configure regional alert thresholds at alivemcp.com — for example, alerting only when more than two regions fail simultaneously (to avoid false alarms from single-PoP hiccups) versus alerting on any single-region failure for latency-sensitive deployments.
# Hono middleware for request logging with region context
app.use('*', async (c, next) => {
const start = Date.now();
await next();
const region = c.req.header('cf-ray')?.split('-')[1] ?? 'unknown';
console.log(JSON.stringify({
method: c.req.method,
path: c.req.path,
status: c.res.status,
region,
ms: Date.now() - start,
}));
});
The cf-ray header on Cloudflare Workers contains the three-letter airport code of the PoP that handled the request, making it easy to correlate AliveMCP alerts with specific Cloudflare regions in your logs.
Frequently asked questions
Can I use Hono middleware (like auth) with my MCP routes?
Yes. Hono's middleware chain runs before your route handler, so you can add authentication, logging, and rate limiting as standard Hono middleware on the /mcp path prefix. For example, app.use('/mcp/*', bearerAuth({ token: Bun.env.MCP_TOKEN })) using Hono's built-in bearer auth middleware. The one caveat is that middleware which consumes the request body (calling c.req.json()) will buffer the body into memory — you need to ensure your MCP route handler reads the already-buffered body rather than trying to read the raw stream a second time.
Why can't I use persistent session state in Cloudflare Workers?
Cloudflare Workers run in V8 isolates that can be started and stopped by the platform at any time. A single Worker deployment runs as many concurrent isolate instances as needed to handle traffic — and there's no guarantee that two consecutive requests from the same client reach the same isolate. An in-process Map only lives in one isolate's memory. For persistent session state, use Cloudflare Durable Objects, which provide a single-threaded stateful execution environment with a guaranteed unique instance per session ID. Alternatively, design your MCP tools to be stateless so each request carries all the context it needs.
How does Hono compare to Express for MCP server performance?
On Cloudflare Workers and Deno Deploy, Hono is the only practical choice because those runtimes don't support Node.js's HTTP server. On Node.js, Hono with @hono/node-server performs comparably to Fastify and outperforms Express in throughput benchmarks. More important than raw throughput for MCP servers is cold start time: Hono's minimal dependency footprint (no external dependencies in the core package) gives sub-5ms cold starts on Workers, versus several hundred milliseconds for a full Express app with all its middleware.
What's the correct way to handle SSE streaming on Cloudflare Workers with Hono?
Cloudflare Workers support streaming responses natively through the ReadableStream API. Return a new Response(readableStream, { headers: { 'Content-Type': 'text/event-stream' } }) from your Hono handler. The MCP SDK's StreamableHTTPServerTransport handles writing to the stream when given access to the underlying response object. For Workers in particular, ensure you don't set Cache-Control: no-store globally — Workers caches responses by default, and a cached SSE stream returns stale data rather than a live connection, which breaks the MCP notification channel entirely.
How does AliveMCP monitor a Cloudflare Workers MCP server across regions?
AliveMCP runs probes from data centers in each major region and tracks uptime, latency, and response correctness independently per region. When you add a Cloudflare Workers MCP endpoint to AliveMCP, you can opt into regional alerts so you receive separate notifications for North America, Europe, and Asia-Pacific availability. This is critical for Workers deployments because a misconfiguration in Cloudflare's routing table or a KV namespace outage can affect a single region while the rest of the world sees normal responses. Configure multi-region monitoring at alivemcp.com — it takes about 30 seconds to add regional probes to an existing monitor.
Further reading
- MCP server Cloudflare Workers — Durable Objects and stateful edge sessions
- MCP server Deno — TypeScript-native MCP deployment
- MCP server Bun — fast MCP development with Bun runtime
- MCP server health checks — designing a robust /health endpoint
- MCP server uptime monitoring — tools and strategies
- MCP server Express — HTTP transport with Express.js