Write tools in TypeScript
defineAgent puts the versioned definition and the code its tools run in one object.
Use defineAgent to keep an agent definition beside its tool
handlers. Cereal stores the definition and runs the session. Your app serves the
handlers, including from serverless functions. The example below assumes your app
provides sources and recordTranscript.
// lib/cereal.ts
import { defineAgent, defineProvider, tool } from "@trycereal/sdk"
import { z } from "zod"
export const primary = defineProvider({
slug: "primary",
type: "anthropic",
secret: process.env.ANTHROPIC_API_KEY
})
export const research = defineAgent({
agent: {
slug: "research-agent",
name: "Research agent",
model: {
provider: primary,
model: "claude-sonnet-4.6",
reasoning: "low"
},
system: "Research the question, read source documents, and cite the evidence.",
limits: { maxTurns: 12 },
overrides: { model: false, reasoning: true, system: "append" }
},
tools: [
tool({
name: "read_source",
// Descriptions are what the model chooses on, and, with deferred loading,
// what discovery searches. Say what it does, not what it is.
description: "Read a source document by its ID",
input: z.object({ sourceId: z.string() }),
annotations: { readOnlyHint: true },
handler: async ({ sourceId }, ctx) => {
const source = await sources.get(sourceId)
// ctx: { session?, connection, claims, signal }, all verified.
if (!ctx.session?.identity || ctx.session.identity.scope !== "user" ||
source?.workspaceId !== ctx.session.metadata?.workspaceId) {
return { found: false, error: "source not found" }
}
return { found: true, source }
}
})
],
hooks: {
onSessionEvent: async (event) => {
if (event.type === "agent.message") await recordTranscript(event)
}
}
})Tool inputs validate through Standard Schema. Zod 4.2+ and ArkType work directly;
Valibot and other validators can supply inputJsonSchema. See
tool schema interoperability.
For independent agents and reusable sub-agent references, see multiple agents.
Serving it
One catch-all route serves both the MCP endpoint and the hooks endpoint.
// app/api/cereal/[...cereal]/route.ts
import { research } from "@/lib/cereal"
export const { GET, POST, DELETE } = research.handlersThe handlers take a Web Request and return a Web Response, so the same export
works in Next, Hono, Remix, SvelteKit, Deno, Bun and Cloudflare Workers. The SDK does
not depend on Next. Defaults are /api/cereal/mcp and /api/cereal/hooks; override with
mcpPath and hooksPath.
For Express, Fastify, Hono, Node HTTP, and Fetch-native runtimes, see the
framework integrations. research.fetch(request) is the
framework-neutral entry point and returns 404 for unknown paths and 405 for
unresearched methods.
Verification happens before anything reaches a tool. See MCP and request signing.
hooks takes three callbacks, and sync registers whichever are defined:
onSessionEvent for event hooks, onToolCall for the
decision hook, and onSandboxPrepare for the
sandbox prepare hook.
Application configuration and initialization
Set CEREAL_API_KEY to an environment key with read,data,control. For HTTP callbacks,
set CEREAL_PUBLIC_URL to your application's HTTPS origin. These settings belong to
the application runtime; the agent definition receives neither a client nor deployment context.
The first action initializes the referenced provider, registers the connection and upserts the agent. It learns the verification tenant from the API key, including in fresh serverless instances. Concurrent first actions share initialization.
await research.init() // optional early initialization at application startupUnchanged agent definitions reuse their version. Provider secrets stay out of the
serialized agent definition. research.definition() returns that document without
a network request. Initialization registers configuration; it does not deploy your tool code.
See authoring helpers for the lifecycle and deployment for runtime credentials.
Driving it
const { id } = await research.sessions.start({
message: "What are the Orion launch risks?",
metadata: { workspaceId: "ws_ada" },
identity: { scope: "user", id: "ada" }
})
for await (const event of research.sessions.stream(id)) {
if (event.type === "agent.message") console.log(event.text)
}Before you have a public URL
With NODE_ENV=development, these same handlers automatically connect through an outbound
channel on the first agent action. See
local development.