McpAgent — API Reference
When you build MCP Servers on Cloudflare, you extend the McpAgent class ↗ from the Agents SDK:
import { McpAgent } from "agents/mcp";import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";import { z } from "zod";
export class MyMCP extends McpAgent { server = new McpServer({ name: "Demo", version: "1.0.0" });
async init() { this.server.tool( "add", { a: z.number(), b: z.number() }, async ({ a, b }) => ({ content: [{ type: "text", text: String(a + b) }], }), ); }}import { McpAgent } from "agents/mcp";import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";import { z } from "zod";
export class MyMCP extends McpAgent { server = new McpServer({ name: "Demo", version: "1.0.0" });
async init() { this.server.tool( "add", { a: z.number(), b: z.number() }, async ({ a, b }) => ({ content: [{ type: "text", text: String(a + b) }], }), ); }}This means that each instance of your MCP server has its own durable state, backed by a Durable Object, with its own SQL database.
Your MCP server doesn't necessarily have to be an Agent. You can build MCP servers that are stateless, and just add tools to your MCP server using the @modelcontextprotocol/sdk package.
But if you want your MCP server to:
- remember previous tool calls, and responses it provided
- provide a game to the MCP client, remembering the state of the game board, previous moves, and the score
- cache the state of a previous external API call, so that subsequent tool calls can reuse it
- do anything that an Agent can do, but allow MCP clients to communicate with it
You can use the APIs below in order to do so.
| Property/Method | Description |
|---|---|
state | Current state object (persisted) |
initialState | Default state when instance starts |
setState(state) | Update and persist state |
onStateUpdate(state) | Called when state changes |
sql | Execute SQL queries on embedded database |
server | The McpServer instance for registering tools |
props | User identity and tokens from OAuth authentication |
elicitInput(options, context) | Request structured input from user |
McpAgent.serve(path, options) | Static method to create a Worker handler |
The McpAgent.serve() static method creates a Worker handler that routes requests to your MCP server:
import { McpAgent } from "agents/mcp";import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";import { z } from "zod";
export class MyMCP extends McpAgent { server = new McpServer({ name: "my-server", version: "1.0.0" });
async init() { this.server.tool("square", { n: z.number() }, async ({ n }) => ({ content: [{ type: "text", text: String(n * n) }], })); }}
// Export the Worker handlerexport default MyMCP.serve("/mcp");import { McpAgent } from "agents/mcp";import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";import { z } from "zod";
export class MyMCP extends McpAgent { server = new McpServer({ name: "my-server", version: "1.0.0" });
async init() { this.server.tool("square", { n: z.number() }, async ({ n }) => ({ content: [{ type: "text", text: String(n * n) }], })); }}
// Export the Worker handlerexport default MyMCP.serve("/mcp");This is the simplest way to deploy an MCP server — about 15 lines of code. The serve() method handles Streamable HTTP transport automatically.
When using the OAuth Provider Library ↗, pass your MCP server to apiHandlers:
import { OAuthProvider } from "@cloudflare/workers-oauth-provider";
export default new OAuthProvider({ apiHandlers: { "/mcp": MyMCP.serve("/mcp") }, authorizeEndpoint: "/authorize", tokenEndpoint: "/token", clientRegistrationEndpoint: "/register", defaultHandler: AuthHandler,});import { OAuthProvider } from "@cloudflare/workers-oauth-provider";
export default new OAuthProvider({ apiHandlers: { "/mcp": MyMCP.serve("/mcp") }, authorizeEndpoint: "/authorize", tokenEndpoint: "/token", clientRegistrationEndpoint: "/register", defaultHandler: AuthHandler,});For GDPR and data residency compliance, specify a jurisdiction to ensure your MCP server instances run in specific regions:
// EU jurisdiction for GDPR complianceexport default MyMCP.serve("/mcp", { jurisdiction: "eu" });// EU jurisdiction for GDPR complianceexport default MyMCP.serve("/mcp", { jurisdiction: "eu" });With OAuth:
export default new OAuthProvider({ apiHandlers: { "/mcp": MyMCP.serve("/mcp", { jurisdiction: "eu" }), }, // ... other OAuth config});export default new OAuthProvider({ apiHandlers: { "/mcp": MyMCP.serve("/mcp", { jurisdiction: "eu" }), }, // ... other OAuth config});When you specify jurisdiction: "eu":
- All MCP session data stays within the EU
- User data processed by your tools remains in the EU
- State stored in the Durable Object stays in the EU
Available jurisdictions include "eu" (European Union) and "fedramp" (FedRAMP compliant locations). Refer to Durable Objects data location for more options.
McpAgent instances automatically support WebSockets Hibernation, allowing stateful MCP servers to sleep during inactive periods while preserving their state. This means your agents only consume compute resources when actively processing requests, optimizing costs while maintaining the full context and conversation history.
Hibernation is enabled by default and requires no additional configuration.
The McpAgent class provides seamless integration with the OAuth Provider Library ↗ for authentication and authorization.
When a user authenticates to your MCP server, their identity information and tokens are made available through the props parameter, allowing you to:
- access user-specific data
- check user permissions before performing operations
- customize responses based on user attributes
- use authentication tokens to make requests to external services on behalf of the user
The McpAgent class provides full access to the Agent state APIs:
state— Current persisted stateinitialState— Default state when instance startssetState— Update and persist stateonStateUpdate— React to state changessql— Execute SQL queries on embedded database
For example, the following code implements an MCP server that remembers a counter value, and updates the counter when the add tool is called:
import { McpAgent } from "agents/mcp";import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";import { z } from "zod";
export class MyMCP extends McpAgent { server = new McpServer({ name: "Demo", version: "1.0.0", });
initialState = { counter: 1, };
async init() { this.server.resource(`counter`, `mcp://resource/counter`, (uri) => { return { contents: [{ uri: uri.href, text: String(this.state.counter) }], }; });
this.server.tool( "add", "Add to the counter, stored in the MCP", { a: z.number() }, async ({ a }) => { this.setState({ ...this.state, counter: this.state.counter + a });
return { content: [ { type: "text", text: String(`Added ${a}, total is now ${this.state.counter}`), }, ], }; }, ); }
onStateUpdate(state) { console.log({ stateUpdate: state }); }}import { McpAgent } from "agents/mcp";import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";import { z } from "zod";
type State = { counter: number };
export class MyMCP extends McpAgent<Env, State, {}> { server = new McpServer({ name: "Demo", version: "1.0.0", });
initialState: State = { counter: 1, };
async init() { this.server.resource(`counter`, `mcp://resource/counter`, (uri) => { return { contents: [{ uri: uri.href, text: String(this.state.counter) }], }; });
this.server.tool( "add", "Add to the counter, stored in the MCP", { a: z.number() }, async ({ a }) => { this.setState({ ...this.state, counter: this.state.counter + a });
return { content: [ { type: "text", text: String(`Added ${a}, total is now ${this.state.counter}`), }, ], }; }, ); }
onStateUpdate(state: State) { console.log({ stateUpdate: state }); }}MCP servers can request additional user input during tool execution using elicitation. The MCP client (like Claude Desktop) renders a form based on your JSON Schema and returns the user's response.
- Request structured input that was not part of the original tool call
- Confirm high-stakes operations before proceeding
- Gather additional context or preferences mid-execution
Request structured input from the user during tool execution.
Parameters:
| Parameter | Type | Description |
|---|---|---|
options.message | string | Message explaining what input is needed |
options.requestedSchema | JSON Schema | Schema defining the expected input structure |
context.relatedRequestId | string | The extra.requestId from the tool handler |
Returns: Promise<{ action: "accept" | "decline", content?: object }>
import { McpAgent } from "agents/mcp";import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";import { z } from "zod";
export class CounterMCP extends McpAgent { server = new McpServer({ name: "counter-server", version: "1.0.0", });
initialState = { counter: 0 };
async init() { this.server.tool( "increase-counter", "Increase the counter by a user-specified amount", { confirm: z.boolean().describe("Do you want to increase the counter?") }, async ({ confirm }, extra) => { if (!confirm) { return { content: [{ type: "text", text: "Cancelled." }] }; }
// Request additional input from the user const userInput = await this.server.server.elicitInput( { message: "By how much do you want to increase the counter?", requestedSchema: { type: "object", properties: { amount: { type: "number", title: "Amount", description: "The amount to increase the counter by", }, }, required: ["amount"], }, }, { relatedRequestId: extra.requestId }, );
// Check if user accepted or cancelled if (userInput.action !== "accept" || !userInput.content) { return { content: [{ type: "text", text: "Cancelled." }] }; }
// Use the input const amount = Number(userInput.content.amount); this.setState({ ...this.state, counter: this.state.counter + amount, });
return { content: [ { type: "text", text: `Counter increased by ${amount}, now at ${this.state.counter}`, }, ], }; }, ); }}import { McpAgent } from "agents/mcp";import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";import { z } from "zod";
type State = { counter: number };
export class CounterMCP extends McpAgent<Env, State, {}> { server = new McpServer({ name: "counter-server", version: "1.0.0", });
initialState: State = { counter: 0 };
async init() { this.server.tool( "increase-counter", "Increase the counter by a user-specified amount", { confirm: z.boolean().describe("Do you want to increase the counter?") }, async ({ confirm }, extra) => { if (!confirm) { return { content: [{ type: "text", text: "Cancelled." }] }; }
// Request additional input from the user const userInput = await this.server.server.elicitInput( { message: "By how much do you want to increase the counter?", requestedSchema: { type: "object", properties: { amount: { type: "number", title: "Amount", description: "The amount to increase the counter by", }, }, required: ["amount"], }, }, { relatedRequestId: extra.requestId }, );
// Check if user accepted or cancelled if (userInput.action !== "accept" || !userInput.content) { return { content: [{ type: "text", text: "Cancelled." }] }; }
// Use the input const amount = Number(userInput.content.amount); this.setState({ ...this.state, counter: this.state.counter + amount, });
return { content: [ { type: "text", text: `Counter increased by ${amount}, now at ${this.state.counter}`, }, ], }; }, ); }}The requestedSchema defines the form structure shown to the user:
const schema = { type: "object", properties: { // Text input name: { type: "string", title: "Name", description: "Enter your name", }, // Number input amount: { type: "number", title: "Amount", minimum: 1, maximum: 100, }, // Boolean (checkbox) confirm: { type: "boolean", title: "I confirm this action", }, // Enum (dropdown) priority: { type: "string", enum: ["low", "medium", "high"], title: "Priority", }, }, required: ["name", "amount"],};const result = await this.server.server.elicitInput( { message: "Confirm action", requestedSchema: schema }, { relatedRequestId: extra.requestId },);
switch (result.action) { case "accept": // User submitted the form const { name, amount } = result.content; // Process the input... break;
case "decline": // User cancelled return { content: [{ type: "text", text: "Operation cancelled." }] };}const result = await this.server.server.elicitInput( { message: "Confirm action", requestedSchema: schema }, { relatedRequestId: extra.requestId },);
switch (result.action) { case "accept": // User submitted the form const { name, amount } = result.content as { name: string; amount: number }; // Process the input... break;
case "decline": // User cancelled return { content: [{ type: "text", text: "Operation cancelled." }] };}For more human-in-the-loop patterns including workflow-based approval, refer to Human-in-the-loop patterns.
Was this helpful?
- Resources
- API
- New to Cloudflare?
- Directory
- Sponsorships
- Open Source
- Support
- Help Center
- System Status
- Compliance
- GDPR
- Company
- cloudflare.com
- Our team
- Careers
- © 2026 Cloudflare, Inc.
- Privacy Policy
- Terms of Use
- Report Security Issues
- Trademark
-