One handler, every runtime

A framework usually marries you to a host. AgentBack's REST and MCP surfaces run on any runtime with a fetch(Request) entry point — Node, Fastify, Hono, Bun, Deno, or Cloudflare Workers — from a single handler.

Most service frameworks pick a host for you. The router, the request object, the middleware contract, and the listener are one package. The day you want to run the same service on the edge, or behind a colleague's Fastify app, or on Bun for the cold-start win, you discover how much of the framework was actually about HTTP plumbing rather than your domain.

AgentBack keeps the domain and the plumbing apart. The dispatch pipeline — routing, Zod validation, dependency injection, auth, hooks, streaming, uploads, error envelopes — is exposed as one runtime-neutral function:

const app = new RestApplication({rest: {listen: false}}); // no TCP listener
app.restController(GreetController);
await app.start();                                         // mounts routes

const server = await app.getServer('RestServer');
export const handler = server.fetchHandler();              // { fetch }

fetchHandler() is a fetch(Request): Response. Whatever owns the TCP port bridges its native request into it. The Zod schemas, the OpenAPI document, and the MCP projection are identical wherever it runs.

The same app, six ways

Each host is a thin wrapper around the one handler — a few lines of glue, no codegen, no second router.

// Bun — its server *is* a fetch host
Bun.serve({port: 3000, fetch: handler.fetch});

// Deno
Deno.serve({port: 3000}, handler.fetch);

// Cloudflare Workers
export default {fetch: (req) => handler.fetch(req)};

// Hono — AgentBack as the catch-all behind Hono's own routes
hono.all('*', (c) => handler.fetch(c.req.raw));

// Fastify — AgentBack as a non-greedy fallback
installFastifyHost(fastify, handler);

// Node, no framework at all
new RestApplication({rest: {listener: 'native'}});

That last line is worth a pause. listener: 'native' makes AgentBack serve fetchHandler() through a plain Node http server directly — the runtime-neutral router becomes the single source of truth, the same surface Bun and Fastify drive. Express becomes one option among several, not the foundation.

MCP travels with it

The interesting part isn't that REST is portable — plenty of routers emit a fetch handler. It's that the agent surface comes along. The Model Context Protocol's Streamable HTTP transport has a Web-standard form, so AgentBack mounts MCP onto the same handler:

await installMcpHttp(app); // POST/GET/DELETE /mcp on the fetch path

On a native or edge host this uses the fetch-native transport; on Express it uses the Node one. The mount is auto-selected, with OAuth bearer and strategy auth, per-principal session pinning, and tool-scope filtering intact either way. One listener serves REST, MCP, the OpenAPI document, and the dev console together — on Bun, on Workers, wherever you put it.

Why this matters for one-schema services

AgentBack's claim is one Zod schema projected to REST, OpenAPI, and MCP through a DI container. Host portability is what keeps that claim true off the happy path. The boundary coherence doesn't live in a particular runtime; it lives in the schemas and the container, and those move with the handler. You choose the runtime for operational reasons — cold start, an existing Fastify app, an edge deployment — without renegotiating the contract your applications and agents depend on.

The runnable comparison is the hello-hosts example: one @api controller, served by Fastify, Hono, Bun, and the native listener, with the controller and app code byte-identical across all four.

Open the hosts diagram Read the HTTP hosts guide