@rubrion/mastra-cloudflare
Mastra agents on Cloudflare Workers, without the boilerplate. The template's agents Worker is built on it, and you can use it in any Worker.
npm install @rubrion/mastra-cloudflare
Your wrangler.jsonc needs "compatibility_flags": ["nodejs_compat"]. The package re-exports Agent, Mastra, createTool, z and RequestContext, so it is the only dependency you need.
A Worker in 20 lines
import { env } from 'cloudflare:workers';
import { createAgentServer, createTool, d1Memory, defineAgents, workersAI, z } from '@rubrion/mastra-cloudflare';
const time = createTool({
id: 'current_time',
description: 'The current date and time.',
inputSchema: z.object({}),
execute: async () => ({ now: new Date().toISOString() }),
});
const agents = defineAgents(
[
{ id: 'writer', name: 'Writer', description: 'Writes and edits text.', instructions: 'Write what is asked, briefly.' },
{ id: 'helper', name: 'Helper', description: 'General questions; knows the time.', instructions: 'Be useful.', tools: { current_time: time } },
],
{ memory: d1Memory(() => env.MEMORY) },
);
export default createAgentServer({
agents,
apiKey: (e: Env) => e.API_KEY,
defaultModel: (e: Env) => workersAI(e.AI, '@cf/google/gemma-4-26b-a4b-it', { thinking: false }),
});
createAgentServer
createAgentServer({ agents, name?, apiKey?, provider?, defaultModel?, planner?, maxSteps? }) returns a Worker handler, { fetch(request, env) }, with no HTTP framework (Mastra's own server route table costs most of a Worker's startup CPU).
| Option | |
|---|---|
agents | the registry from defineAgents |
name | shown as owned_by in /v1/models and in the health check |
apiKey(env) | the bearer key callers must send; none leaves the server open |
provider(env) | the OpenAI-compatible provider when the request carries none |
defaultModel(env) | the model when there is no provider at all, e.g. Workers AI |
planner | false turns auto routing off; { model, rules } customizes it |
maxSteps | tool-call rounds per reply (default 5) |
Routes: GET / (health), GET /v1/models, POST /v1/chat/completions (streamed with stream: true; text and image_url content). Per-request headers, for a middleware in front, are in the API reference.
defineAgents and buildAgent
interface AgentSpec {
id: string;
name: string;
description: string; // the router picks agents by it
instructions: string | ((args: { requestContext }) => string);
model?: (requestContext) => Model; // default: the request's provider, else defaultModel
tools?: Record<string, Tool>;
maxOutputTokens?: number;
enabled?: () => boolean; // off when its configuration is missing
}
defineAgents(specs, { memory }) returns { list(), find(id), get(id), mastra() }. Agents are built lazily, once per isolate, because bindings and secrets are only readable inside a request. Specs can carry fields of your own: defineAgents<MySpec>(…). buildAgent(spec, { memory }) builds one uncached Agent, e.g. per request.
Models and memory
openAICompatible({ baseURL, apiKey, model }); // OpenAI, OpenRouter, LiteLLM, vLLM, Ollama, another agent server
workersAI(env.AI, model, { thinking }); // Workers AI; `thinking` for reasoning models (Gemma 4, Qwen 3)
providerModel(requestContext); // the request's provider, else the server's defaultModel
d1Memory(() => env.MEMORY, { lastMessages }); // Mastra Memory on D1, no vector store
workersAI with thinking switches the model's thinking through its chat template and drops reasoning_effort: Mastra sends the AI SDK's unified reasoning option, and reasoning models such as Gemma 4 stop thinking whenever it is present.
Source: github.com/rubrion/mastra-cloudflare · License: GPL-3.0.