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
/
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}¤t=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}¤t=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
No line in this hunk matches that.