Cereal docs
Tools and integrations

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 app

Hono 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.handlers

For 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.

On this page