Follow Discord
Sweep 22 Oct 2026 · 21:04Z Build v2.1.309 528 read Stable v2.1.308 Latest v2.1.309 Next v2.1.309 Feeds RSS JSON llms.txt llms-full.txt Unofficial
One change · claude-code

Give Claude custom tools changedagent-sdk/custom-tools

Nearest release: v2.1.304, published 8 hours before upstream edited the page. Shown because the two are within 24 hours of each other. Nothing here says the release caused the edit.

Upstream edited this page at 20 Oct 2026 04:09 UTC, give or take a minute or two: the time comes from Anthropic’s own sitemap rather than from a commit. This site recorded the change at 20 Oct 2026 18:37 UTC.

Upstream edited
Recorded here
Lines+634added
Lines−656removed
From line 37 where the diff opens
First seen 14 Aug 2026 this site's first read of the page
Recorded edits14to this page, all time

The whole hunk

from line 37, old and new numbered
/
lines

The two sides of this change are more than 400 edits apart, too far apart to line up, so this is the differ's own diff of it and the words inside a line are not marked.

from line 37
3737 
3838Python examples on this page that make HTTP requests use [httpx](https://www.python-httpx.org/). Add it with the package manager your project uses:
3939 
40<Tabs>
41 <Tab title="Python (uv)">
42 ```bash theme={null}
43 uv add httpx
44 ```
45 </Tab>
46 
47 <Tab title="Python (pip)">
48 ```bash theme={null}
49 pip install httpx
50 ```
51 </Tab>
52</Tabs>
40**Python (uv)**
41 
42```bash theme={null}
43uv add httpx
44```
45 
46**Python (pip)**
47 
48```bash theme={null}
49pip install httpx
50```
5351 
5452### Weather tool example
5553 
5654This example defines a `get_temperature` tool and wraps it in an MCP server, without passing the server to `query`. To run the tool, see [Call a custom tool](#call-a-custom-tool) below.
5755 
58<CodeGroup>
59 ```python Python theme={null}
60 from typing import Annotated, Any
61 import httpx
62 from claude_agent_sdk import tool, create_sdk_mcp_server
63 
64 
65 # Define a tool: name, description, input schema, handler
66 @tool(
67 "get_temperature",
68 "Get the current temperature at a location",
69 {
70 "latitude": Annotated[float, "Latitude coordinate"],
71 "longitude": Annotated[float, "Longitude coordinate"],
72 },
73 )
74 async def get_temperature(args: dict[str, Any]) -> dict[str, Any]:
75 async with httpx.AsyncClient() as client:
76 response = await client.get(
77 "https://api.open-meteo.com/v1/forecast",
78 params={
79 "latitude": args["latitude"],
80 "longitude": args["longitude"],
81 "current": "temperature_2m",
82 "temperature_unit": "fahrenheit",
83 },
84 )
85 data = response.json()
86 
87 # Return a content array - Claude sees this as the tool result
88 return {
89 "content": [
90 {
91 "type": "text",
92 "text": f"Temperature: {data['current']['temperature_2m']}°F",
93 }
94 ]
95 }
96 
97 
98 # Wrap the tool in an in-process MCP server
99 weather_server = create_sdk_mcp_server(
100 name="weather",
101 version="1.0.0",
102 tools=[get_temperature],
103 )
104 ```
105 
106 ```typescript TypeScript theme={null}
107 import { tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
108 import { z } from "zod";
109 
110 // Define a tool: name, description, input schema, handler
111 const getTemperature = tool(
56```python Python theme={null}
57from typing import Annotated, Any
58import httpx
59from claude_agent_sdk import tool, create_sdk_mcp_server
60 
61 
62# Define a tool: name, description, input schema, handler
63@tool(
11264 "get_temperature",
11365 "Get the current temperature at a location",
11466 {
115 latitude: z.number().describe("Latitude coordinate"), // .describe() adds a field description Claude sees
116 longitude: z.number().describe("Longitude coordinate")
67 "latitude": Annotated[float, "Latitude coordinate"],
68 "longitude": Annotated[float, "Longitude coordinate"],
11769 },
118 async (args) => {
119 // args is typed from the schema: { latitude: number; longitude: number }
120 const response = await fetch(
121 `https://api.open-meteo.com/v1/forecast?latitude=${args.latitude}&longitude=${args.longitude}&current=temperature_2m&temperature_unit=fahrenheit`
122 );
123 const data: any = await response.json();
124 
125 // Return a content array - Claude sees this as the tool result
126 return {
127 content: [{ type: "text", text: `Temperature: ${data.current.temperature_2m}°F` }]
128 };
129 }
130 );
131 
132 // Wrap the tool in an in-process MCP server
133 const weatherServer = createSdkMcpServer({
134 name: "weather",
135 version: "1.0.0",
136 tools: [getTemperature]
137 });
138 ```
139</CodeGroup>
70)
71async def get_temperature(args: dict[str, Any]) -> dict[str, Any]:
72 async with httpx.AsyncClient() as client:
73 response = await client.get(
74 "https://api.open-meteo.com/v1/forecast",
75 params={
76 "latitude": args["latitude"],
77 "longitude": args["longitude"],
78 "current": "temperature_2m",
79 "temperature_unit": "fahrenheit",
80 },
81 )
82 data = response.json()
83 
84 # Return a content array - Claude sees this as the tool result
85 return {
86 "content": [
87 {
88 "type": "text",
89 "text": f"Temperature: {data['current']['temperature_2m']}°F",
90 }
91 ]
92 }
93 
94 
95# Wrap the tool in an in-process MCP server
96weather_server = create_sdk_mcp_server(
97 name="weather",
98 version="1.0.0",
99 tools=[get_temperature],
100)
101```
102 
103```typescript TypeScript theme={null}
104import { tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
105import { z } from "zod";
106 
107// Define a tool: name, description, input schema, handler
108const getTemperature = tool(
109 "get_temperature",
110 "Get the current temperature at a location",
111 {
112 latitude: z.number().describe("Latitude coordinate"), // .describe() adds a field description Claude sees
113 longitude: z.number().describe("Longitude coordinate")
114 },
115 async (args) => {
116 // args is typed from the schema: { latitude: number; longitude: number }
117 const response = await fetch(
118 `https://api.open-meteo.com/v1/forecast?latitude=${args.latitude}&longitude=${args.longitude}&current=temperature_2m&temperature_unit=fahrenheit`
119 );
120 const data: any = await response.json();
121 
122 // Return a content array - Claude sees this as the tool result
123 return {
124 content: [{ type: "text", text: `Temperature: ${data.current.temperature_2m}°F` }]
125 };
126 }
127);
128 
129// Wrap the tool in an in-process MCP server
130const weatherServer = createSdkMcpServer({
131 name: "weather",
132 version: "1.0.0",
133 tools: [getTemperature]
134});
135```
140136 
141137See the [`tool()`](/docs/en/agent-sdk/typescript#tool) TypeScript reference or the [`@tool`](/docs/en/agent-sdk/python#tool) Python reference for full parameter details, including JSON Schema input formats and return value structure.
142138 
from line 151
155151 
156152These snippets reuse the `weatherServer` from the [weather tool example](#weather-tool-example) to ask Claude what the weather is in a specific location.
157153 
158<CodeGroup>
159 ```python Python theme={null}
160 import asyncio
161 from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
162 
163 
164 async def main():
165 options = ClaudeAgentOptions(
166 mcp_servers={"weather": weather_server},
167 allowed_tools=["mcp__weather__get_temperature"],
168 )
169 
170 async for message in query(
171 prompt="What's the temperature in San Francisco?",
172 options=options,
173 ):
174 # ResultMessage is the final message after all tool calls complete
175 if isinstance(message, ResultMessage) and message.subtype == "success":
176 print(message.result)
177 
178 
179 asyncio.run(main())
180 ```
181 
182 ```typescript TypeScript theme={null}
183 import { query } from "@anthropic-ai/claude-agent-sdk";
184 
185 for await (const message of query({
186 prompt: "What's the temperature in San Francisco?",
187 options: {
188 mcpServers: { weather: weatherServer },
189 allowedTools: ["mcp__weather__get_temperature"]
190 }
191 })) {
192 // "result" is the final message after all tool calls complete
193 if (message.type === "result" && message.subtype === "success") {
194 console.log(message.result);
195 }
154```python Python theme={null}
155import asyncio
156from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
157 
158 
159async def main():
160 options = ClaudeAgentOptions(
161 mcp_servers={"weather": weather_server},
162 allowed_tools=["mcp__weather__get_temperature"],
163 )
164 
165 async for message in query(
166 prompt="What's the temperature in San Francisco?",
167 options=options,
168 ):
169 # ResultMessage is the final message after all tool calls complete
170 if isinstance(message, ResultMessage) and message.subtype == "success":
171 print(message.result)
172 
173 
174asyncio.run(main())
175```
176 
177```typescript TypeScript theme={null}
178import { query } from "@anthropic-ai/claude-agent-sdk";
179 
180for await (const message of query({
181 prompt: "What's the temperature in San Francisco?",
182 options: {
183 mcpServers: { weather: weatherServer },
184 allowedTools: ["mcp__weather__get_temperature"]
196185 }
197 ```
198</CodeGroup>
186})) {
187 // "result" is the final message after all tool calls complete
188 if (message.type === "result" && message.subtype === "success") {
189 console.log(message.result);
190 }
191}
192```
199193 
200194Combine this snippet with the tool and server definitions from the [weather tool example](#weather-tool-example) in one file, `weather.py` or `weather.ts`, then run it from your terminal:
201195 
202<Tabs>
203 <Tab title="TypeScript">
204 ```bash theme={null}
205 npx tsx weather.ts
206 ```
207 </Tab>
208 
209 <Tab title="Python (uv)">
210 ```bash theme={null}
211 uv run weather.py
212 ```
213 </Tab>
214 
215 <Tab title="Python (pip)">
216 Activate the virtual environment where you installed the SDK, then run:
217 
218 ```bash theme={null}
219 python weather.py
220 ```
221 </Tab>
222</Tabs>
196**TypeScript**
197 
198```bash theme={null}
199npx tsx weather.ts
200```
201 
202**Python (uv)**
203 
204```bash theme={null}
205uv run weather.py
206```
207 
208**Python (pip)**
209 
210Activate the virtual environment where you installed the SDK, then run:
211 
212```bash theme={null}
213python weather.py
214```
223215 
224216Claude calls `get_temperature` and the script prints a one-line answer with the current temperature in San Francisco.
225217 
from line 221
229221 
230222The example below defines a second tool, `get_precipitation_chance`, and replaces the `weatherServer` definition from the [weather tool example](#weather-tool-example) with one that lists both tools in the array.
231223 
232<CodeGroup>
233 ```python Python theme={null}
234 # Define a second tool for the same server
235 @tool(
236 "get_precipitation_chance",
237 "Get the hourly precipitation probability for a location",
238 {
239 "type": "object",
240 "properties": {
241 "latitude": {"type": "number"},
242 "longitude": {"type": "number"},
243 "hours": {
244 "type": "integer",
245 "minimum": 1,
246 "maximum": 24,
247 "description": "How many hours of forecast to return",
248 },
249 },
250 # 'hours' is left out of required, so Claude can omit it
251 "required": ["latitude", "longitude"],
252 },
253 )
254 async def get_precipitation_chance(args: dict[str, Any]) -> dict[str, Any]:
255 # 'hours' isn't in required - read it with .get() to fall back to a default
256 hours = args.get("hours", 12)
257 async with httpx.AsyncClient() as client:
258 response = await client.get(
259 "https://api.open-meteo.com/v1/forecast",
260 params={
261 "latitude": args["latitude"],
262 "longitude": args["longitude"],
263 "hourly": "precipitation_probability",
264 "forecast_days": 1,
265 },
266 )
267 data = response.json()
268 chances = data["hourly"]["precipitation_probability"][:hours]
269 
270 return {
271 "content": [
272 {
273 "type": "text",
274 "text": f"Next {hours} hours: {'%, '.join(map(str, chances))}%",
275 }
276 ]
277 }
278 
279 
280 # Rebuild the server with both tools in the array
281 weather_server = create_sdk_mcp_server(
282 name="weather",
283 version="1.0.0",
284 tools=[get_temperature, get_precipitation_chance],
285 )
286 ```
287 
288 ```typescript TypeScript theme={null}
289 // Define a second tool for the same server
290 const getPrecipitationChance = tool(
224```python Python theme={null}
225# Define a second tool for the same server
226@tool(
291227 "get_precipitation_chance",
292228 "Get the hourly precipitation probability for a location",
293229 {
294 latitude: z.number(),
295 longitude: z.number(),
296 hours: z
297 .number()
298 .int()
299 .min(1)
300 .max(24)
301 .optional() // .optional() lets Claude omit the parameter
302 .describe("How many hours of forecast to return")
230 "type": "object",
231 "properties": {
232 "latitude": {"type": "number"},
233 "longitude": {"type": "number"},
234 "hours": {
235 "type": "integer",
236 "minimum": 1,
237 "maximum": 24,
238 "description": "How many hours of forecast to return",
239 },
240 },
241 # 'hours' is left out of required, so Claude can omit it
242 "required": ["latitude", "longitude"],
303243 },
304 async (args) => {
305 const hours = args.hours ?? 12; // Apply the default in the handler
306 const response = await fetch(
307 `https://api.open-meteo.com/v1/forecast?latitude=${args.latitude}&longitude=${args.longitude}&hourly=precipitation_probability&forecast_days=1`
308 );
309 const data: any = await response.json();
310 const chances = data.hourly.precipitation_probability.slice(0, hours);
311 
312 return {
313 content: [{ type: "text", text: `Next ${hours} hours: ${chances.join("%, ")}%` }]
314 };
315 }
316 );
317 
318 // Rebuild the server with both tools in the array
319 const weatherServer = createSdkMcpServer({
320 name: "weather",
321 version: "1.0.0",
322 tools: [getTemperature, getPrecipitationChance]
323 });
324 ```
325</CodeGroup>
244)
245async def get_precipitation_chance(args: dict[str, Any]) -> dict[str, Any]:
246 # 'hours' isn't in required - read it with .get() to fall back to a default
247 hours = args.get("hours", 12)
248 async with httpx.AsyncClient() as client:
249 response = await client.get(
250 "https://api.open-meteo.com/v1/forecast",
251 params={
252 "latitude": args["latitude"],
253 "longitude": args["longitude"],
254 "hourly": "precipitation_probability",
255 "forecast_days": 1,
256 },
257 )
258 data = response.json()
259 chances = data["hourly"]["precipitation_probability"][:hours]
260 
261 return {
262 "content": [
263 {
264 "type": "text",
265 "text": f"Next {hours} hours: {'%, '.join(map(str, chances))}%",
266 }
267 ]
268 }
269 
270 
271# Rebuild the server with both tools in the array
272weather_server = create_sdk_mcp_server(
273 name="weather",
274 version="1.0.0",
275 tools=[get_temperature, get_precipitation_chance],
276)
277```
278 
279```typescript TypeScript theme={null}
280// Define a second tool for the same server
281const getPrecipitationChance = tool(
282 "get_precipitation_chance",
283 "Get the hourly precipitation probability for a location",
284 {
285 latitude: z.number(),
286 longitude: z.number(),
287 hours: z
288 .number()
289 .int()
290 .min(1)
291 .max(24)
292 .optional() // .optional() lets Claude omit the parameter
293 .describe("How many hours of forecast to return")
294 },
295 async (args) => {
296 const hours = args.hours ?? 12; // Apply the default in the handler
297 const response = await fetch(
298 `https://api.open-meteo.com/v1/forecast?latitude=${args.latitude}&longitude=${args.longitude}&hourly=precipitation_probability&forecast_days=1`
299 );
300 const data: any = await response.json();
301 const chances = data.hourly.precipitation_probability.slice(0, hours);
302 
303 return {
304 content: [{ type: "text", text: `Next ${hours} hours: ${chances.join("%, ")}%` }]
305 };
306 }
307);
308 
309// Rebuild the server with both tools in the array
310const weatherServer = createSdkMcpServer({
311 name: "weather",
312 version: "1.0.0",
313 tools: [getTemperature, getPrecipitationChance]
314});
315```
326316 
327317[Tool search](/docs/en/agent-sdk/tool-search) is on by default and defers SDK MCP tools: Claude sees each tool's name in a compact list and loads its full schema on demand. With tool search disabled, every tool in this array consumes context window space on every turn. In TypeScript, pass `alwaysLoad: true` in the `extras` argument of [`tool()`](/docs/en/agent-sdk/typescript#tool) or in the options of [`createSdkMcpServer()`](/docs/en/agent-sdk/typescript#createsdkmcpserver) to keep a tool's full schema in the initial prompt.
328318 
from line 331
341331 
342332This example adds `readOnlyHint` to the `get_temperature` tool from the [weather tool example](#weather-tool-example).
343333 
344<CodeGroup>
345 ```python Python theme={null}
346 from claude_agent_sdk import tool, ToolAnnotations
347 
348 
349 @tool(
350 "get_temperature",
351 "Get the current temperature at a location",
352 {"latitude": float, "longitude": float},
353 annotations=ToolAnnotations(
354 readOnlyHint=True
355 ), # Lets Claude batch this with other read-only calls
356 )
357 async def get_temperature(args):
358 return {"content": [{"type": "text", "text": "..."}]}
359 ```
360 
361 ```typescript TypeScript theme={null}
362 import { tool } from "@anthropic-ai/claude-agent-sdk";
363 import { z } from "zod";
364 
365 tool(
334```python Python theme={null}
335from claude_agent_sdk import tool, ToolAnnotations
336 
337 
338@tool(
366339 "get_temperature",
367340 "Get the current temperature at a location",
368 { latitude: z.number(), longitude: z.number() },
369 async (args) => ({ content: [{ type: "text", text: `...` }] }),
370 { annotations: { readOnlyHint: true } } // Lets Claude batch this with other read-only calls
371 );
372 ```
373</CodeGroup>
341 {"latitude": float, "longitude": float},
342 annotations=ToolAnnotations(
343 readOnlyHint=True
344 ), # Lets Claude batch this with other read-only calls
345)
346async def get_temperature(args):
347 return {"content": [{"type": "text", "text": "..."}]}
348```
349 
350```typescript TypeScript theme={null}
351import { tool } from "@anthropic-ai/claude-agent-sdk";
352import { z } from "zod";
353 
354tool(
355 "get_temperature",
356 "Get the current temperature at a location",
357 { latitude: z.number(), longitude: z.number() },
358 async (args) => ({ content: [{ type: "text", text: `...` }] }),
359 { annotations: { readOnlyHint: true } } // Lets Claude batch this with other read-only calls
360);
361```
374362 
375363See `ToolAnnotations` in the [TypeScript](/docs/en/agent-sdk/typescript#toolannotations) or [Python](/docs/en/agent-sdk/python#toolannotations) reference.
376364 
from line 392
404392 
405393The example below catches two kinds of failures inside the handler and composes the error message Claude reads. A non-200 HTTP status is caught from the response and returned as an error result. A network error or invalid JSON is caught by the surrounding `try/except` (Python) or `try/catch` (TypeScript) and also returned as an error result. In both cases Claude receives a message that describes the failure instead of a bare exception string.
406394 
407<CodeGroup>
408 ```python Python theme={null}
409 import json
410 import httpx
411 from typing import Any
412 from claude_agent_sdk import tool
413 
414 
415 @tool(
416 "fetch_data",
417 "Fetch data from an API",
418 {"endpoint": str}, # Simple schema
419 )
420 async def fetch_data(args: dict[str, Any]) -> dict[str, Any]:
421 try:
422 async with httpx.AsyncClient() as client:
423 response = await client.get(args["endpoint"])
424 if response.status_code != 200:
425 # Return the failure as a tool result so Claude can react to it.
426 # is_error marks this as a failed call rather than odd-looking data.
427 return {
428 "content": [
429 {
430 "type": "text",
431 "text": f"API error: {response.status_code} {response.reason_phrase}",
432 }
433 ],
434 "is_error": True,
435 }
436 
437 data = response.json()
438 return {"content": [{"type": "text", "text": json.dumps(data, indent=2)}]}
439 except Exception as e:
440 # Composes the message Claude reads. An uncaught exception would
441 # reach Claude as the raw str(e) with no context.
442 return {
443 "content": [{"type": "text", "text": f"Failed to fetch data: {str(e)}"}],
444 "is_error": True,
445 }
446 ```
447 
448 ```typescript TypeScript theme={null}
449 import { tool } from "@anthropic-ai/claude-agent-sdk";
450 import { z } from "zod";
451 
452 tool(
395```python Python theme={null}
396import json
397import httpx
398from typing import Any
399from claude_agent_sdk import tool
400 
401 
402@tool(
453403 "fetch_data",
454404 "Fetch data from an API",
455 {
456 endpoint: z.string().url().describe("API endpoint URL")
457 },
458 async (args) => {
459 try {
460 const response = await fetch(args.endpoint);
461 
462 if (!response.ok) {
463 // Return the failure as a tool result so Claude can react to it.
464 // isError marks this as a failed call rather than odd-looking data.
465 return {
466 content: [
467 {
468 type: "text",
469 text: `API error: ${response.status} ${response.statusText}`
470 }
471 ],
472 isError: true
473 };
405 {"endpoint": str}, # Simple schema
406)
407async def fetch_data(args: dict[str, Any]) -> dict[str, Any]:
408 try:
409 async with httpx.AsyncClient() as client:
410 response = await client.get(args["endpoint"])
411 if response.status_code != 200:
412 # Return the failure as a tool result so Claude can react to it.
413 # is_error marks this as a failed call rather than odd-looking data.
414 return {
415 "content": [
416 {
417 "type": "text",
418 "text": f"API error: {response.status_code} {response.reason_phrase}",
419 }
420 ],
421 "is_error": True,
422 }
423 
424 data = response.json()
425 return {"content": [{"type": "text", "text": json.dumps(data, indent=2)}]}
426 except Exception as e:
427 # Composes the message Claude reads. An uncaught exception would
428 # reach Claude as the raw str(e) with no context.
429 return {
430 "content": [{"type": "text", "text": f"Failed to fetch data: {str(e)}"}],
431 "is_error": True,
474432 }
475 
476 const data = await response.json();
433```
434 
435```typescript TypeScript theme={null}
436import { tool } from "@anthropic-ai/claude-agent-sdk";
437import { z } from "zod";
438 
439tool(
440 "fetch_data",
441 "Fetch data from an API",
442 {
443 endpoint: z.string().url().describe("API endpoint URL")
444 },
445 async (args) => {
446 try {
447 const response = await fetch(args.endpoint);
448 
449 if (!response.ok) {
450 // Return the failure as a tool result so Claude can react to it.
451 // isError marks this as a failed call rather than odd-looking data.
477452 return {
478453 content: [
479454 {
480455 type: "text",
481 text: JSON.stringify(data, null, 2)
482 }
483 ]
484 };
485 } catch (error) {
486 // Composes the message Claude reads. An uncaught throw would
487 // reach Claude as the raw error message with no context.
488 return {
489 content: [
490 {
491 type: "text",
492 text: `Failed to fetch data: ${error instanceof Error ? error.message : String(error)}`
456 text: `API error: ${response.status} ${response.statusText}`
493457 }
494458 ],
495459 isError: true
496460 };
497461 }
498 }
499 );
500 ```
501</CodeGroup>
462 
463 const data = await response.json();
464 return {
465 content: [
466 {
467 type: "text",
468 text: JSON.stringify(data, null, 2)
469 }
470 ]
471 };
472 } catch (error) {
473 // Composes the message Claude reads. An uncaught throw would
474 // reach Claude as the raw error message with no context.
475 return {
476 content: [
477 {
478 type: "text",
479 text: `Failed to fetch data: ${error instanceof Error ? error.message : String(error)}`
480 }
481 ],
482 isError: true
483 };
484 }
485 }
486);
487```
502488 
503489## Return images and resources
504490 
from line 502
516502| `data` | `string` | Base64-encoded bytes. Raw base64 only, no `data:image/...;base64,` prefix |
517503| `mimeType` | `string` | Required. For example `image/png`, `image/jpeg`, `image/webp`, `image/gif` |
518504 
519<CodeGroup>
520 ```python Python theme={null}
521 import base64
522 import httpx
523 from claude_agent_sdk import tool
524 
525 
526 # Define a tool that fetches an image from a URL and returns it to Claude
527 @tool("fetch_image", "Fetch an image from a URL and return it to Claude", {"url": str})
528 async def fetch_image(args):
529 async with httpx.AsyncClient() as client: # Fetch the image bytes
530 response = await client.get(args["url"])
531 
532 return {
533 "content": [
534 {
535 "type": "image",
536 "data": base64.b64encode(response.content).decode(
537 "ascii"
538 ), # Base64-encode the raw bytes
539 "mimeType": response.headers.get(
540 "content-type", "image/png"
541 ), # Read MIME type from the response
542 }
543 ]
544 }
545 ```
546 
547 ```typescript TypeScript theme={null}
548 import { tool } from "@anthropic-ai/claude-agent-sdk";
549 import { z } from "zod";
550 
551 tool(
552 "fetch_image",
553 "Fetch an image from a URL and return it to Claude",
554 {
555 url: z.string().url()
556 },
557 async (args) => {
558 const response = await fetch(args.url); // Fetch the image bytes
559 const buffer = Buffer.from(await response.arrayBuffer()); // Read into a Buffer for base64 encoding
560 const mimeType = response.headers.get("content-type") ?? "image/png";
561 
562 return {
563 content: [
564 {
565 type: "image",
566 data: buffer.toString("base64"), // Base64-encode the raw bytes
567 mimeType
568 }
505```python Python theme={null}
506import base64
507import httpx
508from claude_agent_sdk import tool
509 
510 
511# Define a tool that fetches an image from a URL and returns it to Claude
512@tool("fetch_image", "Fetch an image from a URL and return it to Claude", {"url": str})
513async def fetch_image(args):
514 async with httpx.AsyncClient() as client: # Fetch the image bytes
515 response = await client.get(args["url"])
516 
517 return {
518 "content": [
519 {
520 "type": "image",
521 "data": base64.b64encode(response.content).decode(
522 "ascii"
523 ), # Base64-encode the raw bytes
524 "mimeType": response.headers.get(
525 "content-type", "image/png"
526 ), # Read MIME type from the response
527 }
569528 ]
570 };
571 }
572 );
573 ```
574</CodeGroup>
529 }
530```
531 
532```typescript TypeScript theme={null}
533import { tool } from "@anthropic-ai/claude-agent-sdk";
534import { z } from "zod";
535 
536tool(
537 "fetch_image",
538 "Fetch an image from a URL and return it to Claude",
539 {
540 url: z.string().url()
541 },
542 async (args) => {
543 const response = await fetch(args.url); // Fetch the image bytes
544 const buffer = Buffer.from(await response.arrayBuffer()); // Read into a Buffer for base64 encoding
545 const mimeType = response.headers.get("content-type") ?? "image/png";
546 
547 return {
548 content: [
549 {
550 type: "image",
551 data: buffer.toString("base64"), // Base64-encode the raw bytes
552 mimeType
553 }
554 ]
555 };
556 }
557);
558```
575559 
576560### Resources
577561 
from line 571
587571 
588572This example shows a resource block returned from inside a tool handler. The SDK doesn't read from the example's URI, `file:///tmp/report.md`.
589573 
590<CodeGroup>
591 ```typescript TypeScript theme={null}
592 return {
593 content: [
594 {
595 type: "resource",
596 resource: {
597 uri: "file:///tmp/report.md", // Label for Claude to reference, not a path the SDK reads
598 mimeType: "text/markdown",
599 text: "# Report\n..." // The actual content, inline
574```typescript TypeScript theme={null}
575return {
576 content: [
577 {
578 type: "resource",
579 resource: {
580 uri: "file:///tmp/report.md", // Label for Claude to reference, not a path the SDK reads
581 mimeType: "text/markdown",
582 text: "# Report\n..." // The actual content, inline
583 }
584 }
585 ]
586};
587```
588 
589```python Python theme={null}
590return {
591 "content": [
592 {
593 "type": "resource",
594 "resource": {
595 "uri": "file:///tmp/report.md", # Not a path the SDK reads
596 "mimeType": "text/markdown",
597 "text": "# Report\n...", # The actual content, inline
598 },
600599 }
601 }
602600 ]
603 };
604 ```
605 
606 ```python Python theme={null}
607 return {
608 "content": [
609 {
610 "type": "resource",
611 "resource": {
612 "uri": "file:///tmp/report.md", # Not a path the SDK reads
613 "mimeType": "text/markdown",
614 "text": "# Report\n...", # The actual content, inline
615 },
616 }
617 ]
618 }
619 ```
620</CodeGroup>
601}
602```
621603 
622604These block shapes come from the MCP `CallToolResult` type. See the [MCP specification](https://modelcontextprotocol.io/specification/2025-06-18/server/tools#tool-result) for the full definition.
623605 
from line 626
644626};
645627```
646628 
647<Note>
648 The Python `@tool` decorator forwards only `content` and `is_error` from the handler's return dict. To return `structuredContent` from Python, run a [standalone MCP server](/docs/en/agent-sdk/mcp) instead of an in-process SDK server.
649</Note>
629> [!NOTE]
630>
631> The Python `@tool` decorator forwards only `content` and `is_error` from the handler's return dict. To return `structuredContent` from Python, run a [standalone MCP server](/docs/en/agent-sdk/mcp) instead of an in-process SDK server.
650632 
651633## Example: unit converter
652634 
from line 639
657639* **Enum schemas:** `unit_type` is constrained to a fixed set of values. In TypeScript, use `z.enum()`. In Python, the dict schema doesn't support enums, so the full JSON Schema dict is required.
658640* **Unsupported input handling:** when a conversion pair isn't found, the handler returns `isError: true` so Claude can tell the user what went wrong rather than treating a failure as a normal result.
659641 
660<CodeGroup>
661 ```python Python theme={null}
662 from typing import Any
663 from claude_agent_sdk import tool, create_sdk_mcp_server
664 
665 
666 # z.enum() in TypeScript becomes an "enum" constraint in JSON Schema.
667 # The dict schema has no equivalent, so full JSON Schema is required.
668 @tool(
669 "convert_units",
670 "Convert a value from one unit to another",
671 {
672 "type": "object",
673 "properties": {
674 "unit_type": {
675 "type": "string",
676 "enum": ["length", "temperature", "weight"],
677 "description": "Category of unit",
678 },
679 "from_unit": {
680 "type": "string",
681 "description": "Unit to convert from, e.g. kilometers, fahrenheit, pounds",
682 },
683 "to_unit": {"type": "string", "description": "Unit to convert to"},
684 "value": {"type": "number", "description": "Value to convert"},
685 },
686 "required": ["unit_type", "from_unit", "to_unit", "value"],
687 },
688 )
689 async def convert_units(args: dict[str, Any]) -> dict[str, Any]:
690 conversions = {
691 "length": {
692 "kilometers_to_miles": lambda v: v * 0.621371,
693 "miles_to_kilometers": lambda v: v * 1.60934,
694 "meters_to_feet": lambda v: v * 3.28084,
695 "feet_to_meters": lambda v: v * 0.3048,
696 },
697 "temperature": {
698 "celsius_to_fahrenheit": lambda v: (v * 9) / 5 + 32,
699 "fahrenheit_to_celsius": lambda v: (v - 32) * 5 / 9,
700 "celsius_to_kelvin": lambda v: v + 273.15,
701 "kelvin_to_celsius": lambda v: v - 273.15,
702 },
703 "weight": {
704 "kilograms_to_pounds": lambda v: v * 2.20462,
705 "pounds_to_kilograms": lambda v: v * 0.453592,
706 "grams_to_ounces": lambda v: v * 0.035274,
707 "ounces_to_grams": lambda v: v * 28.3495,
708 },
709 }
710 
711 key = f"{args['from_unit']}_to_{args['to_unit']}"
712 fn = conversions.get(args["unit_type"], {}).get(key)
713 
714 if not fn:
715 return {
716 "content": [
717 {
718 "type": "text",
719 "text": f"Unsupported conversion: {args['from_unit']} to {args['to_unit']}",
720 }
721 ],
722 "is_error": True,
723 }
724 
725 result = fn(args["value"])
726 return {
727 "content": [
728 {
729 "type": "text",
730 "text": f"{args['value']} {args['from_unit']} = {result:.4f} {args['to_unit']}",
731 }
732 ]
733 }
734 
735 
736 converter_server = create_sdk_mcp_server(
737 name="converter",
738 version="1.0.0",
739 tools=[convert_units],
740 )
741 ```
742 
743 ```typescript TypeScript theme={null}
744 import { tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
745 import { z } from "zod";
746 
747 const convert = tool(
642```python Python theme={null}
643from typing import Any
644from claude_agent_sdk import tool, create_sdk_mcp_server
645 
646 
647# z.enum() in TypeScript becomes an "enum" constraint in JSON Schema.
648# The dict schema has no equivalent, so full JSON Schema is required.
649@tool(
748650 "convert_units",
749651 "Convert a value from one unit to another",
750652 {
751 unit_type: z.enum(["length", "temperature", "weight"]).describe("Category of unit"),
752 from_unit: z
753 .string()
754 .describe("Unit to convert from, e.g. kilometers, fahrenheit, pounds"),
755 to_unit: z.string().describe("Unit to convert to"),
756 value: z.number().describe("Value to convert")
653 "type": "object",
654 "properties": {
655 "unit_type": {
656 "type": "string",
657 "enum": ["length", "temperature", "weight"],
658 "description": "Category of unit",
659 },
660 "from_unit": {
661 "type": "string",
662 "description": "Unit to convert from, e.g. kilometers, fahrenheit, pounds",
663 },
664 "to_unit": {"type": "string", "description": "Unit to convert to"},
665 "value": {"type": "number", "description": "Value to convert"},
666 },
667 "required": ["unit_type", "from_unit", "to_unit", "value"],
757668 },
758 async (args) => {
759 type Conversions = Record<string, Record<string, (v: number) => number>>;
760 
761 const conversions: Conversions = {
762 length: {
763 kilometers_to_miles: (v) => v * 0.621371,
764 miles_to_kilometers: (v) => v * 1.60934,
765 meters_to_feet: (v) => v * 3.28084,
766 feet_to_meters: (v) => v * 0.3048
669)
670async def convert_units(args: dict[str, Any]) -> dict[str, Any]:
671 conversions = {
672 "length": {
673 "kilometers_to_miles": lambda v: v * 0.621371,
674 "miles_to_kilometers": lambda v: v * 1.60934,
675 "meters_to_feet": lambda v: v * 3.28084,
676 "feet_to_meters": lambda v: v * 0.3048,
767677 },
768 temperature: {
769 celsius_to_fahrenheit: (v) => (v * 9) / 5 + 32,
770 fahrenheit_to_celsius: (v) => ((v - 32) * 5) / 9,
771 celsius_to_kelvin: (v) => v + 273.15,
772 kelvin_to_celsius: (v) => v - 273.15
678 "temperature": {
679 "celsius_to_fahrenheit": lambda v: (v * 9) / 5 + 32,
680 "fahrenheit_to_celsius": lambda v: (v - 32) * 5 / 9,
681 "celsius_to_kelvin": lambda v: v + 273.15,
682 "kelvin_to_celsius": lambda v: v - 273.15,
773683 },
774 weight: {
775 kilograms_to_pounds: (v) => v * 2.20462,
776 pounds_to_kilograms: (v) => v * 0.453592,
777 grams_to_ounces: (v) => v * 0.035274,
778 ounces_to_grams: (v) => v * 28.3495
684 "weight": {
685 "kilograms_to_pounds": lambda v: v * 2.20462,
686 "pounds_to_kilograms": lambda v: v * 0.453592,
687 "grams_to_ounces": lambda v: v * 0.035274,
688 "ounces_to_grams": lambda v: v * 28.3495,
689 },
690 }
691 
692 key = f"{args['from_unit']}_to_{args['to_unit']}"
693 fn = conversions.get(args["unit_type"], {}).get(key)
694 
695 if not fn:
696 return {
697 "content": [
698 {
699 "type": "text",
700 "text": f"Unsupported conversion: {args['from_unit']} to {args['to_unit']}",
701 }
702 ],
703 "is_error": True,
779704 }
780 };
781 
782 const key = `${args.from_unit}_to_${args.to_unit}`;
783 const fn = conversions[args.unit_type]?.[key];
784 
785 if (!fn) {
786 return {
787 content: [
705 
706 result = fn(args["value"])
707 return {
708 "content": [
788709 {
789 type: "text",
790 text: `Unsupported conversion: ${args.from_unit} to ${args.to_unit}`
710 "type": "text",
711 "text": f"{args['value']} {args['from_unit']} = {result:.4f} {args['to_unit']}",
791712 }
792 ],
793 isError: true
794 };
713 ]
714 }
715 
716 
717converter_server = create_sdk_mcp_server(
718 name="converter",
719 version="1.0.0",
720 tools=[convert_units],
721)
722```
723 
724```typescript TypeScript theme={null}
725import { tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
726import { z } from "zod";
727 
728const convert = tool(
729 "convert_units",
730 "Convert a value from one unit to another",
731 {
732 unit_type: z.enum(["length", "temperature", "weight"]).describe("Category of unit"),
733 from_unit: z
734 .string()
735 .describe("Unit to convert from, e.g. kilometers, fahrenheit, pounds"),
736 to_unit: z.string().describe("Unit to convert to"),
737 value: z.number().describe("Value to convert")
738 },
739 async (args) => {
740 type Conversions = Record<string, Record<string, (v: number) => number>>;
741 
742 const conversions: Conversions = {
743 length: {
744 kilometers_to_miles: (v) => v * 0.621371,
745 miles_to_kilometers: (v) => v * 1.60934,
746 meters_to_feet: (v) => v * 3.28084,
747 feet_to_meters: (v) => v * 0.3048
748 },
749 temperature: {
750 celsius_to_fahrenheit: (v) => (v * 9) / 5 + 32,
751 fahrenheit_to_celsius: (v) => ((v - 32) * 5) / 9,
752 celsius_to_kelvin: (v) => v + 273.15,
753 kelvin_to_celsius: (v) => v - 273.15
754 },
755 weight: {
756 kilograms_to_pounds: (v) => v * 2.20462,
757 pounds_to_kilograms: (v) => v * 0.453592,
758 grams_to_ounces: (v) => v * 0.035274,
759 ounces_to_grams: (v) => v * 28.3495
795760 }
796 
797 const result = fn(args.value);
761 };
762 
763 const key = `${args.from_unit}_to_${args.to_unit}`;
764 const fn = conversions[args.unit_type]?.[key];
765 
766 if (!fn) {
798767 return {
799768 content: [
800769 {
801770 type: "text",
802 text: `${args.value} ${args.from_unit} = ${result.toFixed(4)} ${args.to_unit}`
771 text: `Unsupported conversion: ${args.from_unit} to ${args.to_unit}`
803772 }
804 ]
773 ],
774 isError: true
805775 };
806776 }
807 );
808 
809 const converterServer = createSdkMcpServer({
810 name: "converter",
811 version: "1.0.0",
812 tools: [convert]
813 });
814 ```
815</CodeGroup>
777 
778 const result = fn(args.value);
779 return {
780 content: [
781 {
782 type: "text",
783 text: `${args.value} ${args.from_unit} = ${result.toFixed(4)} ${args.to_unit}`
784 }
785 ]
786 };
787 }
788);
789 
790const converterServer = createSdkMcpServer({
791 name: "converter",
792 version: "1.0.0",
793 tools: [convert]
794});
795```
816796 
817797Once the server is defined, pass it to `query` the same way as the weather example. This example sends three different prompts in a loop to show the same tool handling different unit types. For each response, it inspects `AssistantMessage` objects (which contain the tool calls Claude made during that turn) and prints each `ToolUseBlock` before printing the final `ResultMessage` text. This lets you see when Claude is using the tool versus answering from its own knowledge.
818798 
819799Because [tool search](/docs/en/agent-sdk/tool-search) is on by default, the output may also include a `ToolSearch` call as Claude loads the deferred tool schema.
820800 
821<CodeGroup>
822 ```python Python theme={null}
823 import asyncio
824 from claude_agent_sdk import (
825 query,
826 ClaudeAgentOptions,
827 ResultMessage,
828 AssistantMessage,
829 ToolUseBlock,
830 )
831 
832 
833 async def main():
834 options = ClaudeAgentOptions(
835 mcp_servers={"converter": converter_server},
836 allowed_tools=["mcp__converter__convert_units"],
837 )
838 
839 prompts = [
840 "Convert 100 kilometers to miles.",
841 "What is 72°F in Celsius?",
842 "How many pounds is 5 kilograms?",
843 ]
844 
845 for prompt in prompts:
846 try:
847 async for message in query(prompt=prompt, options=options):
848 if isinstance(message, AssistantMessage):
849 for block in message.content:
850 if isinstance(block, ToolUseBlock):
851 print(f"[tool call] {block.name}({block.input})")
852 elif isinstance(message, ResultMessage) and message.subtype == "success":
853 print(f"Q: {prompt}\nA: {message.result}\n")
854 except Exception as error:
855 # A single-shot query() raises after yielding an error result. Only success
856 # results are printed above, so handle the failure here and continue with
857 # the next prompt.
858 print(f"Call failed: {error}")
859 
860 
861 asyncio.run(main())
862 ```
863 
864 ```typescript TypeScript theme={null}
865 import { query } from "@anthropic-ai/claude-agent-sdk";
866 
867 const prompts = [
868 "Convert 100 kilometers to miles.",
869 "What is 72°F in Celsius?",
870 "How many pounds is 5 kilograms?"
871 ];
872 
873 for (const prompt of prompts) {
874 try {
875 for await (const message of query({
876 prompt,
877 options: {
878 mcpServers: { converter: converterServer },
879 allowedTools: ["mcp__converter__convert_units"]
801```python Python theme={null}
802import asyncio
803from claude_agent_sdk import (
804 query,
805 ClaudeAgentOptions,
806 ResultMessage,
807 AssistantMessage,
808 ToolUseBlock,
809)
810 
811 
812async def main():
813 options = ClaudeAgentOptions(
814 mcp_servers={"converter": converter_server},
815 allowed_tools=["mcp__converter__convert_units"],
816 )
817 
818 prompts = [
819 "Convert 100 kilometers to miles.",
820 "What is 72°F in Celsius?",
821 "How many pounds is 5 kilograms?",
822 ]
823 
824 for prompt in prompts:
825 try:
826 async for message in query(prompt=prompt, options=options):
827 if isinstance(message, AssistantMessage):
828 for block in message.content:
829 if isinstance(block, ToolUseBlock):
830 print(f"[tool call] {block.name}({block.input})")
831 elif isinstance(message, ResultMessage) and message.subtype == "success":
832 print(f"Q: {prompt}\nA: {message.result}\n")
833 except Exception as error:
834 # A single-shot query() raises after yielding an error result. Only success
835 # results are printed above, so handle the failure here and continue with
836 # the next prompt.
837 print(f"Call failed: {error}")
838 
839 
840asyncio.run(main())
841```
842 
843```typescript TypeScript theme={null}
844import { query } from "@anthropic-ai/claude-agent-sdk";
845 
846const prompts = [
847 "Convert 100 kilometers to miles.",
848 "What is 72°F in Celsius?",
849 "How many pounds is 5 kilograms?"
850];
851 
852for (const prompt of prompts) {
853 try {
854 for await (const message of query({
855 prompt,
856 options: {
857 mcpServers: { converter: converterServer },
858 allowedTools: ["mcp__converter__convert_units"]
859 }
860 })) {
861 if (message.type === "assistant") {
862 for (const block of message.message.content) {
863 if (block.type === "tool_use") {
864 console.log(`[tool call] ${block.name}`, block.input);
865 }
880866 }
881 })) {
882 if (message.type === "assistant") {
883 for (const block of message.message.content) {
884 if (block.type === "tool_use") {
885 console.log(`[tool call] ${block.name}`, block.input);
886 }
887 }
888 } else if (message.type === "result" && message.subtype === "success") {
889 console.log(`Q: ${prompt}\nA: ${message.result}\n`);
890 }
867 } else if (message.type === "result" && message.subtype === "success") {
868 console.log(`Q: ${prompt}\nA: ${message.result}\n`);
891869 }
892 } catch (error) {
893 // A single-shot query() throws after yielding an error result. Only success
894 // results are logged above, so handle the failure here and continue with
895 // the next prompt.
896 console.error(`Call failed: ${error}`);
897 }
870 }
871 } catch (error) {
872 // A single-shot query() throws after yielding an error result. Only success
873 // results are logged above, so handle the failure here and continue with
874 // the next prompt.
875 console.error(`Call failed: ${error}`);
898876 }
899 ```
900</CodeGroup>
877}
878```
901879 
902880## Next steps
903881 
904882 
Feedback