obsidian/wiki/agent-sdk/custom-tools.md
2026-04-17 12:49:28 +01:00

6.2 KiB

title aliases tags sources created updated
Custom Tools in the Agent SDK
custom-tools
sdk-custom-tools
mcp-custom-tools
agent-sdk
mcp
tools
python
typescript
raw/Give Claude custom tools.md
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 Stopsquery() 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 in create_sdk_mcp_server → pass to query() via mcpServers
  • Tool names follow mcp__{server_name}__{tool_name}; use wildcard mcp__server__* to approve all
  • Return is_error: True (not throw) to keep the agent loop alive on tool failure
  • readOnlyHint: true lets Claude batch parallel calls — keep annotations accurate
  • Use tools: [] to remove all built-ins; use tools: ["Read"] to whitelist specific built-ins
  • Images must be base64 inline; resources carry content in text or blob, URI is just a label
  • For enum/complex input schemas in Python, pass a full JSON Schema dict instead of the shorthand dict

Sources