MCP agents
MCP (Model Context Protocol) servers expose tools, resources, and prompts to LLM clients. ayjnt auto-detects McpAgent subclasses and routes them through the SDK's MCP handler.
What MCP is
MCP is a protocol standardized by Anthropic for giving LLMs
structured access to tools, resources, and prompts. An MCP server
registers handlers (e.g. tool("add", schema, handler)) and speaks one
of two transports: streamable HTTP (modern) or SSE (legacy).
Clients like Claude Desktop, the @modelcontextprotocol/sdk TypeScript client, or any
other MCP-aware agent can connect and invoke tools.
Cloudflare's Agents SDK ships an McpAgent base
class that handles the transport and session management. You provide
the tool registry. ayjnt detects the base class at scan time and
wires the dispatch correctly — no manual McpAgent.serve()
call needed.
A minimal MCP agent
From examples/mcp:
import { McpAgent } from "agents/mcp";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
type State = { invocations: number };
export default class Tools extends McpAgent<{}, State> {
override initialState: State = { invocations: 0 };
server = new McpServer({
name: "ayjnt-example-tools",
version: "0.1.0",
});
async init() {
this.server.tool(
"echo",
"Echo back whatever you send.",
{ text: z.string() },
async ({ text }) => {
this.setState({ invocations: this.state.invocations + 1 });
return { content: [{ type: "text", text }] };
},
);
this.server.tool(
"add",
"Add two numbers.",
{ a: z.number(), b: z.number() },
async ({ a, b }) => {
this.setState({ invocations: this.state.invocations + 1 });
return { content: [{ type: "text", text: String(a + b) }] };
},
);
}
}Four things to notice:
-
extends McpAgent— this is what makes ayjnt route this agent throughMcpAgent.serve()instead of normal agent dispatch. -
server = new McpServer(...)— an instance field. The base class looks for this property to route incoming MCP messages. -
async init()— called once on agent startup. Register your tools/resources/prompts here. -
Tool handlers can update
this.statelike any other agent, so MCP servers can have memory across tool calls.
How ayjnt detects MCP agents
The scanner looks at the base class name in the source:
// ✓ detected
import { McpAgent } from "agents/mcp";
export default class Tools extends McpAgent<{}, State> { ... }
// ✗ NOT detected — base class is "M", not "McpAgent"
import { McpAgent as M } from "agents/mcp";
export default class Tools extends M<{}, State> { ... }
// ✗ NOT detected — base class is "BaseTools"
abstract class BaseTools extends McpAgent { ... }
export default class Tools extends BaseTools { ... }
The detection is source-level regex — we specifically match extends McpAgent. Keep your import plain and you're
fine. If ayjnt doesn't recognize the agent as MCP, it'll
route normally and the tool protocol won't work.
What the dispatch looks like
In the generated .ayjnt/dist/entry.ts, MCP agents get
special dispatch:
if (match.isMcp) {
const ClassRef = CLASSES[match.binding];
const handler = ClassRef.serve(
ROUTE_PREFIX_BY_BINDING[match.binding], // "/tools"
{ binding: match.binding },
);
return handler.fetch(request, env, executionCtx);
} McpAgent.serve(path, opts) returns a { fetch } handler that manages MCP session state,
transport negotiation (streamable HTTP vs SSE), and message
dispatch to the DO. ayjnt forwards the request to that handler
verbatim. Middleware still runs before this dispatch — if you want
to rate-limit tool calls or require auth, a middleware.ts in the MCP agent's folder works
like for any other agent.
Connecting from Claude Desktop
Once deployed, tell Claude Desktop where your MCP server lives:
{
"mcpServers": {
"ayjnt-tools": {
"url": "https://your-worker.workers.dev/tools"
}
}
}
Claude Desktop speaks streamable HTTP by default. The same URL
also serves SSE for clients that only support the legacy
transport — McpAgent.serve() handles both.
Connecting from another MCP client
The @modelcontextprotocol/sdk TypeScript client can
connect too:
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
const transport = new StreamableHTTPClientTransport(
new URL("http://localhost:8787/tools"),
);
const client = new Client({ name: "demo-client", version: "0.1.0" });
await client.connect(transport);
const tools = await client.listTools();
console.log(tools.tools.map((t) => t.name));
// → ["echo", "add"]
const result = await client.callTool({
name: "add",
arguments: { a: 7, b: 42 },
});
console.log(result);
// → { content: [{ type: "text", text: "49" }] }URL shape is different for MCP
Unlike normal agents (which use /<route>/<instanceId>), MCP routes use
just /<route> — no instance id in the URL.
Session management is inside the MCP protocol; each client gets a
fresh session via the Mcp-Session-Id header
(streamable HTTP) or the sessionId query parameter
(SSE). McpAgent.serve() handles creating a per-session
DO instance transparently.
Consequences to know:
- You can't pick an instance from the URL the way you can with a normal agent.
- Each MCP client session gets isolated state. If you need shared state across sessions (a global rate limit, for instance), keep it in KV or another DO and fetch it from tool handlers.
- Sessions end when the client disconnects. Don't rely on the same DO sticking around indefinitely.
Advanced: resources and prompts
MCP has three primitives: tools (function calls), resources (file-
like content the LLM can read), and prompts (pre-shaped message
templates). The McpServer API has .tool(), .resource(), and .prompt() methods — all three work in async init() the same way:
this.server.resource(
"notes://today",
"Today's agenda",
async () => ({
contents: [{ uri: "notes://today", text: this.state.notes }],
}),
);
this.server.prompt(
"summarize-in-tone",
{ source: z.string(), tone: z.enum(["formal", "casual"]) },
async ({ source, tone }) => ({
messages: [
{ role: "user", content: { type: "text", text: `Summarize in ${tone}: ${source}` } },
],
}),
);For the full schema, see the Cloudflare MCP docs and the MCP spec itself .
Packages you'll need
bun add agents @modelcontextprotocol/sdk zod agents includes McpAgent and the server-
side transport handling. @modelcontextprotocol/sdk
provides McpServer (what you register tools against)
and the client helpers. zod is the schema library McpServer.tool() expects.
The MCP SDK install pulls a fair bit of transitive weight. If bun install seems to hang after the lockfile is
saved, try bun install --ignore-scripts — one of
the transitive postinstalls can loop. See the examples/mcp/README.md for the exact workaround.