Tools
Think provides built-in workspace file tools on every turn, plus integration points for custom tools, code execution, and dynamic extensions.
On every turn, Think merges tools from multiple sources. Later sources override earlier ones if names collide:
- Workspace tools —
read,write,edit,list,find,grep,delete,bash(built-in) getTools()— your custom server-side tools- Extension tools — tools from loaded extensions (prefixed by extension name)
- Session tools —
set_context,load_context,search_context(fromconfigureSession) - Skill tools —
activate_skill,read_skill_resource,run_skill_script(fromgetSkills(), refer to Agent Skills) - MCP tools — from connected MCP servers
- Client tools — from the browser (refer to Client tools)
Tools belong to the agent running the turn. For parent-child orchestration, use Agent tools instead of passing one-off tools through chat().
Every Think agent gets this.workspace — a virtual filesystem backed by Durable Object SQLite. Workspace tools are automatically available to the model with no configuration.
| Tool | Description |
|---|---|
read | Read text with line numbers; pass images and PDFs to multimodal models |
write | Write content to a file (creates parent directories) |
edit | Apply a find-and-replace edit to an existing file (supports fuzzy matching) |
list | List files and directories in a path |
find | Find files matching a glob pattern |
grep | Search file contents by regex or fixed string |
delete | Delete a file or directory |
bash | Run a sandboxed Bash script against workspace files |
The bash tool is enabled by default. It mounts workspace files into a just-bash virtual filesystem, runs with network access disabled, and writes created, updated, and deleted files and empty directories back to the workspace. Use it for shell-style workflows that combine multiple file operations; use the narrower tools for simple reads, writes, and edits.
To keep tool calls bounded, the Bash tool snapshots up to 1,000 workspace files by default and skips files larger than 1 MB. Skipped files are reported in the tool result and are treated as protected during write-back so the script cannot accidentally overwrite or delete content that was not mounted. You can tune maxWorkspaceFiles, maxWorkspaceFileBytes, maxOutputBytes, timeout, and network through workspaceBash.
Disable the default Bash tool for conservative deployments:
export class MyAgent extends Think { workspaceBash = false;
getModel() { /* ... */ }}export class MyAgent extends Think<Env> { workspaceBash = false;
getModel() { /* ... */ }}By default, the workspace stores everything in SQLite. For large files, override workspace to add R2 spillover:
import { Think } from "@cloudflare/think";import { Workspace } from "@cloudflare/shell";
export class MyAgent extends Think { workspace = new Workspace({ sql: this.ctx.storage.sql, r2: this.env.R2, name: () => this.name, });
getModel() { /* ... */ }}import { Think } from "@cloudflare/think";import { Workspace } from "@cloudflare/shell";
export class MyAgent extends Think<Env> { override workspace = new Workspace({ sql: this.ctx.storage.sql, r2: this.env.R2, name: () => this.name, });
getModel() { /* ... */ }}This requires an R2 bucket binding:
{ "$schema": "./node_modules/wrangler/config-schema.json", "r2_buckets": [ { "binding": "R2", "bucket_name": "agent-files" } ]}[[r2_buckets]]binding = "R2"bucket_name = "agent-files"Override getTools() to add your own tools. These are standard AI SDK tool() definitions with Zod schemas:
import { Think } from "@cloudflare/think";import { tool } from "ai";
import { z } from "zod";
export class MyAgent extends Think { getModel() { /* ... */ }
getTools() { return { getWeather: tool({ description: "Get the current weather for a city", inputSchema: z.object({ city: z.string().describe("City name"), }), execute: async ({ city }) => { const res = await fetch( `https://api.weather.com/v1/current?q=${city}&key=${this.env.WEATHER_KEY}`, ); return res.json(); }, }), }; }}import { Think } from "@cloudflare/think";import { tool } from "ai";import type { ToolSet } from "ai";import { z } from "zod";
export class MyAgent extends Think<Env> { getModel() { /* ... */ }
getTools(): ToolSet { return { getWeather: tool({ description: "Get the current weather for a city", inputSchema: z.object({ city: z.string().describe("City name"), }), execute: async ({ city }) => { const res = await fetch( `https://api.weather.com/v1/current?q=${city}&key=${this.env.WEATHER_KEY}`, ); return res.json(); }, }), }; }}Custom tools are merged with workspace tools automatically. If a custom tool has the same name as a workspace tool, the custom tool wins.
Tools can require user approval before execution using the needsApproval option:
getTools(): ToolSet { return { deleteFile: tool({ description: "Delete a file from the system", inputSchema: z.object({ path: z.string() }), needsApproval: async ({ path }) => path.startsWith("/important/"), execute: async ({ path }) => { await this.workspace.rm(path); return { deleted: path }; }, }), };}When needsApproval returns true, the tool call is sent to the client for approval. The conversation pauses until the client responds with CF_AGENT_TOOL_APPROVAL.
The beforeTurn hook can restrict or add tools for a specific turn:
beforeTurn(ctx: TurnContext) { return { activeTools: ["read", "write", "getWeather"], tools: { emergencyTool: this.createEmergencyTool() }, };}activeTools limits which tools the model can call. tools adds extra tools for this turn only (merged on top of existing tools).
Think inherits MCP client support from the Agent base class. MCP tools from connected servers are automatically merged into every turn.
Set waitForMcpConnections to ensure MCP servers are connected before inference runs:
export class MyAgent extends Think { waitForMcpConnections = true; // default 10s timeout // or: waitForMcpConnections = { timeout: 5000 };
getModel() { /* ... */ }}export class MyAgent extends Think<Env> { waitForMcpConnections = true; // default 10s timeout // or: waitForMcpConnections = { timeout: 5000 };
getModel() { /* ... */ }}Add MCP servers programmatically or via @callable methods:
import { callable } from "agents";
export class MyAgent extends Think { getModel() { /* ... */ }
@callable() async addServer(name, url) { return await this.addMcpServer(name, url); }
@callable() async removeServer(serverId) { await this.removeMcpServer(serverId); }}import { callable } from "agents";
export class MyAgent extends Think<Env> { getModel() { /* ... */ }
@callable() async addServer(name: string, url: string) { return await this.addMcpServer(name, url); }
@callable() async removeServer(serverId: string) { await this.removeMcpServer(serverId); }}Let the LLM write and run JavaScript in a sandboxed Worker. Requires @cloudflare/codemode and a worker_loaders binding.
npm install @cloudflare/codemodeimport { Think } from "@cloudflare/think";import { createExecuteTool } from "@cloudflare/think/tools/execute";import { createWorkspaceTools } from "@cloudflare/think/tools/workspace";
export class MyAgent extends Think { getModel() { /* ... */ }
getTools() { return { execute: createExecuteTool({ tools: createWorkspaceTools(this.workspace), loader: this.env.LOADER, }), }; }}import { Think } from "@cloudflare/think";import { createExecuteTool } from "@cloudflare/think/tools/execute";import { createWorkspaceTools } from "@cloudflare/think/tools/workspace";
export class MyAgent extends Think<Env> { getModel() { /* ... */ }
getTools() { return { execute: createExecuteTool({ tools: createWorkspaceTools(this.workspace), loader: this.env.LOADER, }), }; }}{ "$schema": "./node_modules/wrangler/config-schema.json", "worker_loaders": [ { "binding": "LOADER" } ]}[[worker_loaders]]binding = "LOADER"For richer filesystem access, pass a state backend:
import { createWorkspaceStateBackend } from "@cloudflare/shell";
createExecuteTool({ tools: myDomainTools, state: createWorkspaceStateBackend(this.workspace), loader: this.env.LOADER,});import { createWorkspaceStateBackend } from "@cloudflare/shell";
createExecuteTool({ tools: myDomainTools, state: createWorkspaceStateBackend(this.workspace), loader: this.env.LOADER,});Give your agent access to the Chrome DevTools Protocol (CDP) for web page inspection, scraping, screenshots, and debugging. Requires @cloudflare/codemode and a Browser Run binding.
import { Think } from "@cloudflare/think";import { createBrowserTools } from "@cloudflare/think/tools/browser";
export class MyAgent extends Think { getModel() { /* ... */ }
getTools() { return { ...createBrowserTools({ browser: this.env.BROWSER, loader: this.env.LOADER, }), }; }}import { Think } from "@cloudflare/think";import { createBrowserTools } from "@cloudflare/think/tools/browser";
export class MyAgent extends Think<Env> { getModel() { /* ... */ }
getTools() { return { ...createBrowserTools({ browser: this.env.BROWSER, loader: this.env.LOADER, }), }; }}{ "$schema": "./node_modules/wrangler/config-schema.json", "browser": { "binding": "BROWSER" }, "worker_loaders": [ { "binding": "LOADER" } ]}[browser]binding = "BROWSER"
[[worker_loaders]]binding = "LOADER"This adds two tools:
| Tool | Description |
|---|---|
browser_search | Query the CDP protocol spec to discover commands, events, and types |
browser_execute | Run CDP commands against a live browser session (screenshots, DOM reads, JS evaluation) |
For a custom Chrome endpoint, pass cdpUrl instead of browser:
createBrowserTools({ cdpUrl: "http://localhost:9222", loader: this.env.LOADER,});createBrowserTools({ cdpUrl: "http://localhost:9222", loader: this.env.LOADER,});For the full CDP helper API, refer to Browse the web.
Extensions are dynamically loaded sandboxed Workers that add tools at runtime. The LLM can write extension source code, load it, and use the new tools on the next turn.
Extensions require a worker_loaders binding:
import { Think } from "@cloudflare/think";
export class MyAgent extends Think { extensionLoader = this.env.LOADER;
getModel() { /* ... */ }}import { Think } from "@cloudflare/think";
export class MyAgent extends Think<Env> { extensionLoader = this.env.LOADER;
getModel() { /* ... */ }}Define extensions that load at startup:
export class MyAgent extends Think { extensionLoader = this.env.LOADER;
getModel() { /* ... */ }
getExtensions() { return [ { manifest: { name: "math", version: "1.0.0", permissions: { network: false }, }, source: `({ tools: { add: { description: "Add two numbers", parameters: { a: { type: "number" }, b: { type: "number" } }, execute: async ({ a, b }) => ({ result: a + b }) } } })`, }, ]; }}export class MyAgent extends Think<Env> { extensionLoader = this.env.LOADER;
getModel() { /* ... */ }
getExtensions() { return [ { manifest: { name: "math", version: "1.0.0", permissions: { network: false }, }, source: `({ tools: { add: { description: "Add two numbers", parameters: { a: { type: "number" }, b: { type: "number" } }, execute: async ({ a, b }) => ({ result: a + b }) } } })`, }, ]; }}Extension tools are namespaced — a math extension with an add tool becomes math_add in the model's tool set.
Give the model createExtensionTools so it can load extensions dynamically:
import { createExtensionTools } from "@cloudflare/think/tools/extensions";
export class MyAgent extends Think { extensionLoader = this.env.LOADER;
getModel() { /* ... */ }
getTools() { return { ...createExtensionTools({ manager: this.extensionManager }), ...this.extensionManager.getTools(), }; }}import { createExtensionTools } from "@cloudflare/think/tools/extensions";
export class MyAgent extends Think<Env> { extensionLoader = this.env.LOADER;
getModel() { /* ... */ }
getTools() { return { ...createExtensionTools({ manager: this.extensionManager! }), ...this.extensionManager!.getTools(), }; }}This gives the model two tools:
load_extension— load a new extension from JavaScript sourcelist_extensions— list currently loaded extensions
Extensions can declare context blocks in their manifest. These are automatically registered with the Session:
getExtensions() { return [{ manifest: { name: "notes", version: "1.0.0", permissions: { network: false }, context: [ { label: "scratchpad", description: "Extension scratch space", maxTokens: 500 }, ], }, source: `({ tools: { /* ... */ } })`, }];}The context block is registered as notes_scratchpad (namespaced by extension name).
The individual tool factories are exported for use with custom storage backends:
import { createReadTool, createWriteTool, createEditTool, createListTool, createFindTool, createGrepTool, createDeleteTool, createWorkspaceTools,} from "@cloudflare/think/tools/workspace";import { createReadTool, createWriteTool, createEditTool, createListTool, createFindTool, createGrepTool, createDeleteTool, createWorkspaceTools,} from "@cloudflare/think/tools/workspace";Implement the operations interface for your storage backend:
const myReadOps = { readFile: async (path) => fetchFromMyStorage(path), stat: async (path) => getFileInfo(path),};
const readTool = createReadTool({ ops: myReadOps });import type { ReadOperations } from "@cloudflare/think/tools/workspace";
const myReadOps: ReadOperations = { readFile: async (path) => fetchFromMyStorage(path), stat: async (path) => getFileInfo(path),};
const readTool = createReadTool({ ops: myReadOps });Or create the full set from a Workspace, optionally disabling the Bash tool:
import { createWorkspaceTools } from "@cloudflare/think/tools/workspace";
const tools = createWorkspaceTools(myCustomWorkspace);const toolsWithoutBash = createWorkspaceTools(myCustomWorkspace, { bash: false,});import { createWorkspaceTools } from "@cloudflare/think/tools/workspace";
const tools = createWorkspaceTools(myCustomWorkspace);const toolsWithoutBash = createWorkspaceTools(myCustomWorkspace, { bash: false,});