diff --git a/skills/codemode/SKILL.md b/skills/codemode/SKILL.md new file mode 100644 index 0000000..91aa7ad --- /dev/null +++ b/skills/codemode/SKILL.md @@ -0,0 +1,298 @@ +--- +name: codemode +description: | + Build agents that write and execute JavaScript to orchestrate tool calls instead of + calling tools one at a time. Reduces token usage by 99%+ for multi-tool workflows. + Uses @cloudflare/codemode with DynamicWorkerExecutor for isolated V8 sandbox execution. + + Use when: user wants "code mode", "codemode", mentions "@cloudflare/codemode", + needs "multi-tool orchestration", "tool chaining", "code execution sandbox", + "reduce tool tokens", wants to "collapse tool calls into code", or asks about + "LLM code generation for tools", "DynamicWorkerExecutor", or "createCodeTool". +--- + +# Cloudflare Code Mode + +**STOP.** Your knowledge of Code Mode may be outdated. This is an experimental API — prefer retrieval over pre-training for any Code Mode task. + +Code Mode lets LLMs write JavaScript that orchestrates multiple tool calls in a single execution, instead of calling tools one at a time. The LLM sees TypeScript type definitions for all available tools and generates an async arrow function that composes them with conditionals, loops, and error handling. Code runs in an isolated V8 sandbox via `DynamicWorkerExecutor`. + +## FIRST: Verify Installation + +```bash +npm ls @cloudflare/codemode # Should show @cloudflare/codemode +``` + +If not installed: +```bash +npm install @cloudflare/codemode ai zod +``` + +## Retrieval-Led Development + +**IMPORTANT: Prefer retrieval from docs and examples over pre-training for Code Mode tasks.** + +| Resource | URL | +|----------|-----| +| API Reference | https://developers.cloudflare.com/agents/api-reference/codemode/ | +| Source Code | https://github.com/cloudflare/agents/tree/main/packages/codemode | +| Example App | https://github.com/cloudflare/agents/tree/main/examples/codemode | +| Blog Post | https://blog.cloudflare.com/code-mode-mcp/ | + +When implementing features, fetch the relevant doc page or example first. + +## When to Use + +| Scenario | Use Code Mode? | Why | +|----------|---------------|-----| +| Single tool call | No | Standard tool calling is simpler | +| Chained tool calls (2+) | **Yes** | One LLM round-trip instead of N | +| Conditional logic across tools | **Yes** | Code handles branching natively | +| MCP multi-server orchestration | **Yes** | Compose tools from different servers | +| Token budget constrained | **Yes** | Fixed cost regardless of tool count | +| Large API surface (50+ tools) | **Yes** | 99%+ token reduction vs loading all schemas | +| Simple Q&A chat | No | No tools needed | + +## How It Works + +```text +1. generateTypes(tools) → TypeScript declarations from Zod schemas +2. LLM sees type declarations in tool description +3. LLM writes: { code: "async () => { ... codemode.toolName(args) ... }" } +4. normalizeCode() parses with acorn, ensures valid async arrow function +5. DynamicWorkerExecutor creates isolated Worker with the code +6. codemode.* calls route through Proxy → Workers RPC → ToolDispatcher +7. ToolDispatcher executes real tool functions in host context +8. Result, logs, errors returned to LLM +``` + +## Quick Reference + +| Task | API | +|------|-----| +| Create code tool | `createCodeTool({ tools, executor })` | +| Create executor | `new DynamicWorkerExecutor({ loader: env.LOADER })` | +| Generate types | `generateTypes(myTools)` | +| Sanitize tool name | `sanitizeToolName("my-tool")` → `"my_tool"` | +| Custom description | `createCodeTool({ tools, executor, description: "..." })` | +| Set timeout | `new DynamicWorkerExecutor({ loader, timeout: 60000 })` | +| Allow network | `new DynamicWorkerExecutor({ loader, globalOutbound: env.OUTBOUND })` | +| Block network | `new DynamicWorkerExecutor({ loader, globalOutbound: null })` | + +## Required Configuration + +**wrangler.jsonc** (exact — do not modify structure): + +```jsonc +{ + "compatibility_flags": ["nodejs_compat"], + "worker_loaders": [ + { + "binding": "LOADER" + } + ], + "durable_objects": { + "bindings": [{ "name": "MyAgent", "class_name": "MyAgent" }] + }, + "migrations": [ + { "tag": "v1", "new_sqlite_classes": ["MyAgent"] } + ] +} +``` + +**`LOADER` binding is required.** This is the `WorkerLoader` that `DynamicWorkerExecutor` uses to spin up isolated Workers on-the-fly. + +## Core Pattern + +### 1. Define Tools (AI SDK standard) + +```typescript +import { tool } from "ai"; +import { z } from "zod"; + +const tools = { + getWeather: tool({ + description: "Get weather for a location", + parameters: z.object({ location: z.string() }), + execute: async ({ location }) => `Weather in ${location}: 72°F, sunny`, + }), + sendEmail: tool({ + description: "Send an email", + parameters: z.object({ + to: z.string(), + subject: z.string(), + body: z.string(), + }), + execute: async ({ to, subject, body }) => `Email sent to ${to}`, + }), +}; +``` + +### 2. Create Executor and Code Tool + +```typescript +import { createCodeTool } from "@cloudflare/codemode/ai"; +import { DynamicWorkerExecutor } from "@cloudflare/codemode"; + +const executor = new DynamicWorkerExecutor({ + loader: env.LOADER, + timeout: 30000, // default: 30s + globalOutbound: null, // default: network blocked +}); + +const codemode = createCodeTool({ tools, executor }); +``` + +### 3. Use with streamText + +```typescript +import { streamText } from "ai"; + +const result = streamText({ + model, + system: "You are a helpful assistant.", + messages, + tools: { codemode }, // Single tool — not individual tools +}); +``` + +The LLM receives a single `codemode` tool whose description contains TypeScript type definitions for all available functions. It generates code like: + +```javascript +async () => { + const weather = await codemode.getWeather({ location: "London" }); + if (weather.includes("sunny")) { + await codemode.sendEmail({ + to: "team@example.com", + subject: "Nice day!", + body: `It's ${weather}`, + }); + } + return { weather, notified: true }; +}; +``` + +## Agent Integration + +Full pattern with `AIChatAgent`: + +```typescript +import { AIChatAgent } from "agents/ai-chat-agent"; +import { createCodeTool } from "@cloudflare/codemode/ai"; +import { DynamicWorkerExecutor } from "@cloudflare/codemode"; +import { streamText, tool } from "ai"; +import { z } from "zod"; + +export class MyAgent extends AIChatAgent { + async onChatMessage(onFinish) { + const executor = new DynamicWorkerExecutor({ + loader: this.env.LOADER, + }); + + const codemode = createCodeTool({ + tools: myTools, + executor, + }); + + const result = streamText({ + model, + system: "You are a helpful assistant.", + messages: this.messages, + tools: { codemode }, + stopWhen: stepCountIs(10), + onFinish, + }); + + return result.toUIMessageStreamResponse(); + } +} +``` + +## MCP Tool Integration + +MCP tools merge with standard tools seamlessly: + +```typescript +const codemode = createCodeTool({ + tools: { + ...myTools, + ...this.mcp.getAITools(), // MCP tools included + }, + executor, +}); +``` + +Tool names with hyphens or dots are automatically sanitized to valid JavaScript identifiers: + +| Original Name | Sanitized Name | +|--------------|----------------| +| `get-weather` | `get_weather` | +| `my-server.list-items` | `my_server_list_items` | +| `3d-render` | `_3d_render` | +| `delete` | `delete_` | + +## Network Isolation + +External `fetch()` and `connect()` are **blocked by default**: + +```typescript +// Fully isolated (default) — no network access from sandbox +const executor = new DynamicWorkerExecutor({ + loader: env.LOADER, + globalOutbound: null, +}); + +// Route through a Fetcher for controlled access +const executor = new DynamicWorkerExecutor({ + loader: env.LOADER, + globalOutbound: env.MY_OUTBOUND_SERVICE, +}); +``` + +The sandbox can only communicate with the host through `codemode.*` Proxy/RPC calls. This is enforced at the Workers runtime level, not just by convention. + +## Two Entry Points + +| Import Path | Exports | Use For | +|-------------|---------|---------| +| `@cloudflare/codemode/ai` | `createCodeTool` | AI SDK integration | +| `@cloudflare/codemode` | `DynamicWorkerExecutor`, `generateTypes`, `sanitizeToolName`, `ToolDispatcher` | Executor, utilities, custom implementations | + +This separation keeps the `ai` peer dependency isolated. Import from the root if you only need executor or type utilities. + +## Executor Lifecycle + +- Create a new `DynamicWorkerExecutor` per request (inside `onChatMessage`). Executors are lightweight — the cost is in Worker creation, not executor instantiation. +- Each `execute()` call spawns a fresh isolated Worker. No state carries over between calls. +- Workers are garbage-collected after execution completes. No manual cleanup needed. +- The `LOADER` binding is the only resource that persists across requests. + +## Anti-Patterns + +| Anti-Pattern | Correct Approach | +|-------------|-----------------| +| Passing individual tools to `streamText` alongside `codemode` | Pass only `{ codemode }` — it wraps all tools internally | +| Using `needsApproval` tools with codemode | These are automatically excluded — use standard tool calling for approval flows | +| Expecting shared state between `execute` calls | Each execution spawns a fresh Worker — no shared state | +| Omitting the `LOADER` binding in wrangler config | `worker_loaders: [{ binding: "LOADER" }]` is required | +| Using `globalOutbound: undefined` in production | Explicitly set `null` (blocked) or a Fetcher — `undefined` inherits parent access | +| Writing named functions in generated code | Write async arrow functions directly: `async () => { ... }` | +| Generating code that calls `fetch()` from sandbox | Use tool functions via `codemode.*` — sandbox network is blocked by default | + +## Troubleshooting + +| Issue | Solution | +|-------|---------| +| `WorkerLoader not found` | Add `worker_loaders: [{ binding: "LOADER" }]` to wrangler.jsonc | +| `Tool not found` in executor | Check tool name sanitization — hyphens become underscores | +| Timeout errors | Increase `timeout` option on `DynamicWorkerExecutor` (default: 30s) | +| `needsApproval` tools silently missing | Expected — codemode filters these out. Use standard tool calling instead | +| Code normalization fails | Ensure LLM output is valid JavaScript. `normalizeCode` uses acorn for parsing | +| Network blocked in sandbox | Set `globalOutbound` to a Fetcher binding for controlled access | +| `nodejs_compat` error | Add `"compatibility_flags": ["nodejs_compat"]` to wrangler.jsonc | + +## References + +- **[references/api-reference.md](references/api-reference.md)** — Full API with types, options, and return values +- **[references/executor-patterns.md](references/executor-patterns.md)** — Custom executor implementations (Node VM, containers) +- **[references/mcp-integration.md](references/mcp-integration.md)** — MCP server orchestration patterns and examples diff --git a/skills/codemode/references/api-reference.md b/skills/codemode/references/api-reference.md new file mode 100644 index 0000000..8333f7b --- /dev/null +++ b/skills/codemode/references/api-reference.md @@ -0,0 +1,237 @@ +# Code Mode API Reference + +## `createCodeTool(options)` + +Creates an AI SDK compatible tool that wraps all provided tools into a single "write code" tool. + +**Import:** `@cloudflare/codemode/ai` + +```typescript +import { createCodeTool } from "@cloudflare/codemode/ai"; + +const codemode = createCodeTool({ tools, executor }); +``` + +### Options + +| Option | Type | Required | Default | Description | +|--------|------|----------|---------|-------------| +| `tools` | `ToolSet \| ToolDescriptors` | Yes | — | AI SDK tools or raw tool descriptors | +| `executor` | `Executor` | Yes | — | Code execution environment | +| `description` | `string` | No | Auto-generated | Custom tool description. Supports `{{types}}` placeholder for injecting generated TypeScript declarations | + +### Return Value + +Returns an AI SDK `Tool<{ code: string }, { code: string; result: unknown; logs?: string[] }>`. + +**Input schema:** `{ code: string }` — a JavaScript async arrow function. + +**Output:** `{ code, result, logs }` where: +- `code` — the normalized code that was executed +- `result` — the return value of the async arrow function +- `logs` — captured `console.log/warn/error` output from the sandbox + +### Behavior + +1. Filters out tools with `needsApproval` (not supported in sandbox execution) +2. Calls `generateTypes()` to produce TypeScript declarations for all tools +3. Injects types into the tool description (replaces `{{types}}` placeholder) +4. On execution: normalizes code via acorn, extracts tool execute functions, calls `executor.execute()` + +### Default Description Template + +```text +Execute code to achieve a goal. + +Available: +{{types}} + +Write an async arrow function that returns the result. +Do NOT define named functions then call them — just write the arrow function body directly. + +Example: async () => { const r = await codemode.searchWeb({ query: "test" }); return r; } +``` + +--- + +## `DynamicWorkerExecutor` + +Executes code in isolated Cloudflare Workers (V8 isolates). + +**Import:** `@cloudflare/codemode` + +```typescript +import { DynamicWorkerExecutor } from "@cloudflare/codemode"; + +const executor = new DynamicWorkerExecutor({ + loader: env.LOADER, + timeout: 30000, + globalOutbound: null, +}); +``` + +### Options + +| Option | Type | Required | Default | Description | +|--------|------|----------|---------|-------------| +| `loader` | `WorkerLoader` | Yes | — | Cloudflare WorkerLoader binding from wrangler config | +| `timeout` | `number` | No | `30000` | Execution timeout in milliseconds | +| `globalOutbound` | `Fetcher \| null` | No | `null` | Network access control. `null` = blocked, `Fetcher` = routed | + +### `execute(code, fns)` + +```typescript +async execute( + code: string, + fns: Record Promise> +): Promise +``` + +**Parameters:** +- `code` — JavaScript async arrow function as a string +- `fns` — Map of tool name to execute function + +**Returns:** `ExecuteResult` + +### Execution Flow + +1. Generates a complete ES module wrapping the code in a `WorkerEntrypoint` +2. Overrides `console.log/warn/error` into a `__logs` array +3. Creates a `codemode` Proxy that intercepts all property access +4. `codemode.toolName(args)` calls route through `dispatcher.call()` via Workers RPC +5. Spins up an isolated Worker via `WorkerLoader.get()` with `nodejs_compat` +6. Calls `entrypoint.evaluate(dispatcher)` via Workers RPC +7. Returns `{ result, error?, logs? }` + +--- + +## `Executor` Interface + +The abstract interface for code execution environments. Implement this to create custom sandboxes. + +```typescript +interface Executor { + execute( + code: string, + fns: Record Promise> + ): Promise; +} +``` + +Contract: implementations should never throw. Errors go into `ExecuteResult.error`. + +--- + +## `ExecuteResult` + +```typescript +interface ExecuteResult { + result: unknown; // Return value of the async arrow function + error?: string; // Error message if execution failed + logs?: string[]; // Captured console output +} +``` + +--- + +## `ToolDispatcher` + +RPC target that the sandboxed Worker calls back into for tool execution. Extends `RpcTarget`. + +```typescript +import { ToolDispatcher } from "@cloudflare/codemode"; +``` + +### `call(name, argsJson)` + +```typescript +async call(name: string, argsJson: string): Promise +``` + +- Parses JSON args, invokes matching function, returns JSON-serialized result +- Returns `{ error: "Tool not found" }` for unknown tools +- Catches exceptions and returns `{ error: message }` — never throws + +--- + +## `generateTypes(tools)` + +Generates TypeScript type declarations from tool schemas for inclusion in LLM prompts. + +**Import:** `@cloudflare/codemode` + +```typescript +import { generateTypes } from "@cloudflare/codemode"; + +const types = generateTypes(myTools); +// Returns string with type aliases and `declare const codemode: { ... }` +``` + +**Output format:** +- Named type aliases: `{ToolName}Input`, `{ToolName}Output` +- JSDoc comments from tool descriptions and Zod field descriptions +- `declare const codemode: { toolName(input: ToolNameInput): Promise; ... }` + +Uses `zod-to-ts` to convert Zod schemas into TypeScript AST nodes. + +--- + +## `sanitizeToolName(name)` + +Converts tool names to valid JavaScript identifiers. + +**Import:** `@cloudflare/codemode` + +```typescript +import { sanitizeToolName } from "@cloudflare/codemode"; +``` + +**Rules:** +- Replaces `-`, `.`, spaces with `_` +- Strips non-identifier characters +- Prefixes with `_` if starts with a digit +- Appends `_` to JavaScript reserved words +- Falls back to `_` for empty strings + +**Examples:** + +| Input | Output | +|-------|--------| +| `"get-weather"` | `"get_weather"` | +| `"my-server.list-items"` | `"my_server_list_items"` | +| `"3d-render"` | `"_3d_render"` | +| `"delete"` | `"delete_"` | +| `"hello world"` | `"hello_world"` | + +--- + +## `ToolDescriptor` + +Raw tool descriptor shape (alternative to AI SDK's `tool()` format). + +```typescript +interface ToolDescriptor { + description?: string; + inputSchema: ZodType; + outputSchema?: ZodType; + execute?: (args: unknown) => Promise; +} + +type ToolDescriptors = Record; +``` + +Both `ToolDescriptors` and AI SDK's `ToolSet` are accepted by `createCodeTool` and `generateTypes`. + +--- + +## `normalizeCode(code)` (Internal) + +Parses LLM output with acorn and normalizes it into a valid async arrow function: + +- Empty input → `async () => {}` +- Single arrow function expression → passes through +- Statements ending with expression → wraps in `async () => { ... return (lastExpr) }` +- Multiple statements → wraps in `async () => { ... }` +- Parse failure → wraps blindly in `async () => { ... }` + +This handles various LLM code formats gracefully. diff --git a/skills/codemode/references/executor-patterns.md b/skills/codemode/references/executor-patterns.md new file mode 100644 index 0000000..95de544 --- /dev/null +++ b/skills/codemode/references/executor-patterns.md @@ -0,0 +1,166 @@ +# Executor Patterns + +## DynamicWorkerExecutor (Production) + +The default production executor. Creates isolated V8 Workers for each execution. + +```typescript +import { DynamicWorkerExecutor } from "@cloudflare/codemode"; + +const executor = new DynamicWorkerExecutor({ + loader: env.LOADER, + timeout: 30000, + globalOutbound: null, // Network blocked +}); +``` + +### Wrangler Configuration + +```jsonc +{ + "worker_loaders": [{ "binding": "LOADER" }], + "compatibility_flags": ["nodejs_compat"] +} +``` + +### Network Control + +```typescript +// Fully isolated (default) — fetch() and connect() throw +new DynamicWorkerExecutor({ loader, globalOutbound: null }); + +// Route outbound through a Fetcher (controlled access) +new DynamicWorkerExecutor({ loader, globalOutbound: env.MY_OUTBOUND }); + +// Inherit parent Worker's access (use with caution) +new DynamicWorkerExecutor({ loader, globalOutbound: undefined }); +``` + +### Security Properties + +- Each execution runs in a separate V8 isolate +- No shared state between executions +- Console output captured (not leaked to host) +- Timeout enforced via `Promise.race` +- Tool calls dispatched via Workers RPC (not network requests) + +--- + +## Custom Executor: Node.js VM (Development) + +For local development without Cloudflare Workers. Uses Node.js `vm` module. + +```typescript +import vm from "node:vm"; +import type { Executor, ExecuteResult } from "@cloudflare/codemode"; + +class NodeVMExecutor implements Executor { + async execute( + code: string, + fns: Record Promise> + ): Promise { + const logs: string[] = []; + + // Create codemode proxy + const codemode = new Proxy({}, { + get(_, prop: string) { + return async (...args: unknown[]) => { + const fn = fns[prop]; + if (!fn) throw new Error(`Tool not found: ${prop}`); + return fn(args[0]); + }; + }, + }); + + // Create sandbox context + const context = vm.createContext({ + codemode, + console: { + log: (...args: unknown[]) => logs.push(args.map(String).join(" ")), + warn: (...args: unknown[]) => logs.push(`[warn] ${args.map(String).join(" ")}`), + error: (...args: unknown[]) => logs.push(`[error] ${args.map(String).join(" ")}`), + }, + fetch, + setTimeout, + URL, + Response, + Request, + Headers, + }); + + try { + const script = new vm.Script(`(${code})()`); + const result = await script.runInContext(context, { timeout: 30000 }); + return { result, logs }; + } catch (error) { + return { result: null, error: String(error), logs }; + } + } +} +``` + +### Usage + +```typescript +const executor = new NodeVMExecutor(); +const codemode = createCodeTool({ tools, executor }); +``` + +**Limitations:** +- Less secure than V8 isolates (Node VM can be escaped) +- No WorkerLoader binding needed +- Suitable for local development only + +--- + +## Custom Executor: HTTP Bridge (Remote Execution) + +For running code on a separate server (useful for language-specific runtimes). + +```typescript +import type { Executor, ExecuteResult } from "@cloudflare/codemode"; + +class HTTPExecutor implements Executor { + constructor(private serverUrl: string, private callbackUrl: string) {} + + async execute( + code: string, + fns: Record Promise> + ): Promise { + const execId = crypto.randomUUID(); + + // Register tool callbacks + registerCallbacks(execId, fns); + + try { + const response = await fetch(`${this.serverUrl}/execute`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + code, + callbackUrl: `${this.callbackUrl}/${execId}`, + tools: Object.keys(fns), + }), + }); + + return await response.json() as ExecuteResult; + } finally { + unregisterCallbacks(execId); + } + } +} +``` + +The remote server creates tool proxies that POST back to the callback URL for each `codemode.*` call. See the [codemode example](https://github.com/cloudflare/agents/tree/main/examples/codemode) for a complete Node.js HTTP bridge implementation. + +--- + +## Executor Selection Guide + +| Executor | Environment | Security | Network | Use Case | +|----------|------------|----------|---------|----------| +| `DynamicWorkerExecutor` | Cloudflare Workers | V8 isolate | Configurable | Production | +| Node VM | Node.js | `vm` module | Unrestricted | Local development | +| HTTP Bridge | Any | Depends on server | Depends on server | Multi-language, remote execution | + +Always use `DynamicWorkerExecutor` in production. The `Executor` interface is intentionally minimal to support diverse sandbox implementations. diff --git a/skills/codemode/references/mcp-integration.md b/skills/codemode/references/mcp-integration.md new file mode 100644 index 0000000..f2ea9c1 --- /dev/null +++ b/skills/codemode/references/mcp-integration.md @@ -0,0 +1,113 @@ +# MCP Integration Patterns + +## Merging MCP Tools with Standard Tools + +MCP tools integrate seamlessly with Code Mode. Merge them into the tools object: + +```typescript +const codemode = createCodeTool({ + tools: { + ...myLocalTools, + ...this.mcp.getAITools(), + }, + executor, +}); +``` + +All MCP tool names are automatically sanitized to valid JavaScript identifiers via `sanitizeToolName()`. + +## Tool Name Sanitization + +MCP servers often use namespaced tool names with hyphens and dots. These are converted: + +```typescript +import { sanitizeToolName } from "@cloudflare/codemode"; + +// MCP tools like "github.list-repos" become "github_list_repos" +// The LLM writes: codemode.github_list_repos({ owner: "cloudflare" }) +``` + +The type generation step uses sanitized names, so the LLM sees valid identifiers in the TypeScript declarations. + +## Multi-Server Orchestration + +Code Mode excels when composing tools from multiple MCP servers in a single execution: + +```javascript +// LLM generates code that orchestrates across servers: +async () => { + // Query filesystem MCP + const files = await codemode.fs_list_files({ path: "/projects" }); + + // Query database MCP + const records = await codemode.db_query({ + query: "SELECT * FROM projects WHERE name = ?", + params: [files[0].name], + }); + + // Conditional logic across servers + if (records.length === 0) { + await codemode.tasks_create({ + title: `Review: ${files[0].name}`, + priority: "high", + }); + } + + return { files: files.length, records: records.length }; +}; +``` + +Without Code Mode, this would require 3+ LLM round-trips. With Code Mode, it's a single tool call. + +## Token Comparison + +| Approach | Token Cost | Round Trips | +|----------|-----------|-------------| +| Traditional MCP (90 tools loaded) | ~70K at startup | N per workflow | +| Code Mode with search | ~1K fixed + ~800 per search | 1 per workflow | +| Code Mode with merged tools | ~2K fixed (tool definition) | 1 per workflow | + +## Server-Side Code Mode (Cloudflare MCP Pattern) + +For exposing large APIs (like the Cloudflare API with 2,500+ endpoints), the server-side Code Mode pattern uses two tools: + +1. **`search()`** — Queries the OpenAPI spec through code execution +2. **`execute()`** — Performs authenticated API operations + +```javascript +// search: Agent explores the API +async () => { + const results = []; + for (const [path, methods] of Object.entries(spec.paths)) { + if (path.includes("/zones/") && path.includes("dns")) { + for (const [method, op] of Object.entries(methods)) { + results.push({ method: method.toUpperCase(), path, summary: op.summary }); + } + } + } + return results; +}; + +// execute: Agent calls the discovered endpoints +async () => { + const zones = await cloudflare.request({ + method: "GET", + path: "/zones", + }); + const records = await cloudflare.request({ + method: "GET", + path: `/zones/${zones.result[0].id}/dns_records`, + }); + return records.result.map((r) => ({ name: r.name, type: r.type, content: r.content })); +}; +``` + +This reduces ~1.17M tokens (traditional one-tool-per-endpoint MCP) to ~1K tokens. + +## Best Practices + +- **Merge all relevant tools** before creating the code tool — the LLM can compose across any tools it sees +- **Use descriptive tool names and descriptions** — the LLM relies on these to generate correct code +- **Keep tool granularity fine** — Code Mode handles composition, so tools can be atomic +- **Monitor generated code quality** — depends on prompt engineering and model capability +- **Set appropriate timeouts** — multi-server orchestration may need longer than the 30s default