diff --git a/src/content/docs/agents/api-reference/agents-api.mdx b/src/content/docs/agents/api-reference/agents-api.mdx index 0b1573a66cf..0363c629163 100644 --- a/src/content/docs/agents/api-reference/agents-api.mdx +++ b/src/content/docs/agents/api-reference/agents-api.mdx @@ -53,16 +53,16 @@ flowchart TD E --> F["onClose"] ``` -| Method | When it runs | -| --------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Method | When it runs | +| --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `onStart(props?)` | When the instance starts, or wakes from hibernation. Receives optional [initialization props](/agents/api-reference/routing/#props) passed via `getAgentByName` or `routeAgentRequest`. | -| `onRequest(request)` | For each HTTP request to the instance | -| `onConnect(connection, ctx)` | When a WebSocket connection is established | -| `onMessage(connection, message)` | For each WebSocket message received | -| `onError(connection, error)` | When a WebSocket error occurs | -| `onClose(connection, code, reason, wasClean)` | When a WebSocket connection closes | -| `onEmail(email)` | When an email is routed to the instance | -| `onStateChanged(state, source)` | When state changes (from server or client) | +| `onRequest(request)` | For each HTTP request to the instance | +| `onConnect(connection, ctx)` | When a WebSocket connection is established | +| `onMessage(connection, message)` | For each WebSocket message received | +| `onError(connection, error)` | When a WebSocket error occurs | +| `onClose(connection, code, reason, wasClean)` | When a WebSocket connection closes | +| `onEmail(email)` | When an email is routed to the instance | +| `onStateChanged(state, source)` | When state changes (from server or client) | ## Core properties @@ -75,21 +75,21 @@ flowchart TD ## Server-side API reference -| Feature | Methods | Documentation | -| -------------------- | --------------------------------------------------------------------- | ------------------------------------------------------------------- | -| **State** | `setState()`, `onStateChanged()`, `initialState` | [Store and sync state](/agents/api-reference/store-and-sync-state/) | -| **Callable methods** | `@callable()` decorator | [Callable methods](/agents/api-reference/callable-methods/) | -| **Scheduling** | `schedule()`, `scheduleEvery()`, `getSchedules()`, `cancelSchedule()` | [Schedule tasks](/agents/api-reference/schedule-tasks/) | -| **Queue** | `queue()`, `dequeue()`, `dequeueAll()`, `getQueue()` | [Queue tasks](/agents/api-reference/queue-tasks/) | -| **WebSockets** | `onConnect()`, `onMessage()`, `onClose()`, `broadcast()` | [WebSockets](/agents/api-reference/websockets/) | -| **HTTP/SSE** | `onRequest()` | [HTTP and SSE](/agents/api-reference/http-sse/) | -| **Email** | `onEmail()`, `replyToEmail()` | [Email routing](/agents/api-reference/email/) | -| **Workflows** | `runWorkflow()`, `waitForApproval()` | [Run Workflows](/agents/api-reference/run-workflows/) | -| **MCP Client** | `addMcpServer()`, `removeMcpServer()`, `getMcpServers()` | [MCP Client API](/agents/api-reference/mcp-client-api/ ) | -| **AI Models** | Workers AI, OpenAI, Anthropic bindings | [Using AI models](/agents/api-reference/using-ai-models/) | -| **Protocol messages** | `shouldSendProtocolMessages()`, `isConnectionProtocolEnabled()` | [Protocol messages](/agents/api-reference/protocol-messages/) | -| **Context** | `getCurrentAgent()` | [getCurrentAgent()](/agents/api-reference/get-current-agent/) | -| **Observability** | `observability.emit()` | [Observability](/agents/api-reference/observability/) | +| Feature | Methods | Documentation | +| --------------------- | --------------------------------------------------------------------- | ------------------------------------------------------------------- | +| **State** | `setState()`, `onStateChanged()`, `initialState` | [Store and sync state](/agents/api-reference/store-and-sync-state/) | +| **Callable methods** | `@callable()` decorator | [Callable methods](/agents/api-reference/callable-methods/) | +| **Scheduling** | `schedule()`, `scheduleEvery()`, `getSchedules()`, `cancelSchedule()` | [Schedule tasks](/agents/api-reference/schedule-tasks/) | +| **Queue** | `queue()`, `dequeue()`, `dequeueAll()`, `getQueue()` | [Queue tasks](/agents/api-reference/queue-tasks/) | +| **WebSockets** | `onConnect()`, `onMessage()`, `onClose()`, `broadcast()` | [WebSockets](/agents/api-reference/websockets/) | +| **HTTP/SSE** | `onRequest()` | [HTTP and SSE](/agents/api-reference/http-sse/) | +| **Email** | `onEmail()`, `replyToEmail()` | [Email routing](/agents/api-reference/email/) | +| **Workflows** | `runWorkflow()`, `waitForApproval()` | [Run Workflows](/agents/api-reference/run-workflows/) | +| **MCP Client** | `addMcpServer()`, `removeMcpServer()`, `getMcpServers()` | [MCP Client API](/agents/api-reference/mcp-client-api/) | +| **AI Models** | Workers AI, OpenAI, Anthropic bindings | [Using AI models](/agents/api-reference/using-ai-models/) | +| **Protocol messages** | `shouldSendProtocolMessages()`, `isConnectionProtocolEnabled()` | [Protocol messages](/agents/api-reference/protocol-messages/) | +| **Context** | `getCurrentAgent()` | [getCurrentAgent()](/agents/api-reference/get-current-agent/) | +| **Observability** | `observability.emit()` | [Observability](/agents/api-reference/observability/) | ## SQL API @@ -110,12 +110,12 @@ For state that needs to sync with clients, use the [State API](/agents/api-refer ## Client-side API reference -| Feature | Methods | Documentation | -| -------------------- | ---------------- | ----------------------------------------------- | -| **WebSocket client** | `AgentClient` | [Client SDK](/agents/api-reference/client-sdk/) | +| Feature | Methods | Documentation | +| -------------------- | ---------------- | ----------------------------------------------------------------------------- | +| **WebSocket client** | `AgentClient` | [Client SDK](/agents/api-reference/client-sdk/) | | **HTTP client** | `agentFetch()` | [Client SDK](/agents/api-reference/client-sdk/#http-requests-with-agentfetch) | -| **React hook** | `useAgent()` | [Client SDK](/agents/api-reference/client-sdk/#react) | -| **Chat hook** | `useAgentChat()` | [Client SDK](/agents/api-reference/client-sdk/) | +| **React hook** | `useAgent()` | [Client SDK](/agents/api-reference/client-sdk/#react) | +| **Chat hook** | `useAgentChat()` | [Client SDK](/agents/api-reference/client-sdk/) | ### Quick example diff --git a/src/content/docs/agents/api-reference/browse-the-web.mdx b/src/content/docs/agents/api-reference/browse-the-web.mdx index 6dddd4860e4..16dbdd88e74 100644 --- a/src/content/docs/agents/api-reference/browse-the-web.mdx +++ b/src/content/docs/agents/api-reference/browse-the-web.mdx @@ -20,16 +20,17 @@ Agents can browse the web using the [Browser Rendering](/browser-rendering/) API The [Browser Rendering](/browser-rendering/) allows you to spin up headless browser instances, render web pages, and interact with websites through your Agent. -You can define a method that uses Puppeteer to pull the content of a web page, parse the DOM, and extract relevant information by calling the OpenAI model: +You can define a method that uses Puppeteer to pull the content of a web page, parse the DOM, and extract relevant information by calling a model via [Workers AI](/workers-ai/): ```ts interface Env { BROWSER: Fetcher; + AI: Ai; } -export class MyAgent extends Agent { +export class MyAgent extends Agent { async browse(browserInstance: Fetcher, urls: string[]) { let responses = []; for (const url of urls) { @@ -42,21 +43,14 @@ export class MyAgent extends Agent { "body", (element) => element.innerHTML, ); - const client = new OpenAI({ - apiKey: this.env.OPENAI_API_KEY, - }); - let resp = await client.chat.completions.create({ - model: this.env.MODEL, + let resp = await this.env.AI.run("@cf/zai-org/glm-4.7-flash", { messages: [ { role: "user", content: `Return a JSON object with the product names, prices and URLs with the following format: { "name": "Product Name", "price": "Price", "url": "URL" } from the website content below. ${bodyContent}`, }, ], - response_format: { - type: "json_object", - }, }); responses.push(resp); @@ -79,6 +73,9 @@ You'll also need to add install the `@cloudflare/puppeteer` package and add the ```jsonc { // ... + "ai": { + "binding": "AI", + }, "browser": { "binding": "MYBROWSER", }, diff --git a/src/content/docs/agents/api-reference/chat-agents.mdx b/src/content/docs/agents/api-reference/chat-agents.mdx index 97b2d5c817e..a1ae99d9318 100644 --- a/src/content/docs/agents/api-reference/chat-agents.mdx +++ b/src/content/docs/agents/api-reference/chat-agents.mdx @@ -675,11 +675,11 @@ export class ChatAgent extends AIChatAgent { ### Three patterns -| Pattern | How | Persisted? | Use case | -| --- | --- | --- | --- | -| **Reconciliation** | Same `type` + `id` → updates in-place | Yes | Progressive state (searching → found) | -| **Append** | No `id`, or different `id` → appends | Yes | Log entries, multiple citations | -| **Transient** | `transient: true` → not added to `message.parts` | No | Ephemeral status (thinking indicator) | +| Pattern | How | Persisted? | Use case | +| ------------------ | ------------------------------------------------ | ---------- | ------------------------------------- | +| **Reconciliation** | Same `type` + `id` → updates in-place | Yes | Progressive state (searching → found) | +| **Append** | No `id`, or different `id` → appends | Yes | Log entries, multiple citations | +| **Transient** | `transient: true` → not added to `message.parts` | No | Ephemeral status (thinking indicator) | Transient parts are broadcast to connected clients in real time but excluded from SQLite persistence and `message.parts`. Use the `onData` callback to consume them. diff --git a/src/content/docs/agents/api-reference/email.mdx b/src/content/docs/agents/api-reference/email.mdx index 05be04b214d..d6e2f2e3fe6 100644 --- a/src/content/docs/agents/api-reference/email.mdx +++ b/src/content/docs/agents/api-reference/email.mdx @@ -316,9 +316,9 @@ When your agent sends emails and expects replies, use secure reply routing to pr ```jsonc { - "vars": { - "EMAIL_SECRET": "change-me-in-production", - }, + "vars": { + "EMAIL_SECRET": "change-me-in-production", + }, } ``` diff --git a/src/content/docs/agents/api-reference/http-sse.mdx b/src/content/docs/agents/api-reference/http-sse.mdx index a32cd2ded05..8d9c0e88ef8 100644 --- a/src/content/docs/agents/api-reference/http-sse.mdx +++ b/src/content/docs/agents/api-reference/http-sse.mdx @@ -115,18 +115,20 @@ The [AI SDK](https://sdk.vercel.ai/) provides built-in SSE streaming: ```ts import { Agent } from "agents"; import { streamText } from "ai"; -import { createOpenAI } from "@ai-sdk/openai"; +import { createWorkersAI } from "workers-ai-provider"; -export class ChatAgent extends Agent { +interface Env { + AI: Ai; +} + +export class ChatAgent extends Agent { async onRequest(request: Request): Promise { const { prompt } = await request.json<{ prompt: string }>(); - const openai = createOpenAI({ - apiKey: this.env.OPENAI_API_KEY, - }); + const workersai = createWorkersAI({ binding: this.env.AI }); const result = streamText({ - model: openai("gpt-4o"), + model: workersai("@cf/zai-org/glm-4.7-flash"), prompt: prompt, }); diff --git a/src/content/docs/agents/api-reference/mcp-agent-api.mdx b/src/content/docs/agents/api-reference/mcp-agent-api.mdx index 504b062b2dc..6242a1ad492 100644 --- a/src/content/docs/agents/api-reference/mcp-agent-api.mdx +++ b/src/content/docs/agents/api-reference/mcp-agent-api.mdx @@ -55,7 +55,7 @@ You can use the APIs below in order to do so. | `state` | Current state object (persisted) | | `initialState` | Default state when instance starts | | `setState(state)` | Update and persist state | -| `onStateChanged(state)` | Called when state changes | +| `onStateChanged(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 | diff --git a/src/content/docs/agents/api-reference/mcp-client-api.mdx b/src/content/docs/agents/api-reference/mcp-client-api.mdx index d3d58ab158a..9fad5836f5b 100644 --- a/src/content/docs/agents/api-reference/mcp-client-api.mdx +++ b/src/content/docs/agents/api-reference/mcp-client-api.mdx @@ -161,14 +161,8 @@ class MyAgent extends Agent { ); if (result.state === "authenticating") { - // Option 1: Redirect the user + // Redirect the user to the OAuth authorization page return Response.redirect(result.authUrl); - - // Option 2: Return the URL for client-side redirect - return Response.json({ - status: "needs_auth", - authUrl: result.authUrl, - }); } return Response.json({ status: "connected", id: result.id }); @@ -345,14 +339,15 @@ To use MCP tools with the Vercel AI SDK, use `this.mcp.getAITools()` which conve ```ts import { generateText } from "ai"; -import { openai } from "@ai-sdk/openai"; +import { createWorkersAI } from "workers-ai-provider"; -export class MyAgent extends Agent { +export class MyAgent extends Agent { async onRequest(request: Request) { + const workersai = createWorkersAI({ binding: this.env.AI }); const response = await generateText({ - model: openai("gpt-4"), + model: workersai("@cf/zai-org/glm-4.7-flash"), prompt: "What's the weather in San Francisco?", - tools: this.mcp.getAITools(), // Converts MCP tools to AI SDK format + tools: this.mcp.getAITools(), }); return new Response(response.text); @@ -633,26 +628,26 @@ The override is used for both new connections (`addMcpServer`) and restored conn import { Agent } from "agents"; import type { AgentMcpOAuthProvider } from "agents"; -export class MyAgent extends Agent { +export class MyAgent extends Agent { createMcpOAuthProvider(callbackUrl: string): AgentMcpOAuthProvider { + const env = this.env; return { get redirectUrl() { return callbackUrl; }, get clientMetadata() { return { - client_id: this.env.MCP_CLIENT_ID, - client_secret: this.env.MCP_CLIENT_SECRET, + client_id: env.MCP_CLIENT_ID, + client_secret: env.MCP_CLIENT_SECRET, redirect_uris: [callbackUrl], }; }, clientInformation() { return { - client_id: this.env.MCP_CLIENT_ID, - client_secret: this.env.MCP_CLIENT_SECRET, + client_id: env.MCP_CLIENT_ID, + client_secret: env.MCP_CLIENT_SECRET, }; }, - // ... implement other AgentMcpOAuthProvider methods }; } } @@ -831,7 +826,7 @@ Use error detection utilities to handle connection errors: ```ts -import { isUnauthorized, isTransportNotImplemented } from "agents/mcp"; +import { isUnauthorized, isTransportNotImplemented } from "agents"; export class MyAgent extends Agent { async onRequest(request: Request) { diff --git a/src/content/docs/agents/api-reference/mcp-handler-api.mdx b/src/content/docs/agents/api-reference/mcp-handler-api.mdx index 8b170384c32..c5e9b353c4a 100644 --- a/src/content/docs/agents/api-reference/mcp-handler-api.mdx +++ b/src/content/docs/agents/api-reference/mcp-handler-api.mdx @@ -7,9 +7,9 @@ sidebar: order: 12 --- -import { Render, TypeScriptExample, LinkCard } from "~/components"; +import { TypeScriptExample, LinkCard } from "~/components"; -The `createMcpHandler` function creates a fetch handler to serve your [MCP server](/agents/model-context-protocol/). You can use it as an alternative to the [`McpAgent`](/agents/api-reference/mcp-agent-api) class when you don't need the deprecated SSE transport. +The `createMcpHandler` function creates a fetch handler to serve your [MCP server](/agents/model-context-protocol/). Use it when you want a stateless MCP server that runs in a plain Worker (no Durable Object). For stateful MCP servers that persist state across requests, use the [`McpAgent`](/agents/api-reference/mcp-agent-api) class instead. It uses an implementation of the MCP Transport interface, `WorkerTransport`, built on top of web standards, which conforms to the [streamable-http](https://modelcontextprotocol.io/specification/draft/basic/transports/#streamable-http) transport specification. @@ -191,6 +191,8 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; const STATE_KEY = "mcp-transport-state"; +type State = { counter: number }; + export class MyStatefulMcpAgent extends Agent { server = new McpServer({ name: "Stateful MCP Server", @@ -201,18 +203,18 @@ export class MyStatefulMcpAgent extends Agent { sessionIdGenerator: () => this.name, storage: { get: () => { - return this.ctx.storage.kv.get(STATE_KEY); + return this.ctx.storage.get(STATE_KEY); }, set: (state: TransportState) => { - this.ctx.storage.kv.put(STATE_KEY, state); + this.ctx.storage.put(STATE_KEY, state); }, }, }); - async onMcpRequest(request: Request) { + async onRequest(request: Request) { return createMcpHandler(this.server, { transport: this.transport, - })(request, this.env, {} as ExecutionContext); + })(request, this.env, this.ctx as unknown as ExecutionContext); } } ``` @@ -236,7 +238,7 @@ export default { const agent = await getAgentByName(env.MyStatefulMcpAgent, sessionId); // Route the MCP request to the agent - return await agent.onMcpRequest(request); + return await agent.onRequest(request); }, } satisfies ExportedHandler; ``` @@ -257,11 +259,11 @@ The MCP SDK 1.26.0 introduces a breaking change for stateless MCP servers that a ### Who is affected? -| Server Type | Affected? | Action Required | -|-------------|-----------|-----------------| -| Stateful servers using `Agent`/Durable Object | No | No changes needed | -| Stateless servers using `createMcpHandler` | Yes | Create new `McpServer` per request | -| Stateless servers using raw SDK transport | Yes | Create new `McpServer` and transport per request | +| Server Type | Affected? | Action Required | +| --------------------------------------------- | --------- | ------------------------------------------------ | +| Stateful servers using `Agent`/Durable Object | No | No changes needed | +| Stateless servers using `createMcpHandler` | Yes | Create new `McpServer` per request | +| Stateless servers using raw SDK transport | Yes | Create new `McpServer` and transport per request | ### Why is this necessary? @@ -524,14 +526,13 @@ interface TransportState { ```ts +// Inside an Agent or Durable Object class method: const transport = new WorkerTransport({ storage: { get: async () => { - // Retrieve state from Durable Object storage return await this.ctx.storage.get("mcp-state"); }, set: async (state) => { - // Persist state to Durable Object storage await this.ctx.storage.put("mcp-state", state); }, }, @@ -552,7 +553,7 @@ interface McpAuthContext { ### getMcpAuthContext -Retrieve the current authentication context within an MCP tool handler. This returns user information that was populated by the OAuth provider. Note that if using `McpAgent`, this information is accessable directly on `this.props` instead. +Retrieve the current authentication context within an MCP tool handler. This returns user information that was populated by the OAuth provider. Note that if using `McpAgent`, this information is accessible directly on `this.props` instead. ```ts import { getMcpAuthContext } from "agents/mcp"; @@ -566,23 +567,26 @@ function getMcpAuthContext(): McpAuthContext | undefined; import { getMcpAuthContext } from "agents/mcp"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -const server = new McpServer({ name: "Auth Server", version: "1.0.0" }); +function createServer() { + const server = new McpServer({ name: "Auth Server", version: "1.0.0" }); -server.tool("getProfile", "Get the current user's profile", {}, async () => { - // Access user info automatically populated by OAuth provider - const auth = getMcpAuthContext(); - const username = auth?.props?.username as string | undefined; - const email = auth?.props?.email as string | undefined; + server.tool("getProfile", "Get the current user's profile", {}, async () => { + const auth = getMcpAuthContext(); + const username = auth?.props?.username as string | undefined; + const email = auth?.props?.email as string | undefined; - return { - content: [ - { - type: "text", - text: `User: ${username ?? "anonymous"}, Email: ${email ?? "none"}`, - }, - ], - }; -}); + return { + content: [ + { + type: "text", + text: `User: ${username ?? "anonymous"}, Email: ${email ?? "none"}`, + }, + ], + }; + }); + + return server; +} ``` diff --git a/src/content/docs/agents/api-reference/retries.mdx b/src/content/docs/agents/api-reference/retries.mdx index 8f45646a147..5fa57173fb2 100644 --- a/src/content/docs/agents/api-reference/retries.mdx +++ b/src/content/docs/agents/api-reference/retries.mdx @@ -130,11 +130,7 @@ const data = await this.retry( maxAttempts: 5, shouldRetry: (err, nextAttempt) => { // Do not retry 4xx client errors — our request is wrong - if ( - err instanceof HttpError && - err.status >= 400 && - err.status < 500 - ) { + if (err instanceof HttpError && err.status >= 400 && err.status < 500) { return false; } return true; // retry everything else (5xx, network errors, etc.) @@ -153,9 +149,14 @@ Pass retry options when creating a schedule: ```ts // Retry up to 5 times if the callback fails -await this.schedule("processTask", 60, { taskId: "123" }, { - retry: { maxAttempts: 5 }, -}); +await this.schedule( + "processTask", + 60, + { taskId: "123" }, + { + retry: { maxAttempts: 5 }, + }, +); // Retry with custom backoff await this.schedule( @@ -172,14 +173,24 @@ await this.schedule( ); // Cron with retries -await this.schedule("0 8 * * *", "dailyDigest", {}, { - retry: { maxAttempts: 3 }, -}); +await this.schedule( + "0 8 * * *", + "dailyDigest", + {}, + { + retry: { maxAttempts: 3 }, + }, +); // Interval with retries -await this.scheduleEvery(30, "poll", { source: "api" }, { - retry: { maxAttempts: 5, baseDelayMs: 200 }, -}); +await this.scheduleEvery( + 30, + "poll", + { source: "api" }, + { + retry: { maxAttempts: 5, baseDelayMs: 200 }, + }, +); ``` @@ -193,9 +204,13 @@ Pass retry options when adding a task to the queue: ```ts -await this.queue("sendEmail", { to: "user@example.com" }, { - retry: { maxAttempts: 5 }, -}); +await this.queue( + "sendEmail", + { to: "user@example.com" }, + { + retry: { maxAttempts: 5 }, + }, +); await this.queue("processWebhook", webhookData, { retry: { @@ -223,9 +238,14 @@ await this.queue("sendEmail", data, { }); // Throws immediately: "retry.baseDelayMs must be > 0" -await this.schedule(60, "process", {}, { - retry: { baseDelayMs: -100 }, -}); +await this.schedule( + 60, + "process", + {}, + { + retry: { baseDelayMs: -100 }, + }, +); // Throws immediately: "retry.maxAttempts must be an integer" await this.retry(() => fetch(url), { maxAttempts: 2.5 }); @@ -303,9 +323,14 @@ To disable retries for a specific task, set `maxAttempts: 1`: ```ts -await this.schedule(60, "oneShot", {}, { - retry: { maxAttempts: 1 }, -}); +await this.schedule( + 60, + "oneShot", + {}, + { + retry: { maxAttempts: 1 }, + }, +); ``` diff --git a/src/content/docs/agents/api-reference/store-and-sync-state.mdx b/src/content/docs/agents/api-reference/store-and-sync-state.mdx index 00573ee09b6..96984557507 100644 --- a/src/content/docs/agents/api-reference/store-and-sync-state.mdx +++ b/src/content/docs/agents/api-reference/store-and-sync-state.mdx @@ -646,7 +646,11 @@ For example, you can use an Agent's built-in SQL database to pull history, query ```ts -export class ReasoningAgent extends Agent { +interface Env { + AI: Ai; +} + +export class ReasoningAgent extends Agent { async callReasoningModel(prompt: Prompt) { let result = this .sql`SELECT * FROM history WHERE user = ${prompt.userId} ORDER BY timestamp DESC LIMIT 1000`; @@ -655,30 +659,22 @@ export class ReasoningAgent extends Agent { context.push(row.entry); } - const client = new OpenAI({ - apiKey: this.env.OPENAI_API_KEY, - }); - - // Combine user history with the current prompt const systemPrompt = prompt.system || "You are a helpful assistant."; const userPrompt = `${prompt.user}\n\nUser history:\n${context.join("\n")}`; try { - const completion = await client.chat.completions.create({ - model: this.env.MODEL || "o3-mini", + const response = await this.env.AI.run("@cf/zai-org/glm-4.7-flash", { messages: [ { role: "system", content: systemPrompt }, { role: "user", content: userPrompt }, ], - temperature: 0.7, - max_tokens: 1000, }); // Store the response in history this - .sql`INSERT INTO history (timestamp, user, entry) VALUES (${new Date()}, ${prompt.userId}, ${completion.choices[0].message.content})`; + .sql`INSERT INTO history (timestamp, user, entry) VALUES (${new Date()}, ${prompt.userId}, ${response.response})`; - return completion.choices[0].message.content; + return response.response; } catch (error) { console.error("Error calling reasoning model:", error); throw error; @@ -705,7 +701,7 @@ This works because each instance of an Agent has its own database, and the state | Method | Signature | Description | | --------------------- | ------------------------------------------------------------ | --------------------------------------------- | | `setState` | `(state: State) => void` | Update state, persist, and broadcast | -| `onStateChanged` | `(state: State, source: Connection \| "server") => void` | Called when state changes | +| `onStateChanged` | `(state: State, source: Connection \| "server") => void` | Called when state changes | | `validateStateChange` | `(nextState: State, source: Connection \| "server") => void` | Validate before persistence (throw to reject) | ### Workflow step methods diff --git a/src/content/docs/agents/api-reference/using-ai-models.mdx b/src/content/docs/agents/api-reference/using-ai-models.mdx index acfa9397c38..b83fb56ea10 100644 --- a/src/content/docs/agents/api-reference/using-ai-models.mdx +++ b/src/content/docs/agents/api-reference/using-ai-models.mdx @@ -17,70 +17,55 @@ import { PackageManagers, } from "~/components"; -Agents can communicate with AI models hosted on any provider, including: +Agents can call AI models from any provider. [Workers AI](/workers-ai/) is built in and requires no API keys. You can also use [OpenAI](https://platform.openai.com/docs/quickstart?language=javascript), [Anthropic](https://docs.anthropic.com/en/api/client-sdks#typescript), [Google Gemini](https://ai.google.dev/gemini-api/docs/openai), or any service that exposes an OpenAI-compatible API. -- [Workers AI](/workers-ai/) -- The [AI SDK](https://sdk.vercel.ai/docs/ai-sdk-core/overview) -- [OpenAI](https://platform.openai.com/docs/quickstart?language=javascript) -- [Anthropic](https://docs.anthropic.com/en/api/client-sdks#typescript) -- [Google's Gemini](https://ai.google.dev/gemini-api/docs/openai) - -You can also use the model routing features in [AI Gateway](/ai-gateway/) to route across providers, eval responses, and manage AI provider rate limits. - -Because Agents are built on top of [Durable Objects](/durable-objects/), each Agent or chat session is associated with a stateful compute instance. Traditional serverless architectures often present challenges for persistent connections needed in real-time applications like chat. - -A user can disconnect during a long-running response from a modern reasoning model (such as `o3-mini` or DeepSeek R1), or lose conversational context when refreshing the browser. Instead of relying on request-response patterns and managing an external database to track & store conversation state, state can be stored directly within the Agent. If a client disconnects, the Agent can write to its own distributed storage, and catch the client up as soon as it reconnects: even if it's hours or days later. +The [AI SDK](https://sdk.vercel.ai/docs/introduction) provides a unified interface across all of these providers, and is what `AIChatAgent` and the starter template use under the hood. You can also use the model routing features in [AI Gateway](/ai-gateway/) to route across providers, eval responses, and manage rate limits. ## Calling AI Models You can call models from any method within an Agent, including from HTTP requests using the [`onRequest`](/agents/api-reference/agents-api/) handler, when a [scheduled task](/agents/api-reference/schedule-tasks/) runs, when handling a WebSocket message in the [`onMessage`](/agents/api-reference/websockets/) handler, or from any of your own methods. -Importantly, Agents can call AI models on their own — autonomously — and can handle long-running responses that can take minutes (or longer) to respond in full. +Agents can call AI models on their own — autonomously — and can handle long-running responses that take minutes (or longer) to respond in full. If a client disconnects mid-stream, the Agent keeps running and can catch the client up when it reconnects. -### Long-running model requests {/* long-running-model-requests */} +### Streaming over WebSockets {/* long-running-model-requests */} -Modern [reasoning models](https://platform.openai.com/docs/guides/reasoning) or "thinking" model can take some time to both generate a response _and_ stream the response back to the client. - -Instead of buffering the entire response, or risking the client disconnecting, you can stream the response back to the client by using the [WebSocket API](/agents/api-reference/websockets/). +Modern reasoning models can take some time to both generate a response _and_ stream the response back to the client. Instead of buffering the entire response, you can stream it back over [WebSockets](/agents/api-reference/websockets/). ```ts import { Agent } from "agents"; -import { OpenAI } from "openai"; +import { streamText } from "ai"; +import { createWorkersAI } from "workers-ai-provider"; -export class MyAgent extends Agent { +interface Env { + AI: Ai; +} + +export class MyAgent extends Agent { async onConnect(connection: Connection, ctx: ConnectionContext) { // } async onMessage(connection: Connection, message: WSMessage) { let msg = JSON.parse(message); - // This can run as long as it needs to, and return as many messages as it needs to! - await queryReasoningModel(connection, msg.prompt); + await this.queryReasoningModel(connection, msg.prompt); } async queryReasoningModel(connection: Connection, userPrompt: string) { - const client = new OpenAI({ - apiKey: this.env.OPENAI_API_KEY, - }); - try { - const stream = await client.chat.completions.create({ - model: this.env.MODEL || "o3-mini", - messages: [{ role: "user", content: userPrompt }], - stream: true, + const workersai = createWorkersAI({ binding: this.env.AI }); + const result = streamText({ + model: workersai("@cf/zai-org/glm-4.7-flash"), + prompt: userPrompt, }); - // Stream responses back as WebSocket messages - for await (const chunk of stream) { - const content = chunk.choices[0]?.delta?.content || ""; - if (content) { - connection.send(JSON.stringify({ type: "chunk", content })); + for await (const chunk of result.textStream) { + if (chunk) { + connection.send(JSON.stringify({ type: "chunk", content: chunk })); } } - // Send completion message connection.send(JSON.stringify({ type: "done" })); } catch (error) { connection.send(JSON.stringify({ type: "error", error: error })); @@ -91,15 +76,13 @@ export class MyAgent extends Agent { -You can also persist AI model responses back to [Agent's internal state](/agents/api-reference/store-and-sync-state/) by using the `this.setState` method. For example, if you run a [scheduled task](/agents/api-reference/schedule-tasks/), you can store the output of the task and read it later. Or, if a user disconnects, read the message history back and send it to the user when they reconnect. - -### Workers AI +You can also persist AI model responses back to [Agent state](/agents/api-reference/store-and-sync-state/) using `this.setState`. If a user disconnects, read the message history back and send it to the user when they reconnect. -### Hosted models +## Workers AI -You can use [any of the models available in Workers AI](/workers-ai/models/) within your Agent by [configuring a binding](/workers-ai/configuration/bindings/). +You can use [any of the models available in Workers AI](/workers-ai/models/) within your Agent by [configuring a binding](/workers-ai/configuration/bindings/). No API keys are required. -Workers AI supports streaming responses out-of-the-box by setting `stream: true`, and we strongly recommend using them to avoid buffering and delaying responses, especially for larger models or reasoning models that require more time to generate a response. +Workers AI supports streaming responses by setting `stream: true`. Use streaming to avoid buffering and delaying responses, especially for larger models or reasoning models. @@ -110,18 +93,17 @@ interface Env { AI: Ai; } -export class MyAgent extends Agent { +export class MyAgent extends Agent { async onRequest(request: Request) { - const response = await env.AI.run( + const stream = await this.env.AI.run( "@cf/deepseek-ai/deepseek-r1-distill-qwen-32b", { prompt: "Build me a Cloudflare Worker that returns JSON.", - stream: true, // Stream a response and don't block the client! + stream: true, }, ); - // Return the stream - return new Response(answer, { + return new Response(stream, { headers: { "content-type": "text/event-stream" }, }); } @@ -130,7 +112,7 @@ export class MyAgent extends Agent { -Your Wrangler configuration will need an `ai` binding added: +Your Wrangler configuration needs an `ai` binding: @@ -146,13 +128,7 @@ Your Wrangler configuration will need an `ai` binding added: ### Model routing -You can also use the model routing features in [AI Gateway](/ai-gateway/) directly from an Agent by specifying a [`gateway` configuration](/ai-gateway/usage/providers/workersai/) when calling the AI binding. - -:::note - -Model routing allows you to route requests to different AI models based on whether they are reachable, rate-limiting your client, and/or if you've exceeded your cost budget for a specific provider. - -::: +You can use [AI Gateway](/ai-gateway/) directly from an Agent by specifying a [`gateway` configuration](/ai-gateway/usage/providers/workersai/) when calling the AI binding. Model routing lets you route requests across providers based on availability, rate limits, or cost budgets. @@ -163,16 +139,16 @@ interface Env { AI: Ai; } -export class MyAgent extends Agent { +export class MyAgent extends Agent { async onRequest(request: Request) { - const response = await env.AI.run( + const response = await this.env.AI.run( "@cf/deepseek-ai/deepseek-r1-distill-qwen-32b", { prompt: "Build me a Cloudflare Worker that returns JSON.", }, { gateway: { - id: "{gateway_id}", // Specify your AI Gateway ID here + id: "{gateway_id}", skipCache: false, cacheTtl: 3360, }, @@ -186,7 +162,7 @@ export class MyAgent extends Agent { -Your Wrangler configuration will need an `ai` binding added. This is shared across both Workers AI and AI Gateway. +The `ai` binding in your Wrangler configuration is shared across both Workers AI and AI Gateway. @@ -202,11 +178,39 @@ Your Wrangler configuration will need an `ai` binding added. This is shared acro Visit the [AI Gateway documentation](/ai-gateway/) to learn how to configure a gateway and retrieve a gateway ID. -### AI SDK +## AI SDK -The [AI SDK](https://sdk.vercel.ai/docs/introduction) provides a unified API for using AI models, including for text generation, tool calling, structured responses, image generation, and more. +The [AI SDK](https://sdk.vercel.ai/docs/introduction) provides a unified API for text generation, tool calling, structured responses, and more. It works with any provider that has an AI SDK adapter, including Workers AI via [`workers-ai-provider`](https://www.npmjs.com/package/workers-ai-provider). -To use the AI SDK, install the `ai` package and use it within your Agent. The example below shows how it use it to generate text on request, but you can use it from any method within your Agent, including WebSocket handlers, as part of a scheduled task, or even when the Agent is initialized. + + + + +```ts +import { Agent } from "agents"; +import { generateText } from "ai"; +import { createWorkersAI } from "workers-ai-provider"; + +interface Env { + AI: Ai; +} + +export class MyAgent extends Agent { + async onRequest(request: Request): Promise { + const workersai = createWorkersAI({ binding: this.env.AI }); + const { text } = await generateText({ + model: workersai("@cf/zai-org/glm-4.7-flash"), + prompt: "Build me an AI agent on Cloudflare Workers", + }); + + return Response.json({ modelResponse: text }); + } +} +``` + + + +You can swap the provider to use OpenAI, Anthropic, or any other AI SDK-compatible adapter: @@ -220,7 +224,7 @@ import { openai } from "@ai-sdk/openai"; export class MyAgent extends Agent { async onRequest(request: Request): Promise { const { text } = await generateText({ - model: openai("o3-mini"), + model: openai("gpt-4o"), prompt: "Build me an AI agent on Cloudflare Workers", }); @@ -231,11 +235,11 @@ export class MyAgent extends Agent { -### OpenAI compatible endpoints +## OpenAI-compatible endpoints -Agents can call models across any service, including those that support the OpenAI API. For example, you can use the OpenAI SDK to use one of [Google's Gemini models](https://ai.google.dev/gemini-api/docs/openai#node.js) directly from your Agent. +Agents can call models across any service that supports the OpenAI API. For example, you can use the OpenAI SDK to call one of [Google's Gemini models](https://ai.google.dev/gemini-api/docs/openai#node.js) directly from your Agent. -Agents can stream responses back over HTTP using Server Sent Events (SSE) from within an `onRequest` handler, or by using the native [WebSockets](/agents/api-reference/websockets/) API in your Agent to responses back to a client, which is especially useful for larger models that can take over 30+ seconds to reply. +Agents can stream responses back over HTTP using Server-Sent Events (SSE) from within an `onRequest` handler, or by using the native [WebSocket API](/agents/api-reference/websockets/) to stream responses back to a client. @@ -245,29 +249,25 @@ import { OpenAI } from "openai"; export class MyAgent extends Agent { async onRequest(request: Request): Promise { - const openai = new OpenAI({ + const client = new OpenAI({ apiKey: this.env.GEMINI_API_KEY, baseURL: "https://generativelanguage.googleapis.com/v1beta/openai/", }); - // Create a TransformStream to handle streaming data let { readable, writable } = new TransformStream(); let writer = writable.getWriter(); const textEncoder = new TextEncoder(); - // Use ctx.waitUntil to run the async function in the background - // so that it doesn't block the streaming response - ctx.waitUntil( + this.ctx.waitUntil( (async () => { - const stream = await openai.chat.completions.create({ - model: "4o", + const stream = await client.chat.completions.create({ + model: "gemini-2.0-flash", messages: [ { role: "user", content: "Write me a Cloudflare Worker." }, ], stream: true, }); - // loop over the data as it is streamed and write to the writeable for await (const part of stream) { writer.write( textEncoder.encode(part.choices[0]?.delta?.content || ""), @@ -277,7 +277,6 @@ export class MyAgent extends Agent { })(), ); - // Return the readable stream back to the client return new Response(readable); } } diff --git a/src/content/docs/agents/api-reference/websockets.mdx b/src/content/docs/agents/api-reference/websockets.mdx index 5ff4c325347..7992e69e453 100644 --- a/src/content/docs/agents/api-reference/websockets.mdx +++ b/src/content/docs/agents/api-reference/websockets.mdx @@ -186,10 +186,7 @@ Tag connections for easy filtering. Override `getConnectionTags()` to assign tag ```ts export class ChatAgent extends Agent { - getConnectionTags( - connection: Connection, - ctx: ConnectionContext, - ): string[] { + getConnectionTags(connection: Connection, ctx: ConnectionContext): string[] { const url = new URL(ctx.request.url); const role = url.searchParams.get("role"); @@ -217,7 +214,7 @@ export class ChatAgent extends Agent { | ------------------- | ----------------------------------------- | -------------------------------------- | | `getConnections` | `(tag?: string) => Iterable` | Get all connections, optionally by tag | | `getConnection` | `(id: string) => Connection \| undefined` | Get connection by ID | -| `getConnectionTags` | `(connection, ctx) => string[]` | Override to tag connections | +| `getConnectionTags` | `(connection, ctx) => string[]` | Override to tag connections | | `broadcast` | `(message, without?: string[]) => void` | Send to all connections | ## Handling binary data diff --git a/src/content/docs/agents/concepts/agent-class.mdx b/src/content/docs/agents/concepts/agent-class.mdx index a97fe846a24..4d69de6dbd7 100644 --- a/src/content/docs/agents/concepts/agent-class.mdx +++ b/src/content/docs/agents/concepts/agent-class.mdx @@ -5,17 +5,15 @@ sidebar: order: 5 --- -The core of the `agents` library is the exported `Agent` class. Following the pattern from [Durable Objects](/durable-objects/api/), the main API for developers is to extend the `Agent` class to inherit all the built-in features. While this effectively is a supercharged primitive that allows developers to only write the logic they need in their agents, it obscures the inner workings. +The core of the `agents` library is the `Agent` class. You extend it, override a few methods, and get state management, WebSockets, scheduling, RPC, and more for free. This page explains how `Agent` is built, layer by layer, so you understand what is happening under the hood. -This document tries to bridge that gap, empowering any developer aiming to get started writing agents to get the full picture and avoid common pitfalls. The snippets shown here are primarily illustrative, and do not necessarily represent best practices. For a more in-depth look at the inner workings of the `Agent` class, check out the [API reference](/agents/api-reference/) and the [source code](https://github.com/cloudflare/agents/blob/main/packages/agents/src/index.ts). +The snippets shown here are illustrative and do not necessarily represent best practices. For the full API, refer to the [API reference](/agents/api-reference/) and the [source code](https://github.com/cloudflare/agents/blob/main/packages/agents/src/index.ts). ## What is the Agent? -The `Agent` class is an extension of `DurableObject`. That is to say, they _are_ Durable Objects. If you are not familiar with Durable Objects, it is highly recommended that you read [What are Durable Objects](/durable-objects/), but at their core, Durable Objects are globally addressable (each instance has a unique ID) single-threaded compute instances with long term storage (key-value/SQLite). +The `Agent` class is an extension of `DurableObject` — agents _are_ Durable Objects. If you are not familiar with Durable Objects, read [What are Durable Objects](/durable-objects/) first. At their core, Durable Objects are globally addressable (each instance has a unique ID), single-threaded compute instances with long-term storage (key-value and SQLite). -Note that `Agent` does not extend `DurableObject` directly, but instead extends `Server`. `Server` is a class provided by [PartyKit](https://github.com/cloudflare/partykit/tree/main/packages/partyserver). - -You can visualize the logic as a Matryoshka doll: **DurableObject** > **Server** > **Agent**. +`Agent` does not extend `DurableObject` directly. It extends `Server` from the [`partyserver`](https://github.com/cloudflare/partykit/tree/main/packages/partyserver) package, which extends `DurableObject`. Think of it as layers: **DurableObject** > **Server** > **Agent**. ## Layer 0: Durable Object @@ -41,8 +39,8 @@ By writing a Durable Object class which inherits from the built-in type `Durable // not initialized or maybe had never even been created! const stub = env.MY_DO.getByName("foo"); -// We can call any public method of the class since. The runtime -// **ensures** the constructor is called for us if the instance wasn't active. +// We can call any public method on the class. The runtime +// ensures the constructor is called if the instance was not active. await stub.bar(); ``` @@ -73,8 +71,7 @@ export class MyDurableObject extends DurableObject { } async webSocketMessage(ws, message) { - // echo back the messages - ws.send(msg); + ws.send(message); } } ``` @@ -95,30 +92,27 @@ The base `DurableObject` class sets the [DurableObjectState](/durable-objects/ap ```ts const sql = this.ctx.storage.sql; -const kv = this.ctx.storage.kv; -// An example of a synchronous SQL query +// Synchronous SQL query const rows = sql.exec("SELECT * FROM contacts WHERE country = ?", "US"); -// And an example of the synchronous KV -const token = kv.get("someToken"); +// Key-value storage +const token = this.ctx.storage.get("someToken"); ``` ### `this.ctx.env` Lastly, it is worth mentioning that the Durable Object also has the Worker `Env` in `this.env`. Learn more in [Bindings](/workers/runtime-apis/bindings). -## Layer 1: Partykit `Server` +## Layer 1: `Server` (partyserver) -Now that you have seen what Durable Objects come with out-of-the-box, what [PartyKit](https://github.com/cloudflare/partykit)'s `Server` (package `partyserver`) implements will be clearer. It is an opinionated `DurableObject` wrapper that improves DX by hiding away Durable Object primitives in favor of more developer friendly callbacks. +Now that you have seen what Durable Objects provide out of the box, the `Server` class from [`partyserver`](https://github.com/cloudflare/partykit/tree/main/packages/partyserver) will make more sense. It is an opinionated `DurableObject` wrapper that replaces low-level primitives with developer-friendly callbacks. -An important note is that `Server` does NOT persist to the Durable Object storage, so you will not see extra storage operations by using it. +`Server` does not add any storage operations of its own — it only wraps the Durable Object lifecycle. ### Addressing -`partyserver` exposes helper to address your Durable Objects instead of manually through your bindings. This allows `partyserver` to implement several improvements, including a unique URL routing scheme for your Durable Objects (e.g. `/servers/:durableClass/:durableName`). - -Compare this to the Durable Object addressing [example above](#rpc). +`partyserver` exposes helpers to address Durable Objects by name instead of going through bindings manually. This includes a URL routing scheme (`/servers/:durableClass/:durableName`) that the Agent layer builds on. ```ts // Note the await here! @@ -128,25 +122,21 @@ const stub = await getServerByName(env.MY_DO, "foo"); await stub.bar(); ``` -Since we have a URL addressing scheme, we also get access to `routePartykitRequest()`. +The URL scheme also enables a request router. In the Agent layer, this is re-exported as `routeAgentRequest`: ```ts async fetch(request: Request, env: Env, ctx: ExecutionContext) { - // Behind the scenes, PartyKit normalizes your DO binding names - // and tries to do some pattern matching. - const res = await routePartykitRequest(request, env); + const res = await routeAgentRequest(request, env); if (res) return res; - return Response("Not found", { status: 404 }); + return new Response("Not found", { status: 404 }); } ``` -You can also refer to [the implementation](https://github.com/cloudflare/partykit/blob/main/packages/partyserver/src/index.ts#L122) to learn more. - ### `onStart` -The extra plumbing that `Server` includes on addressing allows it to expose an `onStart` callback that is executed every time the Durable Object starts up (the Durable Object was evicted, hibernated or never created at all) and before any `fetch` or RPC. +The addressing layer allows `Server` to expose an `onStart` callback that runs every time the Durable Object starts up (after eviction, hibernation, or first creation) and before any `fetch` or RPC call. ```ts class MyServer extends Server { @@ -217,7 +207,7 @@ class MyAgent extends Agent { } ``` -State is stored in the `cf_agents_state` SQL table. State messages are sent with `type: "cf_agent_state"` (both from the client and the server). Since the `agents` provides [JS and React clients](/agents/api-reference/store-and-sync-state/#synchronizing-state), real-time state updates are available out of the box. +State is stored in the `cf_agents_state` SQL table. State messages are sent with `type: "cf_agent_state"` (both from the client and the server). Since `agents` provides [JS and React clients](/agents/api-reference/store-and-sync-state/#synchronizing-state), real-time state updates are available out of the box. ### `this.sql` @@ -247,7 +237,7 @@ class MyAgent extends Agent { ### RPC and Callable Methods -`agents` take Durable Objects RPC one step forward by implementing RPC through WebSockets, so clients can also call methods on the Agent directly. To make a method callable through WS, developers can use the `@callable` decorator. Methods can return a serializable value or a stream (when using `@callable({ stream: true })`). +`agents` takes Durable Objects RPC one step further by implementing RPC through WebSockets, so clients can call methods on the Agent directly. To make a method callable through WebSocket, use the `@callable()` decorator. Methods can return a serializable value or a stream (when using `@callable({ stream: true })`). ```ts class MyAgent extends Agent { @@ -389,7 +379,7 @@ export default { ### Context Management -`agents` wraps all your methods with an `AsyncLocalStorage` to maintain context throughout the request lifecycle. This allows you to access the current agent, connection, request, or email (depending of what event is being handled) from anywhere in your code: +`agents` wraps all your methods with an `AsyncLocalStorage` to maintain context throughout the request lifecycle. This allows you to access the current agent, connection, request, or email (depending on what event is being handled) from anywhere in your code: ```ts import { getCurrentAgent } from "agents"; @@ -469,17 +459,22 @@ You can safely call `this.destroy()` from within a scheduled task callback. The ### Routing -The `Agent` class re-exports PartyKit's [addressing helpers](#addressing) as `getAgentByName` and `routeAgentRequest`. +The `Agent` class re-exports the [addressing helpers](#addressing) as `getAgentByName` and `routeAgentRequest`. ```ts -// Same API as getServerByName const stub = await getAgentByName(env.MY_DO, "foo"); -// ... +await stub.someMethod(); -// Same API as routeServerRequest const res = await routeAgentRequest(request, env); - if (res) return res; -return Response("Not found", { status: 404 }); +return new Response("Not found", { status: 404 }); ``` + +## Layer 3: `AIChatAgent` + +The [`AIChatAgent`](/agents/api-reference/chat-agents/) class from `@cloudflare/ai-chat` extends `Agent` with an opinionated layer for AI chat. It adds automatic message persistence to SQLite, resumable streaming, tool support (server-side, client-side, and human-in-the-loop), and a React hook (`useAgentChat`) for building chat UIs. + +The full hierarchy is: **DurableObject** > **Server** > **Agent** > **AIChatAgent**. + +If you are building a chat agent, start with `AIChatAgent`. If you need lower-level control or are not building a chat interface, use `Agent` directly. diff --git a/src/content/docs/agents/concepts/calling-llms.mdx b/src/content/docs/agents/concepts/calling-llms.mdx index 91015500250..529590ff99d 100644 --- a/src/content/docs/agents/concepts/calling-llms.mdx +++ b/src/content/docs/agents/concepts/calling-llms.mdx @@ -2,35 +2,221 @@ title: Calling LLMs pcx_content_type: concept tags: - - LLM + - AI sidebar: order: 6 --- -import { Render } from "~/components"; +import { TypeScriptExample, LinkCard } from "~/components"; -### Understanding LLM providers and model types +Agents change how you work with LLMs. In a stateless Worker, every request starts from scratch — you reconstruct context, call a model, return the response, and forget everything. An Agent keeps state between calls, stays connected to clients over WebSocket, and can call models on its own schedule without a user present. -Different LLM providers offer models optimized for specific types of tasks. When building AI systems, choosing the right model is crucial for both performance and cost efficiency. +This page covers the patterns that become possible when your LLM calls happen inside a stateful Agent. For provider setup and code examples, refer to [Using AI Models](/agents/api-reference/using-ai-models/). -#### Reasoning Models +## State as context -Models like OpenAI's o1, Anthropic's Claude, and DeepSeek's R1 are particularly well-suited for complex reasoning tasks. These models excel at: +Every Agent has a built-in [SQL database](/agents/api-reference/store-and-sync-state/) and key-value state. Instead of passing an entire conversation history from the client on every request, the Agent stores it and builds prompts from its own storage. -- Breaking down problems into steps -- Following complex instructions -- Maintaining context across long conversations -- Generating code and technical content + -For example, when implementing a travel booking system, you might use a reasoning model to analyze travel requirements and generate appropriate booking strategies. +```ts +import { Agent } from "agents"; -#### Instruction Models +export class ResearchAgent extends Agent { + async buildPrompt(userMessage: string) { + const history = this.sql<{ role: string; content: string }>` + SELECT role, content FROM messages + ORDER BY timestamp DESC LIMIT 50`; -Models like GPT-4 and Claude Instant are optimized for following straightforward instructions efficiently. They work well for: + const preferences = this.sql<{ key: string; value: string }>` + SELECT key, value FROM user_preferences`; -- Content generation -- Simple classification tasks -- Basic question answering -- Text transformation + return [ + { role: "system", content: this.systemPrompt(preferences) }, + ...history.reverse(), + { role: "user", content: userMessage }, + ]; + } +} +``` -These models are often more cost-effective for straightforward tasks that do not require complex reasoning. + + +This means the client does not need to send the full conversation on every message. The Agent owns the history, can prune it, enrich it with retrieved documents, or summarize older turns before sending to the model. + +## Surviving disconnections + +Reasoning models like DeepSeek R1 or GLM-4 can take 30 seconds to several minutes to respond. In a stateless request-response architecture, the client must stay connected the entire time. If the connection drops, the response is lost. + +An Agent keeps running after the client disconnects. When the response arrives, the Agent can persist it to state and deliver it when the client reconnects — even hours or days later. + + + +```ts +import { Agent } from "agents"; +import { streamText } from "ai"; +import { createWorkersAI } from "workers-ai-provider"; + +export class MyAgent extends Agent { + async onMessage(connection: Connection, message: WSMessage) { + const { prompt } = JSON.parse(message as string); + const workersai = createWorkersAI({ binding: this.env.AI }); + + const result = streamText({ + model: workersai("@cf/zai-org/glm-4.7-flash"), + prompt, + }); + + for await (const chunk of result.textStream) { + connection.send(JSON.stringify({ type: "chunk", content: chunk })); + } + + this.sql`INSERT INTO responses (prompt, response, timestamp) + VALUES (${prompt}, ${await result.text}, ${Date.now()})`; + } +} +``` + + + +With [`AIChatAgent`](/agents/api-reference/chat-agents/), this is handled automatically — messages are persisted to SQLite and streams resume on reconnect. + +## Autonomous model calls + +Agents do not need a user request to call a model. You can schedule model calls to run in the background — for nightly summarization, periodic classification, monitoring, or any task that should happen without human interaction. + + + +```ts +import { Agent } from "agents"; + +export class DigestAgent extends Agent { + async onStart() { + this.schedule("0 8 * * *", "generateDailyDigest", {}); + } + + async generateDailyDigest() { + const articles = this.sql<{ title: string; body: string }>` + SELECT title, body FROM articles + WHERE created_at > datetime('now', '-1 day')`; + + const workersai = createWorkersAI({ binding: this.env.AI }); + const { text } = await generateText({ + model: workersai("@cf/zai-org/glm-4.7-flash"), + prompt: `Summarize these articles:\n${articles.map((a) => a.title + ": " + a.body).join("\n\n")}`, + }); + + this.sql`INSERT INTO digests (summary, created_at) + VALUES (${text}, ${Date.now()})`; + + this.broadcast(JSON.stringify({ type: "digest", summary: text })); + } +} +``` + + + +## Multi-model pipelines + +Because an Agent maintains state across calls, you can chain multiple models in a single method — using a fast model for classification, a reasoning model for planning, and an embedding model for retrieval — without losing context between steps. + + + +```ts +import { Agent } from "agents"; +import { generateText, embed } from "ai"; +import { createWorkersAI } from "workers-ai-provider"; + +export class TriageAgent extends Agent { + async triage(ticket: string) { + const workersai = createWorkersAI({ binding: this.env.AI }); + + const { text: category } = await generateText({ + model: workersai("@cf/zai-org/glm-4.7-flash"), + prompt: `Classify this support ticket into one of: billing, technical, account. Ticket: ${ticket}`, + }); + + const { embedding } = await embed({ + model: workersai("@cf/baai/bge-base-en-v1.5"), + value: ticket, + }); + const similar = await this.env.VECTOR_DB.query(embedding, { topK: 5 }); + + const { text: response } = await generateText({ + model: workersai("@cf/zai-org/glm-4.7-flash"), + prompt: `Draft a response for this ${category} ticket. Similar resolved tickets: ${JSON.stringify(similar)}. Ticket: ${ticket}`, + }); + + this.sql`INSERT INTO tickets (content, category, response, created_at) + VALUES (${ticket}, ${category}, ${response}, ${Date.now()})`; + + return { category, response }; + } +} +``` + + + +Each intermediate result stays in the Agent's memory for the duration of the method, and the final result is persisted to SQL for future reference. + +## Caching and cost control + +Persistent storage means you can cache model responses and avoid redundant calls. This is especially useful for expensive operations like embeddings or long reasoning chains. + + + +```ts +import { Agent } from "agents"; + +export class CachingAgent extends Agent { + async cachedGenerate(prompt: string) { + const cached = this.sql<{ response: string }>` + SELECT response FROM llm_cache WHERE prompt = ${prompt}`; + + if (cached.length > 0) { + return cached[0].response; + } + + const workersai = createWorkersAI({ binding: this.env.AI }); + const { text } = await generateText({ + model: workersai("@cf/zai-org/glm-4.7-flash"), + prompt, + }); + + this.sql`INSERT INTO llm_cache (prompt, response, created_at) + VALUES (${prompt}, ${text}, ${Date.now()})`; + + return text; + } +} +``` + + + +For provider-level caching and rate limit management across multiple agents, use [AI Gateway](/ai-gateway/). + +## Next steps + + + + + + + + diff --git a/src/content/docs/agents/getting-started/build-a-chat-agent.mdx b/src/content/docs/agents/getting-started/build-a-chat-agent.mdx index 06cb6128996..ee4712430c1 100644 --- a/src/content/docs/agents/getting-started/build-a-chat-agent.mdx +++ b/src/content/docs/agents/getting-started/build-a-chat-agent.mdx @@ -172,11 +172,11 @@ export default { ### What each tool type does -| Tool | `execute`? | `needsApproval`? | Behavior | -| ---------------- | ---------- | ----------------- | ---------------------------------------------------- | -| `getWeather` | Yes | No | Runs on the server automatically | -| `getUserTimezone`| No | No | Sent to the client; browser provides the result | -| `calculate` | Yes | Yes (large numbers) | Pauses for user approval, then runs on server | +| Tool | `execute`? | `needsApproval`? | Behavior | +| ----------------- | ---------- | ------------------- | ----------------------------------------------- | +| `getWeather` | Yes | No | Runs on the server automatically | +| `getUserTimezone` | No | No | Sent to the client; browser provides the result | +| `calculate` | Yes | Yes (large numbers) | Pauses for user approval, then runs on server | ## 4. Write the client diff --git a/src/content/docs/agents/guides/chatgpt-app.mdx b/src/content/docs/agents/guides/chatgpt-app.mdx index 1d5d90c160a..4a7378b7942 100644 --- a/src/content/docs/agents/guides/chatgpt-app.mdx +++ b/src/content/docs/agents/guides/chatgpt-app.mdx @@ -335,7 +335,9 @@ function createServer() { { uri: "ui://widget/index.html", mimeType: "text/html+skybridge", - text: await getWidgetHtml(extra.requestInfo?.headers.host as string), + text: await getWidgetHtml( + extra.requestInfo?.headers.host as string, + ), }, ], }; diff --git a/src/content/docs/agents/guides/connect-mcp-client.mdx b/src/content/docs/agents/guides/connect-mcp-client.mdx index 8d5618ee1e1..41004096a2e 100644 --- a/src/content/docs/agents/guides/connect-mcp-client.mdx +++ b/src/content/docs/agents/guides/connect-mcp-client.mdx @@ -59,7 +59,7 @@ An MCP server to connect to (or use the public example in this tutorial). HelloAgent: DurableObjectNamespace; }; - export class HelloAgent extends Agent { + export class HelloAgent extends Agent { async onRequest(request: Request): Promise { return new Response("Hello, Agent!", { status: 200 }); } @@ -87,7 +87,7 @@ An MCP server to connect to (or use the public example in this tutorial). ```ts title="src/index.ts" - export class HelloAgent extends Agent { + export class HelloAgent extends Agent { async onRequest(request: Request): Promise { const url = new URL(request.url); @@ -166,7 +166,7 @@ The `addMcpServer()` method connects to an MCP server. If the server requires OA ```ts title="src/index.ts" - export class HelloAgent extends Agent { + export class HelloAgent extends Agent { async onRequest(request: Request): Promise { const url = new URL(request.url); diff --git a/src/content/docs/agents/guides/oauth-mcp-client.mdx b/src/content/docs/agents/guides/oauth-mcp-client.mdx index a63b8f0628d..dc6138c6199 100644 --- a/src/content/docs/agents/guides/oauth-mcp-client.mdx +++ b/src/content/docs/agents/guides/oauth-mcp-client.mdx @@ -34,7 +34,7 @@ When connecting to an OAuth-protected server, check if `authUrl` is returned. If ```ts title="src/index.ts" -export class MyAgent extends Agent { +export class MyAgent extends Agent { async onRequest(request: Request): Promise { const url = new URL(request.url); @@ -79,7 +79,7 @@ Redirect users back to your application after OAuth completes: ```ts title="src/index.ts" -export class MyAgent extends Agent { +export class MyAgent extends Agent { onStart() { this.mcp.configureOAuthCallback({ successRedirect: "/dashboard", @@ -102,7 +102,7 @@ If you opened OAuth in a popup, close it automatically when complete: ```ts title="src/index.ts" import { Agent } from "agents"; -export class MyAgent extends Agent { +export class MyAgent extends Agent { onStart() { this.mcp.configureOAuthCallback({ customHandler: () => { @@ -181,7 +181,7 @@ Poll the connection status via an endpoint: ```ts title="src/index.ts" -export class MyAgent extends Agent { +export class MyAgent extends Agent { async onRequest(request: Request): Promise { const url = new URL(request.url); @@ -305,7 +305,7 @@ type Env = { MyAgent: DurableObjectNamespace; }; -export class MyAgent extends Agent { +export class MyAgent extends Agent { onStart() { this.mcp.configureOAuthCallback({ customHandler: () => { diff --git a/src/content/docs/agents/guides/remote-mcp-server.mdx b/src/content/docs/agents/guides/remote-mcp-server.mdx index 4cf7baa2200..3b325d91ee3 100644 --- a/src/content/docs/agents/guides/remote-mcp-server.mdx +++ b/src/content/docs/agents/guides/remote-mcp-server.mdx @@ -18,11 +18,11 @@ This guide will show you how to deploy your own remote MCP server on Cloudflare The Agents SDK provides multiple ways to create MCP servers. Choose the approach that fits your use case: -| Approach | Stateful? | Requires Durable Objects? | Best for | -| --- | --- | --- | --- | -| [`createMcpHandler()`](/agents/api-reference/mcp-handler-api/) | No | No | Stateless tools, simplest setup | -| [`McpAgent`](/agents/api-reference/mcp-agent-api/) | Yes | Yes | Stateful tools, per-session state, elicitation | -| Raw `WebStandardStreamableHTTPServerTransport` | No | No | Full control, no SDK dependency | +| Approach | Stateful? | Requires Durable Objects? | Best for | +| -------------------------------------------------------------- | --------- | ------------------------- | ---------------------------------------------- | +| [`createMcpHandler()`](/agents/api-reference/mcp-handler-api/) | No | No | Stateless tools, simplest setup | +| [`McpAgent`](/agents/api-reference/mcp-agent-api/) | Yes | Yes | Stateful tools, per-session state, elicitation | +| Raw `WebStandardStreamableHTTPServerTransport` | No | No | Full control, no SDK dependency | - **`createMcpHandler()`** is the fastest way to get a stateless MCP server running. Use it when your tools do not need per-session state. - **`McpAgent`** gives you a Durable Object per session with built-in state management, elicitation support, and both SSE and Streamable HTTP transports. @@ -48,70 +48,68 @@ You can use the [Wrangler CLI](/workers/wrangler) to create a new MCP Server on 1. Open a terminal and run the following command: - + - During setup, select the following options: - - For _Do you want to add an AGENTS.md file to help AI coding tools understand - Cloudflare APIs?_, choose `No`. - - For _Do you want to use git for version control?_, choose `No`. - - For _Do you want to deploy your application?_, choose `No` (we will be testing the server before deploying). + During setup, select the following options: - For _Do you want to add an AGENTS.md file to help AI coding tools understand + Cloudflare APIs?_, choose `No`. - For _Do you want to use git for version control?_, choose `No`. - For _Do you want to deploy your application?_, choose `No` (we will be testing the server before deploying). - Now, you have the MCP server setup, with dependencies installed. + Now, you have the MCP server setup, with dependencies installed. 2. Move into the project folder: - ```sh - cd remote-mcp-server-authless - ``` + ```sh + cd remote-mcp-server-authless + ``` 3. In the directory of your new project, run the following command to start the development server: - ```sh - npm start - ``` + ```sh + npm start + ``` - ```sh output - ⎔ Starting local server... - [wrangler:info] Ready on http://localhost:8788 - ``` + ```sh output + ⎔ Starting local server... + [wrangler:info] Ready on http://localhost:8788 + ``` - Check the command output for the local port. In this example, the MCP server runs on port `8788`, and the MCP endpoint URL is `http://localhost:8788/mcp`. - :::note - You cannot interact with the MCP server by opening the `/mcp` URL directly in a web browser. The `/mcp` endpoint expects an MCP client to send MCP protocol messages, which a browser does not do by default. In the next step, we will demonstrate how to connect to the server using an MCP client. - ::: + Check the command output for the local port. In this example, the MCP server runs on port `8788`, and the MCP endpoint URL is `http://localhost:8788/mcp`. + :::note + You cannot interact with the MCP server by opening the `/mcp` URL directly in a web browser. The `/mcp` endpoint expects an MCP client to send MCP protocol messages, which a browser does not do by default. In the next step, we will demonstrate how to connect to the server using an MCP client. + ::: 4. To test the server locally: + 1. In a new terminal, run the [MCP inspector](https://github.com/modelcontextprotocol/inspector). The MCP inspector is an interactive MCP client that allows you to connect to your MCP server and invoke tools from a web browser. - 1. In a new terminal, run the [MCP inspector](https://github.com/modelcontextprotocol/inspector). The MCP inspector is an interactive MCP client that allows you to connect to your MCP server and invoke tools from a web browser. + ```sh + npx @modelcontextprotocol/inspector@latest + ``` - ```sh - npx @modelcontextprotocol/inspector@latest - ``` + ```sh output + 🚀 MCP Inspector is up and running at: + http://localhost:5173/?MCP_PROXY_AUTH_TOKEN=46ab..cd3 - ```sh output - 🚀 MCP Inspector is up and running at: - http://localhost:5173/?MCP_PROXY_AUTH_TOKEN=46ab..cd3 + 🌐 Opening browser... + ``` - 🌐 Opening browser... - ``` + The MCP Inspector will launch in your web browser. You can also launch it manually by opening a browser and going to `http://localhost:`. Check the command output for the local port where MCP Inspector is running. In this example, MCP Inspector is served on port `5173`. - The MCP inspector will launch in your web browser. You can also launch it manually by opening a browser and going to `https://localhost:`. Check the command output for the local port where MCP Inspector is running. In this example, the MCP server is served on port `5173`. - - 3. In the MCP inspector, enter the URL of your MCP server (`http://localhost:8788/mcp`), and select **Connect**. Select **List Tools** to show the tools that your MCP server exposes. + 2. In the MCP inspector, enter the URL of your MCP server (`http://localhost:8788/mcp`), and select **Connect**. Select **List Tools** to show the tools that your MCP server exposes. 5. You can now deploy your MCP server to Cloudflare. From your project directory, run: - ```sh - npx wrangler@latest deploy - ``` + ```sh + npx wrangler@latest deploy + ``` - If you have already [connected a git repository](/workers/ci-cd/builds/) to the Worker with your MCP server, you can deploy your MCP server by pushing a change or merging a pull request to the main branch of the repository. + If you have already [connected a git repository](/workers/ci-cd/builds/) to the Worker with your MCP server, you can deploy your MCP server by pushing a change or merging a pull request to the main branch of the repository. - The MCP server will be deployed to your `*.workers.dev` subdomain at `https://remote-mcp-server-authless.your-account.workers.dev/mcp`. + The MCP server will be deployed to your `*.workers.dev` subdomain at `https://remote-mcp-server-authless.your-account.workers.dev/mcp`. 6. To test the remote MCP server, take the URL of your deployed MCP server (`https://remote-mcp-server-authless.your-account.workers.dev/mcp`) and enter it in the MCP inspector running on `http://localhost:5173`. @@ -125,28 +123,29 @@ For example, to connect from Claude Desktop: 1. Update your Claude Desktop configuration to point to the URL of your MCP server: - ```json - { - "mcpServers": { - "math": { - "command": "npx", - "args": [ - "mcp-remote", - "https://remote-mcp-server-authless.your-account.workers.dev/mcp" - ] - } - } - } - ``` + ```json + { + "mcpServers": { + "math": { + "command": "npx", + "args": [ + "mcp-remote", + "https://remote-mcp-server-authless.your-account.workers.dev/mcp" + ] + } + } + } + ``` 2. Restart Claude Desktop to load the MCP Server. Once this is done, Claude will be able to make calls to your remote MCP server. 3. To test, ask Claude to use one of your tools. For example: - ```txt - Could you use the math tool to add 23 and 19? - ``` - Claude should invoke the tool and show the result generated by the remote MCP server. + ```txt + Could you use the math tool to add 23 and 19? + ``` + + Claude should invoke the tool and show the result generated by the remote MCP server. To learn how to use remote MCP servers with other MCP clients, refer to [Test a Remote MCP Server](/agents/guides/test-remote-mcp-server). @@ -156,13 +155,13 @@ The public MCP server example you deployed earlier allows any client to connect ### Cloudflare Access OAuth -You can configure your MCP server to require user authentication through Cloudflare Access. Cloudflare Access acts as an identity aggregator and verifies user emails, signals from your existing [identity providers](/cloudflare-one/integrations/identity-providers/) (such as GitHub or Google), and other attributes such as IP address or device certificates. When users connect to the MCP server, they will be prompted to log in to the configured identity provider and are only granted access if they pass your [Access policies](/cloudflare-one/access-controls/policies/#selectors). +You can configure your MCP server to require user authentication through Cloudflare Access. Cloudflare Access acts as an identity aggregator and verifies user emails, signals from your existing [identity providers](/cloudflare-one/integrations/identity-providers/) (such as GitHub or Google), and other attributes such as IP address or device certificates. When users connect to the MCP server, they will be prompted to log in to the configured identity provider and are only granted access if they pass your [Access policies](/cloudflare-one/access-controls/policies/#selectors). For a step-by-step deployment guide, refer to [Secure MCP servers with Access for SaaS](/cloudflare-one/access-controls/ai-controls/saas-mcp/). ### Third-party OAuth -You can connect your MCP server with any [OAuth provider](/agents/model-context-protocol/authorization/#2-third-party-oauth-provider) that supports the OAuth 2.0 specification, including GitHub, Google, Slack, [Stytch](/agents/model-context-protocol/authorization/#stytch), [Auth0](/agents/model-context-protocol/authorization/#stytch), [WorkOS](/agents/model-context-protocol/authorization/#stytch), and more. +You can connect your MCP server with any [OAuth provider](/agents/model-context-protocol/authorization/#2-third-party-oauth-provider) that supports the OAuth 2.0 specification, including GitHub, Google, Slack, [Stytch](/agents/model-context-protocol/authorization/#stytch), [Auth0](/agents/model-context-protocol/authorization/#auth0), [WorkOS](/agents/model-context-protocol/authorization/#workos), and more. The following example demonstrates how to use GitHub as an OAuth provider. @@ -191,7 +190,7 @@ import GitHubHandler from "./github-handler"; export default new OAuthProvider({ apiRoute: "/mcp", - apiHandler: MyMCP.Router, + apiHandler: MyMCP.serve("/mcp"), defaultHandler: GitHubHandler, authorizeEndpoint: "/authorize", tokenEndpoint: "/token", @@ -199,7 +198,7 @@ export default new OAuthProvider({ }); ``` -This will ensure that your users are redirected to GitHub to authenticate. To get this working though, you need to create OAuth client apps in the steps below. +This ensures that your users are redirected to GitHub to authenticate. To get this working though, you need to create OAuth client apps in the steps below. #### Step 2 — Create an OAuth App @@ -208,45 +207,44 @@ You'll need to create two [GitHub OAuth Apps](https://docs.github.com/en/apps/oa #### Step 2.1 — Create a new OAuth App for local development 1. Navigate to [github.com/settings/developers](https://github.com/settings/developers) to create a new OAuth App with the following settings: - - - **Application name**: `My MCP Server (local)` - - **Homepage URL**: `http://localhost:8788` - - **Authorization callback URL**: `http://localhost:8788/callback` + - **Application name**: `My MCP Server (local)` + - **Homepage URL**: `http://localhost:8788` + - **Authorization callback URL**: `http://localhost:8788/callback` 2. For the OAuth app you just created, add the client ID of the OAuth app as `GITHUB_CLIENT_ID` and generate a client secret, adding it as `GITHUB_CLIENT_SECRET` to a `.env` file in the root of your project, which [will be used to set secrets in local development](/workers/configuration/secrets/). - ```sh - touch .env - echo 'GITHUB_CLIENT_ID="your-client-id"' >> .env - echo 'GITHUB_CLIENT_SECRET="your-client-secret"' >> .env - cat .env - ``` + ```sh + touch .env + echo 'GITHUB_CLIENT_ID="your-client-id"' >> .env + echo 'GITHUB_CLIENT_SECRET="your-client-secret"' >> .env + cat .env + ``` 3. Run the following command to start the development server: - ```sh - npm start - ``` + ```sh + npm start + ``` - Your MCP server is now running on `http://localhost:8788/mcp`. + Your MCP server is now running on `http://localhost:8788/mcp`. 4. In a new terminal, run the [MCP inspector](https://github.com/modelcontextprotocol/inspector). The MCP inspector is an interactive MCP client that allows you to connect to your MCP server and invoke tools from a web browser. - ```sh - npx @modelcontextprotocol/inspector@latest - ``` + ```sh + npx @modelcontextprotocol/inspector@latest + ``` 5. Open the MCP inspector in your web browser: - ```sh - open http://localhost:5173 - ``` + ```sh + open http://localhost:5173 + ``` 6. In the inspector, enter the URL of your MCP server, `http://localhost:8788/mcp` 7. In the main panel on the right, click the **OAuth Settings** button and then click **Quick OAuth Flow**. - You should be redirected to a GitHub login or authorization page. After authorizing the MCP Client (the inspector) access to your GitHub account, you will be redirected back to the inspector. + You should be redirected to a GitHub login or authorization page. After authorizing the MCP Client (the inspector) access to your GitHub account, you will be redirected back to the inspector. 8. Click **Connect** in the sidebar and you should see the "List Tools" button, which will list the tools that your MCP server exposes. @@ -270,42 +268,42 @@ npx wrangler secret put GITHUB_CLIENT_ID npx wrangler secret put GITHUB_CLIENT_SECRET ``` - ``` - npx wrangler secret put COOKIE_ENCRYPTION_KEY # add any random string here e.g. openssl rand -hex 32 - ``` + ``` + npx wrangler secret put COOKIE_ENCRYPTION_KEY # add any random string here e.g. openssl rand -hex 32 + ``` - :::caution - When you create the first secret, Wrangler will ask if you want to create a new Worker. Submit "Y" to create a new Worker and save the secret. - ::: + :::caution + When you create the first secret, Wrangler will ask if you want to create a new Worker. Submit "Y" to create a new Worker and save the secret. + ::: -3. Set up a KV namespace +3. Set up a KV namespace - a. Create the KV namespace: + a. Create the KV namespace: - ```bash - npx wrangler kv namespace create "OAUTH_KV" - ``` + ```bash + npx wrangler kv namespace create "OAUTH_KV" + ``` - b. Update the `wrangler.jsonc` file with the resulting KV ID: + b. Update the `wrangler.jsonc` file with the resulting KV ID: - ```json - { - "kvNamespaces": [ - { - "binding": "OAUTH_KV", - "id": "" - } - ] - } - ``` + ```json + { + "kvNamespaces": [ + { + "binding": "OAUTH_KV", + "id": "" + } + ] + } + ``` -4. Deploy the MCP server to your Cloudflare `workers.dev` domain: +4. Deploy the MCP server to your Cloudflare `workers.dev` domain: - ```bash - npm run deploy - ``` + ```bash + npm run deploy + ``` -5. Connect to your server running at `worker-name.account-name.workers.dev/mcp` using the [AI Playground](https://playground.ai.cloudflare.com/), MCP inspector or ([other MCP clients](/agents/guides/remote-mcp-server/#connect-your-mcp-server-to-claude-and-other-mcp-clients)), and authenticate with GitHub. +5. Connect to your server running at `worker-name.account-name.workers.dev/mcp` using the [AI Playground](https://playground.ai.cloudflare.com/), MCP Inspector, or [other MCP clients](/agents/guides/test-remote-mcp-server/), and authenticate with GitHub. ## Next steps diff --git a/src/content/docs/agents/guides/securing-mcp-server.mdx b/src/content/docs/agents/guides/securing-mcp-server.mdx index 1dd03e81a21..2f365f57b27 100644 --- a/src/content/docs/agents/guides/securing-mcp-server.mdx +++ b/src/content/docs/agents/guides/securing-mcp-server.mdx @@ -27,7 +27,8 @@ export default new OAuthProvider({ authorizeEndpoint: "/authorize", tokenEndpoint: "/token", clientRegistrationEndpoint: "/register", - apiHandlers: { "/mcp": MyMCP.serve("/mcp") }, + apiRoute: "/mcp", + apiHandler: MyMCP.serve("/mcp"), defaultHandler: AuthHandler, }); ``` diff --git a/src/content/docs/agents/guides/test-remote-mcp-server.mdx b/src/content/docs/agents/guides/test-remote-mcp-server.mdx index 9199ece88cb..d7bb4149935 100644 --- a/src/content/docs/agents/guides/test-remote-mcp-server.mdx +++ b/src/content/docs/agents/guides/test-remote-mcp-server.mdx @@ -17,29 +17,29 @@ This guide will show you options for how to start using your remote MCP server w The [`@modelcontextprotocol/inspector` package](https://github.com/modelcontextprotocol/inspector) is a visual testing tool for MCP servers. -1. Open a terminal and run the following command: +1. Open a terminal and run the following command: - ```sh - npx @modelcontextprotocol/inspector - ``` + ```sh + npx @modelcontextprotocol/inspector + ``` - ```sh output - 🚀 MCP Inspector is up and running at: - http://localhost:5173/?MCP_PROXY_AUTH_TOKEN=46ab..cd3 + ```sh output + 🚀 MCP Inspector is up and running at: + http://localhost:5173/?MCP_PROXY_AUTH_TOKEN=46ab..cd3 - 🌐 Opening browser... - ``` + 🌐 Opening browser... + ``` - The MCP inspector will launch in your web browser. You can also launch it manually by opening a browser and going to `https://localhost:`. Check the command output for the local port where MCP Inspector is running. In this example, the MCP server is served on port `5173`. + The MCP Inspector will launch in your web browser. You can also launch it manually by opening a browser and going to `http://localhost:`. Check the command output for the local port where MCP Inspector is running. In this example, MCP Inspector is served on port `5173`. -2. In the MCP inspector, enter the URL of your MCP server (for example, `http://localhost:8788/mcp`). Select **Connect**. +2. In the MCP inspector, enter the URL of your MCP server (for example, `http://localhost:8788/mcp`). Select **Connect**. - You can connect to an MCP server running on your local machine or a remote MCP server running on Cloudflare. + You can connect to an MCP server running on your local machine or a remote MCP server running on Cloudflare. -3. If your server requires authentication, the connection will fail. To authenticate: - 1. In MCP Inspector, select **Open Auth settings**. - 2. Select **Quick OAuth Flow**. - 3. Once you have authenticated with the OAuth provider, you will be redirected back to MCP Inspector. Select **Connect**. +3. If your server requires authentication, the connection will fail. To authenticate: + 1. In MCP Inspector, select **Open Auth settings**. + 2. Select **Quick OAuth Flow**. + 3. Once you have authenticated with the OAuth provider, you will be redirected back to MCP Inspector. Select **Connect**. You should see the **List tools** button, which will list the tools that your MCP server exposes. @@ -49,7 +49,7 @@ Visit the [Workers AI Playground](https://playground.ai.cloudflare.com/), enter ## Connect your remote MCP server to Claude Desktop via a local proxy -Even though [Claude Desktop](https://claude.ai/download) doesn't yet support remote MCP clients, you can use the [`mcp-remote` local proxy](https://www.npmjs.com/package/mcp-remote) to connect it to your remote MCP server. This lets you to test what an interaction with your remote MCP server will be like with a real-world MCP client. +You can use the [`mcp-remote` local proxy](https://www.npmjs.com/package/mcp-remote) to connect Claude Desktop to your remote MCP server. This lets you test what an interaction with your remote MCP server will be like with a real-world MCP client. 1. Open Claude Desktop and navigate to Settings -> Developer -> Edit Config. This opens the configuration file that controls which MCP servers Claude can access. 2. Replace the content with a configuration like this: diff --git a/src/content/docs/agents/index.mdx b/src/content/docs/agents/index.mdx index af359caade8..b98a7fc0ac9 100644 --- a/src/content/docs/agents/index.mdx +++ b/src/content/docs/agents/index.mdx @@ -25,118 +25,118 @@ import { Tabs, TypeScriptExample, WranglerConfig, + LinkCard, } from "~/components"; -Agents are systems that use AI models to make decisions and execute tasks autonomously. They integrate with external APIs, execute custom functions, and maintain access to your data sources to handle complex workflows. +Most AI applications today are stateless — they process a request, return a response, and forget everything. Real agents need more. They need to remember conversations, act on schedules, call tools, coordinate with other agents, and stay connected to users in real-time. The Agents SDK gives you all of this as a TypeScript class. -Think of agents as digital assistants with persistent context, they remember previous interactions, maintain consistent behavior patterns, and have access to specific tools and capabilities for their designated roles. +Each agent runs on a [Durable Object](/durable-objects/) — a stateful micro-server with its own SQL database, WebSocket connections, and scheduling. Deploy once and Cloudflare runs your agents across its global network, scaling to tens of millions of instances. No infrastructure to manage, no sessions to reconstruct, no state to externalize. -Built on Cloudflare's infrastructure, agents are stateful classes that run on Durable Objects. They handle HTTP requests, WebSocket connections, task scheduling, AI model integration and more. +### Get started -### Ship your first Agent - -To use the Agent starter template and create your first Agent with the Agents SDK: - -1. Create a new project: +Three commands to a running agent. No API keys required — the starter uses [Workers AI](/workers-ai/) by default. ```sh npx create-cloudflare@latest --template cloudflare/agents-starter +cd agents-starter && npm install +npm run dev ``` -2. Install dependencies: +The starter includes streaming AI chat, server-side and client-side tools, human-in-the-loop approval, and task scheduling — a foundation you can build on or tear apart. You can also swap in [OpenAI, Anthropic, Google Gemini, or any other provider](/agents/api-reference/using-ai-models/). -```sh -npm install -``` + -3. Set up your environment: + -Create a `.env` file: +### What agents can do -```txt -OPENAI_API_KEY=your_openai_api_key -``` +- **Remember everything** — Every agent has a built-in [SQL database](/agents/api-reference/store-and-sync-state/) and key-value state that syncs to connected clients in real-time. State survives restarts, deploys, and hibernation. +- **Build AI chat** — [`AIChatAgent`](/agents/api-reference/chat-agents/) gives you streaming AI chat with automatic message persistence, resumable streams, and tool support. Pair it with the [`useAgentChat`](/agents/api-reference/chat-agents/) React hook to build chat UIs in minutes. +- **Think with any model** — Call [any AI model](/agents/api-reference/using-ai-models/) — Workers AI, OpenAI, Anthropic, Gemini — and stream responses over [WebSockets](/agents/api-reference/websockets/) or [Server-Sent Events](/agents/api-reference/http-sse/). Long-running reasoning models that take minutes to respond work out of the box. +- **Use and serve tools** — Define server-side tools, client-side tools that run in the browser, and [human-in-the-loop](/agents/concepts/human-in-the-loop/) approval flows. Expose your agent's tools to other agents and LLMs via [MCP](/agents/api-reference/mcp-agent-api/). +- **Act on their own** — [Schedule tasks](/agents/api-reference/schedule-tasks/) on a delay, at a specific time, or on a cron. Agents can wake themselves up, do work, and go back to sleep — without a user present. +- **Browse the web** — Spin up [headless browsers](/agents/api-reference/browse-the-web/) to scrape, screenshot, and interact with web pages. +- **Orchestrate work** — Run multi-step [workflows](/agents/api-reference/run-workflows/) with automatic retries, or coordinate across multiple agents. +- **React to events** — Handle [inbound email](/agents/api-reference/email/), HTTP requests, WebSocket messages, and state changes — all from the same class. -4. Run locally: +### How it works -```sh -npm start -``` +An agent is a TypeScript class. Methods marked with `@callable()` become typed RPC that clients can call directly over WebSocket. + + + +```ts +import { Agent, callable } from "agents"; -5. Deploy: +export class CounterAgent extends Agent { + initialState = { count: 0 }; -```sh -npm run deploy + @callable() + increment() { + this.setState({ count: this.state.count + 1 }); + return this.state.count; + } +} ``` -Head to the guide on [building a chat agent](/agents/getting-started/build-a-chat-agent/) to learn how the starter project is built and how to use it as a foundation for your own agents. + + +```tsx +import { useAgent } from "agents/react"; -If you are already building on [Workers](/workers/), you can install the `agents` package directly into an existing project: +function Counter() { + const [count, setCount] = useState(0); + const agent = useAgent({ + agent: "CounterAgent", + onStateUpdate: (state) => setCount(state.count), + }); -```sh -npm i agents + return ; +} ``` -And then define your first Agent by creating a class that extends the `Agent` class: +For AI chat, extend `AIChatAgent` instead. Messages are persisted automatically, streams resume on disconnect, and the React hook handles the UI. ```ts -import { Agent } from "agents"; - -export class MyAgent extends Agent { - // Define methods on the Agent: - // https://developers.cloudflare.com/agents/api-reference/agents-api/ - // - // Every Agent has built in state via this.setState and this.sql - // Built-in scheduling via this.schedule - // Agents support WebSockets, HTTP requests, state synchronization and - // can run for seconds, minutes or hours: as long as the tasks need. +import { AIChatAgent } from "@cloudflare/ai-chat"; +import { createWorkersAI } from "workers-ai-provider"; +import { streamText, convertToModelMessages } from "ai"; + +export class ChatAgent extends AIChatAgent { + async onChatMessage() { + const workersai = createWorkersAI({ binding: this.env.AI }); + const result = streamText({ + model: workersai("@cf/zai-org/glm-4.7-flash"), + messages: await convertToModelMessages(this.messages), + }); + return result.toUIMessageStreamResponse(); + } } ``` -Lastly, add the [Durable Objects](/durable-objects/) binding to your wrangler file: - - -```jsonc -{ - "durable_objects": { - "bindings": [ - { - "name": "MyAgent", - "class_name": "MyAgent" - } - ] - }, - "migrations": [ - { - "tag": "v1", - "new_sqlite_classes": [ - "MyAgent" - ] - } - ] -} -``` - - -Dive into the [Agent SDK reference](/agents/api-reference/agents-api/) to learn more about how to use the Agents SDK package and defining an `Agent`. +Refer to the [quick start](/agents/getting-started/quick-start/) for a full walkthrough, the [chat agents guide](/agents/api-reference/chat-agents/) for the full chat API, or the [Agents API reference](/agents/api-reference/agents-api/) for the complete SDK. -### Why build agents on Cloudflare? - -We built the Agents SDK with a few things in mind: +--- -- **Batteries (state) included**: Agents come with [built-in state management](/agents/api-reference/store-and-sync-state/), with the ability to automatically sync state between an Agent and clients, trigger events on state changes, and read+write to each Agent's SQL database. -- **Communicative**: You can connect to an Agent via [WebSockets](/agents/api-reference/websockets/) and stream updates back to client in real-time. Handle a long-running response from a reasoning model, the results of an [asynchronous workflow](/agents/api-reference/run-workflows/), or build a chat app that builds on the `useAgent` hook included in the Agents SDK. -- **Extensible**: Agents are code. Use the [AI models](/agents/api-reference/using-ai-models/) you want, bring-your-own headless browser service, pull data from your database hosted in another cloud, add your own methods to your Agent and call them. +### Build on the Cloudflare Platform -Agents built with Agents SDK can be deployed directly to Cloudflare and run on top of [Durable Objects](/durable-objects/) — which you can think of as stateful micro-servers that can scale to tens of millions — and are able to run wherever they need to. Run your Agents close to a user for low-latency interactivity, close to your data for throughput, and/or anywhere in between. + ---- +Run machine learning models, powered by serverless GPUs, on Cloudflare's global network. No API keys required. -### Build on the Cloudflare Platform + @@ -152,13 +152,7 @@ Observe and control your AI applications with caching, rate limiting, request re -Build full-stack AI applications with Vectorize, Cloudflare's vector database. Adding Vectorize enables you to perform tasks such as semantic search, recommendations, anomaly detection or can be used to provide context and memory to an LLM. - - - - - -Run machine learning models, powered by serverless GPUs, on Cloudflare's global network. +Build full-stack AI applications with Vectorize, Cloudflare's vector database for semantic search, recommendations, and providing context to LLMs. diff --git a/src/content/docs/agents/model-context-protocol/authorization.mdx b/src/content/docs/agents/model-context-protocol/authorization.mdx index 96c8365d192..4bb3392c46f 100644 --- a/src/content/docs/agents/model-context-protocol/authorization.mdx +++ b/src/content/docs/agents/model-context-protocol/authorization.mdx @@ -1,19 +1,16 @@ --- title: Authorization +pcx_content_type: concept tags: - MCP sidebar: order: 3 --- -import { DirectoryListing, LinkCard } from "~/components"; +import { LinkCard } from "~/components"; When building a [Model Context Protocol (MCP)](https://modelcontextprotocol.io) server, you need both a way to allow users to login (authentication) and allow them to grant the MCP client access to resources on their account (authorization). - - - - The Model Context Protocol uses [a subset of OAuth 2.1 for authorization](https://spec.modelcontextprotocol.io/specification/draft/basic/authorization/). OAuth allows your users to grant limited access to resources, without them having to share API keys or other credentials. Cloudflare provides an [OAuth Provider Library](https://github.com/cloudflare/workers-oauth-provider) that implements the provider side of the OAuth 2.1 protocol, allowing you to easily add authorization to your MCP server. @@ -47,7 +44,7 @@ import MyAuthHandler from "./auth-handler"; export default new OAuthProvider({ apiRoute: "/mcp", // Your MCP server: - apiHandler: MyMCPServer.Router, + apiHandler: MyMCPServer.serve("/mcp"), // Replace this handler with your own handler for authentication and authorization with the third-party provider: defaultHandler: MyAuthHandler, authorizeEndpoint: "/authorize", @@ -80,11 +77,11 @@ sequenceDiagram M->>C: MCP access token ``` -Read the docs for the [Workers oAuth Provider Library](https://github.com/cloudflare/workers-oauth-provider) for more details. +Read the docs for the [Workers OAuth Provider Library](https://github.com/cloudflare/workers-oauth-provider) for more details. ### (3) Bring your own OAuth Provider -If your application already implements an OAuth Provider itself, or you use authorization-as-a-service provider, you can use this in the same way that you would use a third-party OAuth provider, described above in [(2) Third-party OAuth Provider](#3-third-party-oauth-provider). +If your application already implements an OAuth Provider itself, or you use an authorization-as-a-service provider, you can use this in the same way that you would use a third-party OAuth provider, described above in [(2) Third-party OAuth Provider](#2-third-party-oauth-provider). You can use the auth provider to: @@ -139,7 +136,7 @@ You provide your own handlers for your MCP Server's API, and authentication and export default new OAuthProvider({ apiRoute: "/mcp", // Your MCP server: - apiHandler: MyMCPServer.Router, + apiHandler: MyMCPServer.serve("/mcp"), // Your handler for authentication and authorization: defaultHandler: MyAuthHandler, authorizeEndpoint: "/authorize", @@ -174,85 +171,98 @@ sequenceDiagram Remember — [authentication is different from authorization](https://www.cloudflare.com/learning/access-management/authn-vs-authz/). Your MCP Server can handle authorization itself, while still relying on an external authentication service to first authenticate users. The [example](/agents/guides/remote-mcp-server) in getting started provides a mock authentication flow. You will need to implement your own authentication handler — either handling authentication yourself, or using an external authentication services. -## Using Authentication Context in Your MCP Server +## Using authentication context in tools + +When a user authenticates through the OAuth Provider, their identity information is available inside your tools. How you access it depends on whether you use `McpAgent` or `createMcpHandler`. + +### With McpAgent + +The third type parameter on `McpAgent` defines the shape of the authentication context. Access it via `this.props` inside `init()` and tool handlers. -When a user authenticates to your MCP server through Cloudflare's OAuth Provider, their identity information and tokens are made available through the `props` parameter. +```ts +import { McpAgent } from "agents/mcp"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; + +type AuthContext = { + claims: { sub: string; name: string; email: string }; + permissions: string[]; +}; -```js export class MyMCP extends McpAgent { - async init() { - this.server.tool("userInfo", "Get user information", {}, async () => ({ - content: [{ type: "text", text: `Hello, ${this.props.claims.name || "user"}!` }], - })); - } + server = new McpServer({ name: "Auth Demo", version: "1.0.0" }); + + async init() { + this.server.tool("whoami", "Get the current user", {}, async () => ({ + content: [{ type: "text", text: `Hello, ${this.props.claims.name}!` }], + })); + } } ``` -The authentication context can be used for: - -- Accessing user-specific data by using the user ID (this.props.claims.sub) as a key -- Checking user permissions before performing operations -- Customizing responses based on user preferences or attributes -- Using authentication tokens to make requests to external services on behalf of the user -- Ensuring consistency when users interact with your application through different interfaces (dashboard, API, MCP server) - -## Implementing Permission-Based Access for MCP Tools - -You can implement fine-grained authorization controls for your MCP tools based on user permissions. This allows you to restrict access to certain tools based on the user's role or specific permissions. - -```js -// Create a wrapper function to check permissions -function requirePermission(permission, handler) { - return async (request, context) => { - // Check if user has the required permission - const userPermissions = context.props.permissions || []; - if (!userPermissions.includes(permission)) { - return { - content: [{ type: "text", text: `Permission denied: requires ${permission}` }], - status: 403 - }; - } - - // If permission check passes, execute the handler - return handler(request, context); - }; -} +### With createMcpHandler + +Use `getMcpAuthContext()` to access the same information from within a tool handler. This uses `AsyncLocalStorage` under the hood. -// Use the wrapper with your MCP tools -async init() { - // Basic tools available to all authenticated users - this.server.tool("basicTool", "Available to all users", {}, async () => { - // Implementation for all users - }); - - // Protected tool using the permission wrapper - this.server.tool( - "adminAction", - "Administrative action requiring special permission", - { /* parameters */ }, - requirePermission("admin", async (req) => { - // Only executes if user has "admin" permission - return { - content: [{ type: "text", text: "Admin action completed" }] - }; - }) - ); - - // Conditionally register tools based on user permissions - if (this.props.permissions?.includes("special_feature")) { - this.server.tool("specialTool", "Special feature", {}, async () => { - // This tool only appears for users with the special_feature permission - }); - } +```ts +import { createMcpHandler, getMcpAuthContext } from "agents/mcp"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; + +function createServer() { + const server = new McpServer({ name: "Auth Demo", version: "1.0.0" }); + + server.tool("whoami", "Get the current user", {}, async () => { + const auth = getMcpAuthContext(); + const name = (auth?.props?.name as string) ?? "anonymous"; + return { + content: [{ type: "text", text: `Hello, ${name}!` }], + }; + }); + + return server; } ``` -Benefits: +## Permission-based tool access + +You can control which tools are available based on user permissions. There are two approaches: check permissions inside the tool handler, or conditionally register tools. + +```ts +export class MyMCP extends McpAgent { + server = new McpServer({ name: "Permissions Demo", version: "1.0.0" }); + + async init() { + this.server.tool("publicTool", "Available to all users", {}, async () => ({ + content: [{ type: "text", text: "Public result" }], + })); + + this.server.tool( + "adminAction", + "Requires admin permission", + {}, + async () => { + if (!this.props.permissions?.includes("admin")) { + return { + content: [ + { type: "text", text: "Permission denied: requires admin" }, + ], + }; + } + return { + content: [{ type: "text", text: "Admin action completed" }], + }; + }, + ); + + if (this.props.permissions?.includes("special_feature")) { + this.server.tool("specialTool", "Special feature", {}, async () => ({ + content: [{ type: "text", text: "Special feature result" }], + })); + } + } +} +``` -- Authorization check at the tool level ensures proper access control -- Allows you to define permission checks once and reuse them across tools -- Provides clear feedback to users when permission is denied -- Can choose to only present tools that the agent is able to call +Checking inside the handler returns an error message to the LLM, which can explain the denial to the user. Conditionally registering tools means the LLM never sees tools the user cannot access — it cannot attempt to call them at all. ## Next steps diff --git a/src/content/docs/agents/model-context-protocol/governance.mdx b/src/content/docs/agents/model-context-protocol/governance.mdx index 6aec40c8ccf..cdb44449c8d 100644 --- a/src/content/docs/agents/model-context-protocol/governance.mdx +++ b/src/content/docs/agents/model-context-protocol/governance.mdx @@ -1,5 +1,6 @@ --- title: MCP governance +pcx_content_type: concept tags: - MCP sidebar: @@ -8,8 +9,6 @@ sidebar: hideIndex: true --- -import { } from "~/components"; - Model Context Protocol (MCP) allows Large Language Models (LLMs) to interact with proprietary data and internal tools. However, as MCP adoption grows, organizations face security risks from "Shadow MCP", where employees run unmanaged local MCP servers against sensitive internal resources. MCP governance means that administrators have control over which MCP servers are used in the organization, who can use them, and under what conditions. ## MCP server portals @@ -28,4 +27,4 @@ Cloudflare Access logs MCP server requests and tool executions made through the To maintain a modern security posture, Cloudflare recommends the use of [remote MCP servers](/agents/guides/remote-mcp-server/) over local installations. Running MCP servers locally introduces risks similar to unmanaged [shadow IT](https://www.cloudflare.com/learning/access-management/what-is-shadow-it/), making it difficult to audit data flow or verify the integrity of the server code. Remote MCP servers give administrators visibility into what servers are being used, along with the ability to control who access them and what tools are authorized for employee use. -You can [build your remote MCP servers](/agents/guides/remote-mcp-server/) directly on Cloudflare Workers. When both your [MCP server portal](#mcp-server-portals) and remote MCP servers run on Cloudflare's network, requests stay on the same infrastructure, minimizing latency and maximizing performance. \ No newline at end of file +You can [build your remote MCP servers](/agents/guides/remote-mcp-server/) directly on Cloudflare Workers. When both your [MCP server portal](#mcp-server-portals) and remote MCP servers run on Cloudflare's network, requests stay on the same infrastructure, minimizing latency and maximizing performance. diff --git a/src/content/docs/agents/model-context-protocol/index.mdx b/src/content/docs/agents/model-context-protocol/index.mdx index 022477e4b2d..775a3993cb0 100644 --- a/src/content/docs/agents/model-context-protocol/index.mdx +++ b/src/content/docs/agents/model-context-protocol/index.mdx @@ -15,7 +15,7 @@ You can build and deploy [Model Context Protocol (MCP)](https://modelcontextprot ### MCP Terminology -- **MCP Hosts**: AI assistants (like [Claude](http://claude.ai) or [Cursor](http://cursor.com)), AI agents, or applications that need to access external capabilities. +- **MCP Hosts**: AI assistants (like [Claude](https://claude.ai) or [Cursor](https://cursor.com)), AI agents, or applications that need to access external capabilities. - **MCP Clients**: Clients embedded within the MCP hosts that connect to MCP servers and invoke tools. Each MCP client instance has a single connection to an MCP server. - **MCP Servers**: Applications that expose [tools](/agents/model-context-protocol/tools/), [prompts](https://modelcontextprotocol.io/docs/concepts/prompts), and [resources](https://modelcontextprotocol.io/docs/concepts/resources) that MCP clients can use. diff --git a/src/content/docs/agents/model-context-protocol/mcp-servers-for-cloudflare.mdx b/src/content/docs/agents/model-context-protocol/mcp-servers-for-cloudflare.mdx index c9363813823..e34b28d40d1 100644 --- a/src/content/docs/agents/model-context-protocol/mcp-servers-for-cloudflare.mdx +++ b/src/content/docs/agents/model-context-protocol/mcp-servers-for-cloudflare.mdx @@ -7,11 +7,9 @@ sidebar: order: 8 --- -import { Render } from "~/components"; +Cloudflare runs a catalog of managed remote MCP Servers which you can connect to using OAuth on clients like [Claude](https://modelcontextprotocol.io/quickstart/user), [Windsurf](https://docs.windsurf.com/windsurf/cascade/mcp), our own [AI Playground](https://playground.ai.cloudflare.com/) or any [SDK that supports MCP](/agents/api-reference/mcp-client-api/). -Cloudflare runs a catalog of managed remote MCP Servers which you can connect to using OAuth on clients like [Claude](https://modelcontextprotocol.io/quickstart/user), [Windsurf](https://docs.windsurf.com/windsurf/cascade/mcp), our own [AI Playground](https://playground.ai.cloudflare.com/) or any [SDK that supports MCP](https://github.com/cloudflare/agents/tree/main/packages/agents/src/mcp). - -These MCP servers allow your MCP Client to read configurations from your account, process information, make suggestions based on data, and even make those suggested changes for you. All of these actions can happen across Cloudflare's many services including application development, security and performance. They support both the `streamble-http` transport via `/mcp` and the `sse` transport (deprecated) via `/sse`. +These MCP servers allow your MCP Client to read configurations from your account, process information, make suggestions based on data, and even make those suggested changes for you. All of these actions can happen across Cloudflare's many services including application development, security and performance. They support both the `streamable-http` transport via `/mcp` and the `sse` transport (deprecated) via `/sse`. | Server Name | Description | Server URL | | ----------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | ---------------------------------------------- | diff --git a/src/content/docs/agents/model-context-protocol/tools.mdx b/src/content/docs/agents/model-context-protocol/tools.mdx index 7d776a54dbd..5b3eac171ec 100644 --- a/src/content/docs/agents/model-context-protocol/tools.mdx +++ b/src/content/docs/agents/model-context-protocol/tools.mdx @@ -1,35 +1,208 @@ --- pcx_content_type: concept title: Tools +tags: + - MCP sidebar: order: 2 --- -import { Render, TypeScriptExample } from "~/components"; +import { TypeScriptExample, LinkCard } from "~/components"; -Model Context Protocol (MCP) tools are functions that a [MCP Server](/agents/model-context-protocol) provides and MCP clients can call. +MCP tools are functions that an [MCP server](/agents/model-context-protocol/) exposes for clients to call. When an LLM decides it needs to take an action — look up data, run a calculation, call an API — it invokes a tool. The MCP server executes the tool and returns the result. -When you build MCP Servers with the `agents` package, you can define tools the [same way as shown in the `@modelcontextprotocol/sdk` package's examples](https://github.com/modelcontextprotocol/typescript-sdk?tab=readme-ov-file#tools). +Tools are defined using the `@modelcontextprotocol/sdk` package. The Agents SDK handles transport and lifecycle; the tool definitions are the same regardless of whether you use [`createMcpHandler`](/agents/api-reference/mcp-handler-api/) or [`McpAgent`](/agents/api-reference/mcp-agent-api/). -For example, the following code from [this example MCP server](https://github.com/cloudflare/ai/tree/main/demos/remote-mcp-server) defines a simple MCP server that adds two numbers together: +## Defining tools + +Use `server.tool()` to register a tool on an `McpServer` instance. Each tool has a name, a description (used by the LLM to decide when to call it), an input schema defined with [Zod](https://zod.dev), and a handler function. + + + +```ts +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; + +function createServer() { + const server = new McpServer({ name: "Math", version: "1.0.0" }); + + server.tool( + "add", + "Add two numbers together", + { a: z.number(), b: z.number() }, + async ({ a, b }) => ({ + content: [{ type: "text", text: String(a + b) }], + }), + ); + + return server; +} +``` + + + +The tool handler receives the validated input and must return an object with a `content` array. Each content item has a `type` (typically `"text"`) and the corresponding data. + +## Tool results + +Tool results are returned as an array of content parts. The most common type is `text`, but you can also return images and embedded resources. -```ts title="src/index.ts" -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp"; + +```ts +server.tool( + "lookup", + "Look up a user by ID", + { userId: z.string() }, + async ({ userId }) => { + const user = await db.getUser(userId); + + if (!user) { + return { + isError: true, + content: [{ type: "text", text: `User ${userId} not found` }], + }; + } + + return { + content: [{ type: "text", text: JSON.stringify(user, null, 2) }], + }; + }, +); +``` + + + +Set `isError: true` to signal that the tool call failed. The LLM receives the error message and can decide how to proceed. + +## Tool descriptions + +The `description` parameter is critical — it is what the LLM reads to decide whether and when to call your tool. Write descriptions that are: + +- **Specific** about what the tool does: "Get the current weather for a city" is better than "Weather tool" +- **Clear about inputs**: "Requires a city name as a string" helps the LLM format the call correctly +- **Honest about limitations**: "Only supports US cities" prevents the LLM from calling it with unsupported inputs + +## Input validation with Zod + +Tool inputs are defined as Zod schemas and validated automatically before the handler runs. Use Zod's `.describe()` method to give the LLM context about each parameter. + + + +```ts +server.tool( + "search", + "Search for documents by query", + { + query: z.string().describe("The search query"), + limit: z + .number() + .min(1) + .max(100) + .default(10) + .describe("Maximum number of results to return"), + category: z + .enum(["docs", "blog", "api"]) + .optional() + .describe("Filter by content category"), + }, + async ({ query, limit, category }) => { + const results = await searchIndex(query, { limit, category }); + return { + content: [{ type: "text", text: JSON.stringify(results) }], + }; + }, +); +``` + + + +## Using tools with `createMcpHandler` + +For stateless MCP servers, define tools inside a factory function and pass the server to [`createMcpHandler`](/agents/api-reference/mcp-handler-api/): + + + +```ts +import { createMcpHandler } from "agents/mcp"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; + +function createServer() { + const server = new McpServer({ name: "My Tools", version: "1.0.0" }); + + server.tool("ping", "Check if the server is alive", {}, async () => ({ + content: [{ type: "text", text: "pong" }], + })); + + return server; +} + +export default { + fetch: (request: Request, env: Env, ctx: ExecutionContext) => { + const server = createServer(); + return createMcpHandler(server)(request, env, ctx); + }, +} satisfies ExportedHandler; +``` + + + +## Using tools with `McpAgent` + +For stateful MCP servers, define tools in the `init()` method of an [`McpAgent`](/agents/api-reference/mcp-agent-api/). Tools have access to the agent instance via `this`, which means they can read and write state. + + + +```ts 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" }); + server = new McpServer({ name: "Stateful Tools", 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) }], - }), + "incrementCounter", + "Increment and return a counter", + {}, + async () => { + const count = (this.state?.count ?? 0) + 1; + this.setState({ count }); + return { + content: [{ type: "text", text: `Counter: ${count}` }], + }; + }, ); } } ``` + + +## Next steps + + + + + + + + diff --git a/src/content/docs/agents/model-context-protocol/transport.mdx b/src/content/docs/agents/model-context-protocol/transport.mdx index 3ab9571878b..badd6f0fac1 100644 --- a/src/content/docs/agents/model-context-protocol/transport.mdx +++ b/src/content/docs/agents/model-context-protocol/transport.mdx @@ -7,7 +7,7 @@ sidebar: order: 4 --- -import { Render, TabItem, Tabs, TypeScriptExample } from "~/components"; +import { TypeScriptExample } from "~/components"; The Model Context Protocol (MCP) specification defines two standard [transport mechanisms](https://spec.modelcontextprotocol.io/specification/draft/basic/transports/) for communication between clients and servers: