scalable/ai/claude agent sdk/lesson 03 cph / /
lesson 03 / 08 · 15 min · updated ·

tools — file io, bash, and the rest

the built-in toolset, defining your own tool, and wiring an mcp server. when to reach for which.

tl;dr. the sdk ships a built-in toolset (file io, bash, glob, grep, web fetch and more), an allow / disallow list to scope what the model can reach, a tool() helper for defining your own in pure typescript, and stdio / http transports for connecting external mcp servers. tool naming follows a strict convention you'll see in every lesson from here on.

the built-in toolset

the sdk inherits the toolset that powers claude code. you get the same primitives the cli uses, callable from any of your query() calls. the names matter: they're case-sensitive identifiers used in allowedTools, canUseTool, and the hook matchers.

allow vs disallow

by default a query() can use any built-in tool. that's rarely what you want — most agents have a narrow job, and a narrower tool surface is a free safety rail.

scope tools to the smallest set that does the job
import { query } from "@anthropic-ai/claude-agent-sdk";

// allow-list: only these tools are exposed to the model.
const run = query({
  prompt: "find every README.md and tell me which is largest",
  options: {
    allowedTools: ["Glob", "Read"],
    // disallowedTools: ["Bash"],  // alternative — block specific tools instead
  },
});

allowedTools is an allow-list (only these are exposed). disallowedTools is the opposite (everything except these). pick whichever produces the shorter, more readable list. for production agents the allow-list is almost always the right answer — it's a closed-world assumption, not an open one.

defining your own

the sdk's tool() helper takes a name, a description, a zod schema for the input, and an async handler that returns a content array. the output of createSdkMcpServer() plugs straight into mcpServers — your tool runs in-process, no extra deployment surface.

an in-process tool with a zod schema
import { query, tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
import { z } from "zod";

const wordCount = tool(
  "word_count",
  "count words in a string",
  { text: z.string().describe("text to count words in") },
  async ({ text }) => ({
    content: [{ type: "text", text: String(text.trim().split(/\s+/).length) }],
  }),
);

const local = createSdkMcpServer({
  name: "local-utils",
  version: "0.1.0",
  tools: [wordCount],
});

const run = query({
  prompt: "how many words in 'a small studio in denmark'?",
  options: {
    mcpServers: { utils: local },
    allowedTools: ["mcp__utils__word_count"],
  },
});

note the tool name in allowedTools: mcp__utils__word_count. mcp tools always carry the mcp__<server>__<tool> prefix so they can never collide with built-ins. that prefix shows up everywhere from canUseTool to hook matchers — once you see it, you'll spot it.

external mcp servers

when the tool you want already exists as an mcp server (github, postgres, slack, filesystem, whatever), wire it as a transport. stdio is the most common — the sdk spawns the process and pipes json-rpc over stdin/stdout. http and sse are also supported.

stdio mcp — github tools without writing one yourself
// connect to a stdio-based mcp server (a separate process)
const run = query({
  prompt: "list open issues in the repo",
  options: {
    mcpServers: {
      github: {
        type: "stdio",
        command: "npx",
        args: ["-y", "@modelcontextprotocol/server-github"],
        env: { GITHUB_TOKEN: process.env.GITHUB_TOKEN! },
      },
    },
    // tool names are namespaced as mcp__<server>__<tool>
    allowedTools: ["mcp__github__list_issues"],
  },
});
tools aren't free. every tool you expose is a tool the model has to consider on every turn. more tools means longer system prompts, more tokens, more latency, more chances to pick the wrong one. start with the smallest set; add only when the model's reasoning gets visibly worse without it.

what to notice

scalable labs·cvr 30091604·github·linkedin·hello@scalable.dk