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.
- Read · Write · Editfile io.
Editis the surgical one — match-and-replace, not blind overwrite. - Glob · Grepfinding files by pattern, finding text inside files. cheap and fast.
- Basharbitrary shell. the most powerful and most dangerous tool — gate it behind a permission rule.
- WebSearch · WebFetchsearch the web and fetch a single url. WebFetch follows redirects and strips boilerplate to a markdown-ish blob.
- Agentspawn a sub-agent with isolated context.
- TodoWritethe model's own scratchpad — useful for long, multi-step runs.
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.
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.
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.
// 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"],
},
}); what to notice
- tools are pure data to the agent.the model sees a name, a description, and a json schema. write descriptions like you're writing a docstring for a coworker.
- errors come back as content.throw inside a handler and the sdk wraps the message as
tool_resultwithis_error: true. the model sees it and usually self-corrects. - no global registry.tools live on the
options.mcpServersbag for that onequery(). two parallel queries can have completely different toolsets.