Framework integrations
Serve Cereal agents with Hono, Fastify, Express, Next.js, or a Fetch-native runtime.
The SDK uses Web Request, Response, fetch, and AbortSignal. Each defined
agent and agent router exposes a bound fetch(request) function. The Node bridge
lives in @trycereal/sdk/node, so importing the core SDK does not import Node HTTP or
stream modules.
Define once
// agents.ts
import { configureCereal, createAgentRouter, defineAgent } from "@trycereal/sdk"
configureCereal({ publicUrl: "https://app.example.com" })
export const research = defineAgent({
agent: {
slug: "research",
model: { provider: "gateway", model: "anthropic/claude-sonnet-4.6" },
system: "Research questions and cite the evidence."
}
})
export const agents = createAgentRouter({ agents: [research] })
// Run in a deployment/startup step. CEREAL_API_KEY identifies the environment.
await agents.init()The router mounts /api/cereal/research/mcp and /api/cereal/research/hooks. Add more
agents to the array; referenced sub-agents are included automatically. Set
basePath on the router to change the prefix. Mount the same router in your
server that you use to sync. See multiple agents.
Each serving process learns its verification tenant from the API key before
handling a signed request. Initialization is lazy; the init() call above is
optional. See verification.
Hono
import { Hono } from "hono"
import { agents } from "./agents.js"
const app = new Hono()
app.all("/api/cereal/*", (c) => agents.fetch(c.req.raw))
export default appHono exposes the original Web request as
c.req.raw. Mount this before middleware
that consumes or replaces the body.
Express 5
import express from "express"
import { createNodeHandler } from "@trycereal/sdk/node"
import { agents } from "./agents.js"
const app = express()
const handleCereal = createNodeHandler(agents, {
origin: "https://app.example.com",
maxBodyBytes: 1024 * 1024
})
// Keep the full URL; Express strips a prefix passed to app.use(prefix, ...).
app.use((req, res, next) => {
if (!req.path.startsWith("/api/cereal/")) return next()
void handleCereal(req, res).catch(next)
})
app.use(express.json())
app.listen(3000)Cereal signs the exact request body. Mount the handler before
express.json(), which
consumes and parses those bytes. The bridge rejects a previously consumed body.
It streams responses with backpressure, preserves separate Set-Cookie headers,
and passes disconnect cancellation through to tool and hook handlers.
Fastify 5
Fastify normally parses JSON before a route runs. Use an encapsulated plugin to retain raw bytes only for Cereal routes:
import Fastify from "fastify"
import { toWebRequest } from "@trycereal/sdk/node"
import { agents } from "./agents.js"
const app = Fastify()
await app.register(async (scope) => {
scope.removeAllContentTypeParsers()
scope.addContentTypeParser(
"application/json",
{ parseAs: "buffer", bodyLimit: 1024 * 1024 },
(_request, body, done) => done(null, body)
)
scope.all("/api/cereal/*", async (request, reply) => {
const webRequest = await toWebRequest(request.raw, {
origin: "https://app.example.com",
...(Buffer.isBuffer(request.body) ? { body: request.body } : {})
})
return reply.send(await agents.fetch(webRequest))
})
})
await app.listen({ port: 3000 })Fastify's reply.send(Response)
forwards the Web response. toWebRequest accepts the original raw Buffer from
the parser. Do not replace it with JSON.stringify(request.body): that changes
the signed bytes. An optional signal lets your request lifecycle cancel tools.
Next.js App Router
// app/api/cereal/[...cereal]/route.ts
import { agents } from "@/agents"
export const { GET, POST, DELETE } = agents.handlersFor a single agent without a router, export research.handlers and use its
configured mcpPath and hooksPath. The defaults are /api/cereal/mcp and
/api/cereal/hooks.
Bun, Deno, and Workers
Fetch-native servers can use the function directly:
Bun.serve({ fetch: agents.fetch })
// Or: Deno.serve(agents.fetch)
// Or a Cloudflare Worker module: export default { fetch: agents.fetch }Keep API credentials in the server runtime. Browser streaming uses the separate
@trycereal/sdk/browser entry point and scoped stream tokens.
Node HTTP
import { createServer } from "node:http"
import { createNodeHandler } from "@trycereal/sdk/node"
import { agents } from "./agents.js"
const handler = createNodeHandler(agents, { origin: "https://app.example.com" })
createServer((req, res) => {
void handler(req, res).catch((error) => {
console.error(error)
if (res.headersSent) res.destroy()
else res.writeHead(500).end()
})
}).listen(3000)The Node bridge requires Node 22 or later. origin sets the canonical application
origin; forwarded proxy headers are never trusted implicitly. It defaults to the
incoming host and socket protocol when omitted. Request bodies default to a 1 MiB
limit; createNodeHandler answers 413 when the limit is exceeded.