Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/cloudflare/agents/llms.txt

Use this file to discover all available pages before exploring further.

A complete chat application built with @cloudflare/ai-chat showcasing the recommended patterns for building AI-powered chat agents.

What it demonstrates

Server (src/server.ts):
  • toUIMessageStreamResponse() - simplest streaming pattern
  • Server-side tools with execute (weather lookup)
  • Client-side tools without execute (browser timezone)
  • Tool approval with needsApproval (calculation with amount threshold)
  • pruneMessages() for managing LLM context in long conversations
  • maxPersistedMessages for storage management
  • MCP server connections and OAuth authentication
Client (src/client.tsx):
  • useAgentChat with onToolCall for client-side tool execution
  • addToolApprovalResponse for approve/reject UI
  • body option for sending custom data with every request
  • Tool part rendering (executing, completed, approval requested)
  • Kumo design system components

Server Implementation

src/server.ts
import { createWorkersAI } from "workers-ai-provider";
import { routeAgentRequest, callable } from "agents";
import { AIChatAgent, type OnChatMessageOptions } from "@cloudflare/ai-chat";
import {
  streamText,
  convertToModelMessages,
  pruneMessages,
  tool,
  stepCountIs
} from "ai";
import { z } from "zod";

export class ChatAgent extends AIChatAgent {
  // Keep the last 200 messages in SQLite storage
  maxPersistedMessages = 200;

  // Wait for MCP connections to restore after hibernation
  waitForMcpConnections = true;

  onStart() {
    // Configure OAuth popup behavior for MCP servers
    this.mcp.configureOAuthCallback({
      customHandler: (result) => {
        if (result.authSuccess) {
          return new Response("<script>window.close();</script>", {
            headers: { "content-type": "text/html" },
            status: 200
          });
        }
        return new Response(
          `Authentication Failed: ${result.authError || "Unknown error"}`,
          { headers: { "content-type": "text/plain" }, status: 400 }
        );
      }
    });
  }

  @callable()
  async addServer(name: string, url: string, host: string) {
    return await this.addMcpServer(name, url, { callbackHost: host });
  }

  @callable()
  async removeServer(serverId: string) {
    await this.removeMcpServer(serverId);
  }

  async onChatMessage(_onFinish: unknown, options?: OnChatMessageOptions) {
    const mcpTools = this.mcp.getAITools();
    const workersai = createWorkersAI({ binding: this.env.AI });

    const result = streamText({
      abortSignal: options?.abortSignal,
      model: workersai("@cf/zai-org/glm-4.7-flash"),
      system:
        "You are a helpful assistant. You can check the weather, get the user's timezone, " +
        "and run calculations. For calculations with large numbers (over 1000), you need user approval first.",
      // Prune old tool calls and reasoning to save tokens
      messages: pruneMessages({
        messages: await convertToModelMessages(this.messages),
        toolCalls: "before-last-2-messages",
        reasoning: "before-last-message"
      }),
      tools: {
        // MCP tools from connected servers
        ...mcpTools,

        // Server-side tool: executes automatically
        getWeather: tool({
          description: "Get the current weather for a city",
          inputSchema: z.object({
            city: z.string().describe("City name")
          }),
          execute: async ({ city }) => {
            const conditions = ["sunny", "cloudy", "rainy", "snowy"];
            const temp = Math.floor(Math.random() * 30) + 5;
            return {
              city,
              temperature: temp,
              condition:
                conditions[Math.floor(Math.random() * conditions.length)],
              unit: "celsius"
            };
          }
        }),

        // Client-side tool: no execute, handled by onToolCall
        getUserTimezone: tool({
          description:
            "Get the user's timezone from their browser. Use this when you need to know the user's local time.",
          inputSchema: z.object({})
          // No execute -- the client provides the result
        }),

        // Tool with approval: requires user confirmation
        calculate: tool({
          description:
            "Perform a math calculation with two numbers. Requires approval for large numbers.",
          inputSchema: z.object({
            a: z.number().describe("First number"),
            b: z.number().describe("Second number"),
            operator: z
              .enum(["+", "-", "*", "/", "%"])
              .describe("Arithmetic operator")
          }),
          needsApproval: async ({ a, b }) =>
            Math.abs(a) > 1000 || Math.abs(b) > 1000,
          execute: async ({ a, b, operator }) => {
            const ops: Record<string, (x: number, y: number) => number> = {
              "+": (x, y) => x + y,
              "-": (x, y) => x - y,
              "*": (x, y) => x * y,
              "/": (x, y) => x / y,
              "%": (x, y) => x % y
            };
            if (operator === "/" && b === 0) {
              return { error: "Division by zero" };
            }
            return {
              expression: `${a} ${operator} ${b}`,
              result: ops[operator](a, b)
            };
          }
        })
      },
      stopWhen: stepCountIs(5)
    });

    return result.toUIMessageStreamResponse();
  }
}

export default {
  async fetch(request: Request, env: Env) {
    return (
      (await routeAgentRequest(request, env)) ||
      new Response("Not found", { status: 404 })
    );
  }
} satisfies ExportedHandler<Env>;

Key Features

Server-side Tools

Tools with an execute function run on the server:
getWeather: tool({
  description: "Get the current weather for a city",
  inputSchema: z.object({
    city: z.string().describe("City name")
  }),
  execute: async ({ city }) => {
    // This runs on the server
    const response = await fetch(`https://api.weather.com/${city}`);
    return await response.json();
  }
})

Client-side Tools

Tools without execute are handled by the client via onToolCall:
// Server: define the tool schema
getUserTimezone: tool({
  description: "Get the user's timezone",
  inputSchema: z.object({})
  // No execute
})

// Client: handle the tool call
const { messages, sendMessage } = useAgentChat({
  agent,
  onToolCall: async (toolCall) => {
    if (toolCall.toolName === "getUserTimezone") {
      return {
        result: Intl.DateTimeFormat().resolvedOptions().timeZone
      };
    }
  }
});

Tool Approval

Require user confirmation before executing sensitive tools:
calculate: tool({
  description: "Perform a math calculation",
  inputSchema: z.object({
    a: z.number(),
    b: z.number(),
    operator: z.enum(["+", "-", "*", "/"])
  }),
  // Require approval for large numbers
  needsApproval: async ({ a, b }) => Math.abs(a) > 1000 || Math.abs(b) > 1000,
  execute: async ({ a, b, operator }) => {
    // This only runs after user approves
    return { result: eval(`${a} ${operator} ${b}`) };
  }
})
In the client, handle approval UI:
{messages.map((msg) =>
  msg.parts.map((part) => {
    if (part.type === "tool-call" && part.needsApproval) {
      return (
        <div>
          <p>Approve calculation: {part.args.a} {part.args.operator} {part.args.b}?</p>
          <button onClick={() => addToolApprovalResponse(part, true)}>
            Approve
          </button>
          <button onClick={() => addToolApprovalResponse(part, false)}>
            Reject
          </button>
        </div>
      );
    }
  })
)}

Message Pruning

Manage LLM context in long conversations:
messages: pruneMessages({
  messages: await convertToModelMessages(this.messages),
  toolCalls: "before-last-2-messages",  // Keep only recent tool calls
  reasoning: "before-last-message"       // Keep only last reasoning
})

Storage Management

Limit messages stored in SQLite:
export class ChatAgent extends AIChatAgent {
  // Keep only the last 200 messages in storage
  maxPersistedMessages = 200;
}

Running the Example

1

Install dependencies

npm install
2

Start development server

npm run dev
3

Try it out

Visit http://localhost:5173 and try these prompts:
  • “What’s the weather in London?” (server-side tool)
  • “What timezone am I in?” (client-side tool)
  • “Calculate 150 * 3, amount is $450” (requires approval)
This example uses Workers AI (no API key needed) with the @cf/zai-org/glm-4.7-flash model.

Dynamic Tools

Client-defined tools for SDK/platform pattern

Codemode

LLMs write executable code instead of tool calls

MCP Client

Connect to MCP servers as a client

AI Chat Guide

In-depth guide to building AI chat agents

Build docs developers (and LLMs) love