6.2 KiB
| title | aliases | tags | sources | created | updated | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Custom Tools in the Agent SDK |
|
|
|
2026-04-17 | 2026-04-17 |
Custom Tools in the Agent SDK
Extend Claude's capabilities by defining your own async functions — wrapped in an in-process MCP server — that Claude can call during any query() session.
Tool Anatomy
Every tool has four required parts:
| Part | Purpose |
|---|---|
| Name | Unique ID Claude uses to call the tool |
| Description | What the tool does — Claude reads this to decide when to call it |
| Input schema | Arguments Claude must supply (Zod in TS, dict or JSON Schema in Python) |
| Handler | async function that runs when Claude calls the tool; returns { content, isError? } |
The content array accepts blocks of type "text", "image", or "resource".
Defining a Tool
Python — use the @tool decorator:
from claude_agent_sdk import tool, create_sdk_mcp_server
@tool(
"get_temperature",
"Get the current temperature at a location",
{"latitude": float, "longitude": float},
)
async def get_temperature(args):
# ... fetch data ...
return {"content": [{"type": "text", "text": "72°F"}]}
weather_server = create_sdk_mcp_server(
name="weather", version="1.0.0", tools=[get_temperature]
)
TypeScript — use the tool() helper with a Zod schema (args are auto-typed).
Registering & Calling Tools
Pass the server to query() via mcpServers. The tool's fully-qualified name follows:
mcp__{server_name}__{tool_name}
List it in allowedTools to skip the permission prompt:
options = ClaudeAgentOptions(
mcp_servers={"weather": weather_server},
allowed_tools=["mcp__weather__get_temperature"],
)
async for msg in query(prompt="Temp in SF?", options=options):
...
Use wildcard mcp__weather__* to approve all tools on a server at once.
Optional Parameters
- TypeScript: add
.default()to a Zod field. - Python: omit the key from the schema dict, mention it in the description, read with
args.get("key", default).
Tool Annotations
Pass via annotations=ToolAnnotations(...) to give Claude hints about side effects:
| Field | Default | Effect |
|---|---|---|
readOnlyHint |
false |
Allows parallel batching with other read-only tools |
destructiveHint |
true |
Informational — flags potentially destructive ops |
idempotentHint |
false |
Informational — repeated calls are safe |
openWorldHint |
true |
Informational — tool reaches external systems |
Annotations are metadata only — they do not enforce behavior.
Controlling Tool Access
Two separate layers govern tool availability:
| Option | Layer | Effect |
|---|---|---|
tools: ["Read", "Grep"] |
Availability | Restricts which built-ins appear in Claude's context |
tools: [] |
Availability | Removes all built-ins; Claude only uses MCP tools |
allowedTools |
Permission | Listed tools run without a prompt |
disallowedTools |
Permission | Calls denied, but tool stays visible (wastes turns) |
Prefer tools over disallowedTools to restrict built-ins — omitting removes it from context entirely.
Error Handling
| Handler behavior | Agent loop |
|---|---|
| Throws uncaught exception | Stops — query() fails, Claude never sees the error |
Returns is_error: True in result |
Continues — Claude sees error as data, can retry or explain |
Always try/except (Python) or try/catch (TS) inside handlers and return is_error: True for recoverable failures.
Returning Images & Resources
Images — base64-encode raw bytes inline (no URL field, no data:image/... prefix):
return {"content": [{"type": "image", "data": base64_str, "mimeType": "image/png"}]}
Resources — embed content identified by a URI label:
return {"content": [{"type": "resource", "resource": {
"uri": "file:///tmp/report.md",
"mimeType": "text/markdown",
"text": "# Report\n..."
}}]}
The URI is a label for Claude — the SDK does not read from that path.
Enum / Complex Schemas (Python)
The dict schema doesn't support enums. Use full JSON Schema when you need enums, ranges, or nested objects:
@tool("convert_units", "...", {
"type": "object",
"properties": {
"unit_type": {"type": "string", "enum": ["length", "temperature", "weight"]},
...
},
"required": ["unit_type", ...]
})
Scaling Beyond a Few Tools
Each tool in the server array consumes context window space every turn. For dozens of tools, use wiki/agent-sdk/tool-search to load tools on demand instead of all upfront.
Key Takeaways
- Define tools with
@tool(Python) /tool()(TS) → wrap increate_sdk_mcp_server→ pass toquery()viamcpServers - Tool names follow
mcp__{server_name}__{tool_name}; use wildcardmcp__server__*to approve all - Return
is_error: True(not throw) to keep the agent loop alive on tool failure readOnlyHint: truelets Claude batch parallel calls — keep annotations accurate- Use
tools: []to remove all built-ins; usetools: ["Read"]to whitelist specific built-ins - Images must be base64 inline; resources carry content in
textorblob, URI is just a label - For enum/complex input schemas in Python, pass a full JSON Schema dict instead of the shorthand dict
Related
- wiki/agent-sdk/mcp-integration — connecting to external MCP servers (filesystem, GitHub, Slack)
- wiki/agent-sdk/configure-permissions — full evaluation order for allowed/disallowed tools
- wiki/agent-sdk/python-api-reference —
@tool,create_sdk_mcp_server,ToolAnnotationssignatures - wiki/agent-sdk/typescript-api-reference —
tool(), Zod schemas,ToolAnnotations - wiki/agent-sdk/structured-outputs — returning validated JSON from agent workflows
Sources
raw/Give Claude custom tools.md— clipped from https://code.claude.com/docs/en/agent-sdk/custom-tools