agent-sdk/custom-tools.md +833 −0 created
1> ## Documentation Index
2> Fetch the complete documentation index at: https://code.claude.com/docs/llms.txt
3> Use this file to discover all available pages before exploring further.
4
5# Dale a Claude herramientas personalizadas
6
7> Define herramientas personalizadas con el servidor MCP en proceso del SDK del Agente Claude para que Claude pueda llamar a sus funciones, acceder a sus APIs y realizar operaciones específicas del dominio.
8
9Las herramientas personalizadas extienden el SDK del Agente permitiéndole definir sus propias funciones que Claude puede llamar durante una conversación. Usando el servidor MCP en proceso del SDK, puede dar a Claude acceso a bases de datos, APIs externas, lógica específica del dominio u cualquier otra capacidad que su aplicación necesite.
10
11Esta guía cubre cómo definir herramientas con esquemas de entrada y controladores, agruparlas en un servidor MCP, pasarlas a `query` y controlar a qué herramientas puede acceder Claude. También cubre manejo de errores, anotaciones de herramientas y devolución de contenido no textual como imágenes.
12
13## Referencia rápida
14
15| Si desea... | Haga esto |
16| :-------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
17| Definir una herramienta | Use [`@tool`](/es/agent-sdk/python#tool) (Python) o [`tool()`](/es/agent-sdk/typescript#tool) (TypeScript) con un nombre, descripción, esquema y controlador. Vea [Crear una herramienta personalizada](#crear-una-herramienta-personalizada). |
18| Registrar una herramienta con Claude | Envuelva en `create_sdk_mcp_server` / `createSdkMcpServer` y pase a `mcpServers` en `query()`. Vea [Llamar a una herramienta personalizada](#llamar-a-una-herramienta-personalizada). |
19| Preaprobación de una herramienta | Agregue a sus herramientas permitidas. Vea [Configurar herramientas permitidas](#configurar-herramientas-permitidas). |
20| Eliminar una herramienta integrada del contexto de Claude | Pase un array `tools` listando solo los integrados que desea. Vea [Configurar herramientas permitidas](#configurar-herramientas-permitidas). |
21| Permitir que Claude llame herramientas en paralelo | Establezca `readOnlyHint: true` en herramientas sin efectos secundarios. Vea [Agregar anotaciones de herramientas](#agregar-anotaciones-de-herramientas). |
22| Manejar errores sin detener el bucle | Devuelva `isError: true` en lugar de lanzar una excepción. Vea [Manejar errores](#manejar-errores). |
23| Devolver imágenes o archivos | Use bloques `image` o `resource` en el array de contenido. Vea [Devolver imágenes y recursos](#devolver-imágenes-y-recursos). |
24| Devolver un resultado JSON legible por máquina | Establezca `structuredContent` en el resultado. Vea [Devolver datos estructurados](#devolver-datos-estructurados). |
25| Escalar a muchas herramientas | Use [búsqueda de herramientas](/es/agent-sdk/tool-search) para cargar herramientas bajo demanda. |
26
27## Crear una herramienta personalizada
28
29Una herramienta se define por cuatro partes, pasadas como argumentos al ayudante [`tool()`](/es/agent-sdk/typescript#tool) en TypeScript o al decorador [`@tool`](/es/agent-sdk/python#tool) en Python:
30
31* **Nombre:** un identificador único que Claude usa para llamar a la herramienta.
32* **Descripción:** qué hace la herramienta. Claude lee esto para decidir cuándo llamarla.
33* **Esquema de entrada:** los argumentos que Claude debe proporcionar. En TypeScript esto es siempre un [esquema Zod](https://zod.dev/), y los `args` del controlador se tipan automáticamente desde él. En Python esto es un diccionario que mapea nombres a tipos, como `{"latitude": float}`, que el SDK convierte a JSON Schema para usted. El decorador de Python también acepta un diccionario completo de [JSON Schema](https://json-schema.org/understanding-json-schema/about) directamente cuando necesita enumeraciones, rangos, campos opcionales u objetos anidados.
34* **Controlador:** la función asincrónica que se ejecuta cuando Claude llama a la herramienta. Recibe los argumentos validados y debe devolver un objeto con:
35 * `content` (requerido): un array de bloques de resultado, cada uno con un `type` de `"text"`, `"image"` o `"resource"`. Vea [Devolver imágenes y recursos](#devolver-imágenes-y-recursos) para bloques no textuales.
36 * `structuredContent` (opcional): un objeto JSON que contiene el resultado como datos legibles por máquina, devuelto junto a `content`. Vea [Devolver datos estructurados](#devolver-datos-estructurados).
37 * `isError` (opcional): establezca en `true` para señalar un fallo de herramienta para que Claude pueda reaccionar a él. Vea [Manejar errores](#manejar-errores).
38
39Después de definir una herramienta, envuélvala en un servidor con [`createSdkMcpServer`](/es/agent-sdk/typescript#createsdkmcpserver) (TypeScript) o [`create_sdk_mcp_server`](/es/agent-sdk/python#create_sdk_mcp_server) (Python). El servidor se ejecuta en proceso dentro de su aplicación, no como un proceso separado.
40
41### Ejemplo de herramienta meteorológica
42
43Este ejemplo define una herramienta `get_temperature` y la envuelve en un servidor MCP. Solo configura la herramienta; para pasarla a `query` y ejecutarla, vea [Llamar a una herramienta personalizada](#llamar-a-una-herramienta-personalizada) abajo.
44
45<CodeGroup>
46 ```python Python theme={null}
47 from typing import Any
48 import httpx
49 from claude_agent_sdk import tool, create_sdk_mcp_server
50
51
52 # Define a tool: name, description, input schema, handler
53 @tool(
54 "get_temperature",
55 "Get the current temperature at a location",
56 {"latitude": float, "longitude": float},
57 )
58 async def get_temperature(args: dict[str, Any]) -> dict[str, Any]:
59 async with httpx.AsyncClient() as client:
60 response = await client.get(
61 "https://api.open-meteo.com/v1/forecast",
62 params={
63 "latitude": args["latitude"],
64 "longitude": args["longitude"],
65 "current": "temperature_2m",
66 "temperature_unit": "fahrenheit",
67 },
68 )
69 data = response.json()
70
71 # Return a content array - Claude sees this as the tool result
72 return {
73 "content": [
74 {
75 "type": "text",
76 "text": f"Temperature: {data['current']['temperature_2m']}°F",
77 }
78 ]
79 }
80
81
82 # Wrap the tool in an in-process MCP server
83 weather_server = create_sdk_mcp_server(
84 name="weather",
85 version="1.0.0",
86 tools=[get_temperature],
87 )
88 ```
89
90 ```typescript TypeScript theme={null}
91 import { tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
92 import { z } from "zod";
93
94 // Define a tool: name, description, input schema, handler
95 const getTemperature = tool(
96 "get_temperature",
97 "Get the current temperature at a location",
98 {
99 latitude: z.number().describe("Latitude coordinate"), // .describe() adds a field description Claude sees
100 longitude: z.number().describe("Longitude coordinate")
101 },
102 async (args) => {
103 // args is typed from the schema: { latitude: number; longitude: number }
104 const response = await fetch(
105 `https://api.open-meteo.com/v1/forecast?latitude=${args.latitude}&longitude=${args.longitude}¤t=temperature_2m&temperature_unit=fahrenheit`
106 );
107 const data: any = await response.json();
108
109 // Return a content array - Claude sees this as the tool result
110 return {
111 content: [{ type: "text", text: `Temperature: ${data.current.temperature_2m}°F` }]
112 };
113 }
114 );
115
116 // Wrap the tool in an in-process MCP server
117 const weatherServer = createSdkMcpServer({
118 name: "weather",
119 version: "1.0.0",
120 tools: [getTemperature]
121 });
122 ```
123</CodeGroup>
124
125Vea la referencia de TypeScript [`tool()`](/es/agent-sdk/typescript#tool) o la referencia de Python [`@tool`](/es/agent-sdk/python#tool) para detalles completos de parámetros, incluyendo formatos de entrada JSON Schema y estructura de valor de retorno.
126
127<Tip>
128 Para hacer un parámetro opcional: en TypeScript, agregue `.default()` al campo Zod. En Python, el esquema dict trata cada clave como requerida, así que deje el parámetro fuera del esquema, menciónelo en la cadena de descripción y léalo con `args.get()` en el controlador. La herramienta [`get_precipitation_chance` abajo](#agregar-más-herramientas) muestra ambos patrones.
129</Tip>
130
131### Llamar a una herramienta personalizada
132
133Pase el servidor MCP que creó a `query` a través de la opción `mcpServers`. La clave en `mcpServers` se convierte en el segmento `{server_name}` en el nombre completamente calificado de cada herramienta: `mcp__{server_name}__{tool_name}`. Liste ese nombre en `allowedTools` para que la herramienta se ejecute sin un aviso de permiso.
134
135Estos fragmentos reutilizan el `weatherServer` del [ejemplo anterior](#ejemplo-de-herramienta-meteorológica) para preguntarle a Claude cuál es el clima en una ubicación específica.
136
137<CodeGroup>
138 ```python Python theme={null}
139 import asyncio
140 from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
141
142
143 async def main():
144 options = ClaudeAgentOptions(
145 mcp_servers={"weather": weather_server},
146 allowed_tools=["mcp__weather__get_temperature"],
147 )
148
149 async for message in query(
150 prompt="What's the temperature in San Francisco?",
151 options=options,
152 ):
153 # ResultMessage is the final message after all tool calls complete
154 if isinstance(message, ResultMessage) and message.subtype == "success":
155 print(message.result)
156
157
158 asyncio.run(main())
159 ```
160
161 ```typescript TypeScript theme={null}
162 import { query } from "@anthropic-ai/claude-agent-sdk";
163
164 for await (const message of query({
165 prompt: "What's the temperature in San Francisco?",
166 options: {
167 mcpServers: { weather: weatherServer },
168 allowedTools: ["mcp__weather__get_temperature"]
169 }
170 })) {
171 // "result" is the final message after all tool calls complete
172 if (message.type === "result" && message.subtype === "success") {
173 console.log(message.result);
174 }
175 }
176 ```
177</CodeGroup>
178
179### Agregar más herramientas
180
181Un servidor contiene tantas herramientas como liste en su array `tools`. Con más de una herramienta en un servidor, puede listar cada una en `allowedTools` individualmente o usar el comodín `mcp__weather__*` para cubrir cada herramienta que el servidor expone.
182
183El ejemplo abajo agrega una segunda herramienta, `get_precipitation_chance`, al `weatherServer` del [ejemplo de herramienta meteorológica](#ejemplo-de-herramienta-meteorológica) y lo reconstruye con ambas herramientas en el array.
184
185<CodeGroup>
186 ```python Python theme={null}
187 # Define a second tool for the same server
188 @tool(
189 "get_precipitation_chance",
190 "Get the hourly precipitation probability for a location. "
191 "Optionally pass 'hours' (1-24) to control how many hours to return.",
192 {"latitude": float, "longitude": float},
193 )
194 async def get_precipitation_chance(args: dict[str, Any]) -> dict[str, Any]:
195 # 'hours' isn't in the schema - read it with .get() to make it optional
196 hours = args.get("hours", 12)
197 async with httpx.AsyncClient() as client:
198 response = await client.get(
199 "https://api.open-meteo.com/v1/forecast",
200 params={
201 "latitude": args["latitude"],
202 "longitude": args["longitude"],
203 "hourly": "precipitation_probability",
204 "forecast_days": 1,
205 },
206 )
207 data = response.json()
208 chances = data["hourly"]["precipitation_probability"][:hours]
209
210 return {
211 "content": [
212 {
213 "type": "text",
214 "text": f"Next {hours} hours: {'%, '.join(map(str, chances))}%",
215 }
216 ]
217 }
218
219
220 # Rebuild the server with both tools in the array
221 weather_server = create_sdk_mcp_server(
222 name="weather",
223 version="1.0.0",
224 tools=[get_temperature, get_precipitation_chance],
225 )
226 ```
227
228 ```typescript TypeScript theme={null}
229 // Define a second tool for the same server
230 const getPrecipitationChance = tool(
231 "get_precipitation_chance",
232 "Get the hourly precipitation probability for a location",
233 {
234 latitude: z.number(),
235 longitude: z.number(),
236 hours: z
237 .number()
238 .int()
239 .min(1)
240 .max(24)
241 .default(12) // .default() makes the parameter optional
242 .describe("How many hours of forecast to return")
243 },
244 async (args) => {
245 const response = await fetch(
246 `https://api.open-meteo.com/v1/forecast?latitude=${args.latitude}&longitude=${args.longitude}&hourly=precipitation_probability&forecast_days=1`
247 );
248 const data: any = await response.json();
249 const chances = data.hourly.precipitation_probability.slice(0, args.hours);
250
251 return {
252 content: [{ type: "text", text: `Next ${args.hours} hours: ${chances.join("%, ")}%` }]
253 };
254 }
255 );
256
257 // Rebuild the server with both tools in the array
258 const weatherServer = createSdkMcpServer({
259 name: "weather",
260 version: "1.0.0",
261 tools: [getTemperature, getPrecipitationChance]
262 });
263 ```
264</CodeGroup>
265
266Cada herramienta en este array consume espacio de ventana de contexto en cada turno. Si está definiendo docenas de herramientas, vea [búsqueda de herramientas](/es/agent-sdk/tool-search) para cargarlas bajo demanda en su lugar.
267
268### Agregar anotaciones de herramientas
269
270Las [anotaciones de herramientas](https://modelcontextprotocol.io/docs/concepts/tools#tool-annotations) son metadatos opcionales que describen cómo se comporta una herramienta. Páselas como el quinto argumento al ayudante `tool()` en TypeScript o a través del argumento de palabra clave `annotations` para el decorador `@tool` en Python. Todos los campos de sugerencia son booleanos.
271
272| Campo | Predeterminado | Significado |
273| :---------------- | :------------- | :-------------------------------------------------------------------------------------------------------------------------------------- |
274| `readOnlyHint` | `false` | La herramienta no modifica su entorno. Controla si la herramienta puede ser llamada en paralelo con otras herramientas de solo lectura. |
275| `destructiveHint` | `true` | La herramienta puede realizar actualizaciones destructivas. Solo informativo. |
276| `idempotentHint` | `false` | Las llamadas repetidas con los mismos argumentos no tienen efecto adicional. Solo informativo. |
277| `openWorldHint` | `true` | La herramienta alcanza sistemas fuera de su proceso. Solo informativo. |
278
279Las anotaciones son metadatos, no aplicación. Una herramienta marcada como `readOnlyHint: true` aún puede escribir en disco si eso es lo que hace el controlador. Mantenga la anotación precisa con respecto al controlador.
280
281Este ejemplo agrega `readOnlyHint` a la herramienta `get_temperature` del [ejemplo de herramienta meteorológica](#ejemplo-de-herramienta-meteorológica).
282
283<CodeGroup>
284 ```python Python theme={null}
285 from claude_agent_sdk import tool, ToolAnnotations
286
287
288 @tool(
289 "get_temperature",
290 "Get the current temperature at a location",
291 {"latitude": float, "longitude": float},
292 annotations=ToolAnnotations(
293 readOnlyHint=True
294 ), # Lets Claude batch this with other read-only calls
295 )
296 async def get_temperature(args):
297 return {"content": [{"type": "text", "text": "..."}]}
298 ```
299
300 ```typescript TypeScript theme={null}
301 tool(
302 "get_temperature",
303 "Get the current temperature at a location",
304 { latitude: z.number(), longitude: z.number() },
305 async (args) => ({ content: [{ type: "text", text: `...` }] }),
306 { annotations: { readOnlyHint: true } } // Lets Claude batch this with other read-only calls
307 );
308 ```
309</CodeGroup>
310
311Vea `ToolAnnotations` en la referencia de [TypeScript](/es/agent-sdk/typescript#toolannotations) o [Python](/es/agent-sdk/python#toolannotations).
312
313## Controlar el acceso a herramientas
314
315El [ejemplo de herramienta meteorológica](#ejemplo-de-herramienta-meteorológica) registró un servidor y listó herramientas en `allowedTools`. Esta sección cubre cómo se construyen los nombres de herramientas y cómo limitar el acceso cuando tiene múltiples herramientas o desea restringir integrados.
316
317### Formato de nombre de herramienta
318
319Cuando las herramientas MCP se exponen a Claude, sus nombres siguen un formato específico:
320
321* Patrón: `mcp__{server_name}__{tool_name}`
322* Ejemplo: Una herramienta nombrada `get_temperature` en servidor `weather` se convierte en `mcp__weather__get_temperature`
323
324### Configurar herramientas permitidas
325
326La opción `tools` y las listas permitidas/no permitidas operan en capas separadas. `tools` controla qué herramientas integradas aparecen en el contexto de Claude. Las listas de herramientas permitidas y no permitidas controlan si las llamadas se aprueban o se deniegan una vez que Claude intenta hacerlas.
327
328| Opción | Capa | Efecto |
329| :------------------------- | :------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
330| `tools: ["Read", "Grep"]` | Disponibilidad | Solo los integrados listados están en el contexto de Claude. Los integrados no listados se eliminan. Las herramientas MCP no se ven afectadas. |
331| `tools: []` | Disponibilidad | Todos los integrados se eliminan. Claude solo puede usar sus herramientas MCP. |
332| herramientas permitidas | Permiso | Las herramientas listadas se ejecutan sin un aviso de permiso. Las herramientas no listadas permanecen disponibles; las llamadas pasan por el [flujo de permiso](/es/agent-sdk/permissions). |
333| herramientas no permitidas | Permiso | Cada llamada a una herramienta listada se deniega. La herramienta permanece en el contexto de Claude, por lo que Claude aún puede intentarla antes de que la llamada sea rechazada. |
334
335Para limitar qué integrados puede usar Claude, prefiera `tools` sobre herramientas no permitidas. Omitir una herramienta de `tools` la elimina del contexto para que Claude nunca la intente; listarla en `disallowedTools` (Python: `disallowed_tools`) bloquea la llamada pero deja la herramienta visible, por lo que Claude puede desperdiciar un turno intentándola. Vea [Configurar permisos](/es/agent-sdk/permissions) para el orden de evaluación completo.
336
337## Manejar errores
338
339Cómo su controlador reporta errores determina si el bucle del agente continúa o se detiene:
340
341| Qué sucede | Resultado |
342| :-------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------- |
343| El controlador lanza una excepción no capturada | El bucle del agente se detiene. Claude nunca ve el error, y la llamada `query` falla. |
344| El controlador captura el error y devuelve `isError: true` (TS) / `"is_error": True` (Python) | El bucle del agente continúa. Claude ve el error como datos y puede reintentar, intentar una herramienta diferente o explicar el fallo. |
345
346El ejemplo abajo captura dos tipos de fallos dentro del controlador en lugar de dejarlos lanzar. Un estado HTTP no 200 se captura de la respuesta y se devuelve como un resultado de error. Un error de red o JSON inválido se captura por el `try/except` (Python) o `try/catch` (TypeScript) circundante y también se devuelve como un resultado de error. En ambos casos el controlador devuelve normalmente y el bucle del agente continúa.
347
348<CodeGroup>
349 ```python Python theme={null}
350 import json
351 import httpx
352 from typing import Any
353
354
355 @tool(
356 "fetch_data",
357 "Fetch data from an API",
358 {"endpoint": str}, # Simple schema
359 )
360 async def fetch_data(args: dict[str, Any]) -> dict[str, Any]:
361 try:
362 async with httpx.AsyncClient() as client:
363 response = await client.get(args["endpoint"])
364 if response.status_code != 200:
365 # Return the failure as a tool result so Claude can react to it.
366 # is_error marks this as a failed call rather than odd-looking data.
367 return {
368 "content": [
369 {
370 "type": "text",
371 "text": f"API error: {response.status_code} {response.reason_phrase}",
372 }
373 ],
374 "is_error": True,
375 }
376
377 data = response.json()
378 return {"content": [{"type": "text", "text": json.dumps(data, indent=2)}]}
379 except Exception as e:
380 # Catching here keeps the agent loop alive. An uncaught exception
381 # would end the whole query() call.
382 return {
383 "content": [{"type": "text", "text": f"Failed to fetch data: {str(e)}"}],
384 "is_error": True,
385 }
386 ```
387
388 ```typescript TypeScript theme={null}
389 tool(
390 "fetch_data",
391 "Fetch data from an API",
392 {
393 endpoint: z.string().url().describe("API endpoint URL")
394 },
395 async (args) => {
396 try {
397 const response = await fetch(args.endpoint);
398
399 if (!response.ok) {
400 // Return the failure as a tool result so Claude can react to it.
401 // isError marks this as a failed call rather than odd-looking data.
402 return {
403 content: [
404 {
405 type: "text",
406 text: `API error: ${response.status} ${response.statusText}`
407 }
408 ],
409 isError: true
410 };
411 }
412
413 const data = await response.json();
414 return {
415 content: [
416 {
417 type: "text",
418 text: JSON.stringify(data, null, 2)
419 }
420 ]
421 };
422 } catch (error) {
423 // Catching here keeps the agent loop alive. An uncaught throw
424 // would end the whole query() call.
425 return {
426 content: [
427 {
428 type: "text",
429 text: `Failed to fetch data: ${error instanceof Error ? error.message : String(error)}`
430 }
431 ],
432 isError: true
433 };
434 }
435 }
436 );
437 ```
438</CodeGroup>
439
440## Devolver imágenes y recursos
441
442El array `content` en un resultado de herramienta acepta bloques `text`, `image` y `resource`. Puede mezclarlos en la misma respuesta.
443
444### Imágenes
445
446Un bloque de imagen lleva los bytes de imagen en línea, codificados como base64. No hay campo de URL. Para devolver una imagen que vive en una URL, búsquela en el controlador, lea los bytes de respuesta y codifíquelos en base64 antes de devolver. El resultado se procesa como entrada visual.
447
448| Campo | Tipo | Notas |
449| :--------- | :-------- | :------------------------------------------------------------------------------------------ |
450| `type` | `"image"` | |
451| `data` | `string` | Bytes codificados en base64. Solo base64 sin procesar, sin prefijo `data:image/...;base64,` |
452| `mimeType` | `string` | Requerido. Por ejemplo `image/png`, `image/jpeg`, `image/webp`, `image/gif` |
453
454<CodeGroup>
455 ```python Python theme={null}
456 import base64
457 import httpx
458
459
460 # Define a tool that fetches an image from a URL and returns it to Claude
461 @tool("fetch_image", "Fetch an image from a URL and return it to Claude", {"url": str})
462 async def fetch_image(args):
463 async with httpx.AsyncClient() as client: # Fetch the image bytes
464 response = await client.get(args["url"])
465
466 return {
467 "content": [
468 {
469 "type": "image",
470 "data": base64.b64encode(response.content).decode(
471 "ascii"
472 ), # Base64-encode the raw bytes
473 "mimeType": response.headers.get(
474 "content-type", "image/png"
475 ), # Read MIME type from the response
476 }
477 ]
478 }
479 ```
480
481 ```typescript TypeScript theme={null}
482 tool(
483 "fetch_image",
484 "Fetch an image from a URL and return it to Claude",
485 {
486 url: z.string().url()
487 },
488 async (args) => {
489 const response = await fetch(args.url); // Fetch the image bytes
490 const buffer = Buffer.from(await response.arrayBuffer()); // Read into a Buffer for base64 encoding
491 const mimeType = response.headers.get("content-type") ?? "image/png";
492
493 return {
494 content: [
495 {
496 type: "image",
497 data: buffer.toString("base64"), // Base64-encode the raw bytes
498 mimeType
499 }
500 ]
501 };
502 }
503 );
504 ```
505</CodeGroup>
506
507### Recursos
508
509Un bloque de recurso incrusta un contenido identificado por un URI. El URI es una etiqueta para que Claude la referencie; el contenido real va en el campo `text` o `blob` del bloque. Use esto cuando su herramienta produce algo que tiene sentido direccionar por nombre más tarde, como un archivo generado o un registro de un sistema externo.
510
511| Campo | Tipo | Notas |
512| :------------------ | :----------- | :------------------------------------------------------------- |
513| `type` | `"resource"` | |
514| `resource.uri` | `string` | Identificador para el contenido. Cualquier esquema de URI |
515| `resource.text` | `string` | El contenido, si es texto. Proporcione esto o `blob`, no ambos |
516| `resource.blob` | `string` | El contenido codificado en base64, si es binario |
517| `resource.mimeType` | `string` | Opcional |
518
519Este ejemplo muestra un bloque de recurso devuelto desde dentro de un controlador de herramienta. El URI `file:///tmp/report.md` es una etiqueta que Claude puede referenciar más tarde; el SDK no lee desde esa ruta.
520
521<CodeGroup>
522 ```typescript TypeScript theme={null}
523 return {
524 content: [
525 {
526 type: "resource",
527 resource: {
528 uri: "file:///tmp/report.md", // Label for Claude to reference, not a path the SDK reads
529 mimeType: "text/markdown",
530 text: "# Report\n..." // The actual content, inline
531 }
532 }
533 ]
534 };
535 ```
536
537 ```python Python theme={null}
538 return {
539 "content": [
540 {
541 "type": "resource",
542 "resource": {
543 "uri": "file:///tmp/report.md", # Label for Claude to reference, not a path the SDK reads
544 "mimeType": "text/markdown",
545 "text": "# Report\n...", # The actual content, inline
546 },
547 }
548 ]
549 }
550 ```
551</CodeGroup>
552
553Estas formas de bloque provienen del tipo MCP `CallToolResult`. Vea la [especificación MCP](https://modelcontextprotocol.io/specification/2025-06-18/server/tools#tool-result) para la definición completa.
554
555## Devolver datos estructurados
556
557`structuredContent` es un objeto JSON opcional en el resultado, separado del array `content`. Úselo para devolver valores sin procesar que Claude pueda leer como campos exactos en lugar de analizarlos de una cadena de texto o imagen.
558
559Cuando `structuredContent` se establece, Claude recibe el JSON más cualquier bloque de imagen o recurso de `content`. Los bloques de texto en `content` no se reenvían, ya que se asume que duplican los datos estructurados. El ejemplo abajo renderiza un gráfico como un bloque de imagen y devuelve los puntos de datos detrás de él en `structuredContent` del mismo controlador.
560
561```typescript TypeScript theme={null}
562return {
563 content: [
564 {
565 type: "image",
566 data: chartPngBuffer.toString("base64"),
567 mimeType: "image/png"
568 }
569 ],
570 structuredContent: {
571 series: "temperature_2m",
572 unit: "fahrenheit",
573 points: [62.1, 63.4, 65.0, 64.2]
574 }
575};
576```
577
578<Note>
579 El decorador `@tool` de Python reenvía solo `content` e `is_error` del diccionario de retorno del controlador. Para devolver `structuredContent` desde Python, ejecute un [servidor MCP independiente](/es/agent-sdk/mcp) en lugar de un servidor SDK en proceso.
580</Note>
581
582## Ejemplo: convertidor de unidades
583
584Esta herramienta convierte valores entre unidades de longitud, temperatura y peso. Un usuario puede preguntar "convertir 100 kilómetros a millas" o "¿cuál es 72°F en Celsius?" y Claude elige el tipo de unidad correcto y las unidades de la solicitud.
585
586Demuestra dos patrones:
587
588* **Esquemas de enumeración:** `unit_type` está restringido a un conjunto fijo de valores. En TypeScript, use `z.enum()`. En Python, el esquema dict no admite enumeraciones, por lo que se requiere el diccionario JSON Schema completo.
589* **Manejo de entrada no admitida:** cuando no se encuentra un par de conversión, el controlador devuelve `isError: true` para que Claude pueda decirle al usuario qué salió mal en lugar de tratar un fallo como un resultado normal.
590
591<CodeGroup>
592 ```python Python theme={null}
593 from typing import Any
594 from claude_agent_sdk import tool, create_sdk_mcp_server
595
596
597 # z.enum() in TypeScript becomes an "enum" constraint in JSON Schema.
598 # The dict schema has no equivalent, so full JSON Schema is required.
599 @tool(
600 "convert_units",
601 "Convert a value from one unit to another",
602 {
603 "type": "object",
604 "properties": {
605 "unit_type": {
606 "type": "string",
607 "enum": ["length", "temperature", "weight"],
608 "description": "Category of unit",
609 },
610 "from_unit": {
611 "type": "string",
612 "description": "Unit to convert from, e.g. kilometers, fahrenheit, pounds",
613 },
614 "to_unit": {"type": "string", "description": "Unit to convert to"},
615 "value": {"type": "number", "description": "Value to convert"},
616 },
617 "required": ["unit_type", "from_unit", "to_unit", "value"],
618 },
619 )
620 async def convert_units(args: dict[str, Any]) -> dict[str, Any]:
621 conversions = {
622 "length": {
623 "kilometers_to_miles": lambda v: v * 0.621371,
624 "miles_to_kilometers": lambda v: v * 1.60934,
625 "meters_to_feet": lambda v: v * 3.28084,
626 "feet_to_meters": lambda v: v * 0.3048,
627 },
628 "temperature": {
629 "celsius_to_fahrenheit": lambda v: (v * 9) / 5 + 32,
630 "fahrenheit_to_celsius": lambda v: (v - 32) * 5 / 9,
631 "celsius_to_kelvin": lambda v: v + 273.15,
632 "kelvin_to_celsius": lambda v: v - 273.15,
633 },
634 "weight": {
635 "kilograms_to_pounds": lambda v: v * 2.20462,
636 "pounds_to_kilograms": lambda v: v * 0.453592,
637 "grams_to_ounces": lambda v: v * 0.035274,
638 "ounces_to_grams": lambda v: v * 28.3495,
639 },
640 }
641
642 key = f"{args['from_unit']}_to_{args['to_unit']}"
643 fn = conversions.get(args["unit_type"], {}).get(key)
644
645 if not fn:
646 return {
647 "content": [
648 {
649 "type": "text",
650 "text": f"Unsupported conversion: {args['from_unit']} to {args['to_unit']}",
651 }
652 ],
653 "is_error": True,
654 }
655
656 result = fn(args["value"])
657 return {
658 "content": [
659 {
660 "type": "text",
661 "text": f"{args['value']} {args['from_unit']} = {result:.4f} {args['to_unit']}",
662 }
663 ]
664 }
665
666
667 converter_server = create_sdk_mcp_server(
668 name="converter",
669 version="1.0.0",
670 tools=[convert_units],
671 )
672 ```
673
674 ```typescript TypeScript theme={null}
675 import { tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
676 import { z } from "zod";
677
678 const convert = tool(
679 "convert_units",
680 "Convert a value from one unit to another",
681 {
682 unit_type: z.enum(["length", "temperature", "weight"]).describe("Category of unit"),
683 from_unit: z
684 .string()
685 .describe("Unit to convert from, e.g. kilometers, fahrenheit, pounds"),
686 to_unit: z.string().describe("Unit to convert to"),
687 value: z.number().describe("Value to convert")
688 },
689 async (args) => {
690 type Conversions = Record<string, Record<string, (v: number) => number>>;
691
692 const conversions: Conversions = {
693 length: {
694 kilometers_to_miles: (v) => v * 0.621371,
695 miles_to_kilometers: (v) => v * 1.60934,
696 meters_to_feet: (v) => v * 3.28084,
697 feet_to_meters: (v) => v * 0.3048
698 },
699 temperature: {
700 celsius_to_fahrenheit: (v) => (v * 9) / 5 + 32,
701 fahrenheit_to_celsius: (v) => ((v - 32) * 5) / 9,
702 celsius_to_kelvin: (v) => v + 273.15,
703 kelvin_to_celsius: (v) => v - 273.15
704 },
705 weight: {
706 kilograms_to_pounds: (v) => v * 2.20462,
707 pounds_to_kilograms: (v) => v * 0.453592,
708 grams_to_ounces: (v) => v * 0.035274,
709 ounces_to_grams: (v) => v * 28.3495
710 }
711 };
712
713 const key = `${args.from_unit}_to_${args.to_unit}`;
714 const fn = conversions[args.unit_type]?.[key];
715
716 if (!fn) {
717 return {
718 content: [
719 {
720 type: "text",
721 text: `Unsupported conversion: ${args.from_unit} to ${args.to_unit}`
722 }
723 ],
724 isError: true
725 };
726 }
727
728 const result = fn(args.value);
729 return {
730 content: [
731 {
732 type: "text",
733 text: `${args.value} ${args.from_unit} = ${result.toFixed(4)} ${args.to_unit}`
734 }
735 ]
736 };
737 }
738 );
739
740 const converterServer = createSdkMcpServer({
741 name: "converter",
742 version: "1.0.0",
743 tools: [convert]
744 });
745 ```
746</CodeGroup>
747
748Una vez que el servidor se define, páselo a `query` de la misma manera que el ejemplo meteorológico. Este ejemplo envía tres indicaciones diferentes en un bucle para mostrar la misma herramienta manejando diferentes tipos de unidades. Para cada respuesta, inspecciona objetos `AssistantMessage` (que contienen las llamadas de herramienta que Claude hizo durante ese turno) e imprime cada `ToolUseBlock` antes de imprimir el texto final de `ResultMessage`. Esto le permite ver cuándo Claude está usando la herramienta versus respondiendo desde su propio conocimiento.
749
750<CodeGroup>
751 ```python Python theme={null}
752 import asyncio
753 from claude_agent_sdk import (
754 query,
755 ClaudeAgentOptions,
756 ResultMessage,
757 AssistantMessage,
758 ToolUseBlock,
759 )
760
761
762 async def main():
763 options = ClaudeAgentOptions(
764 mcp_servers={"converter": converter_server},
765 allowed_tools=["mcp__converter__convert_units"],
766 )
767
768 prompts = [
769 "Convert 100 kilometers to miles.",
770 "What is 72°F in Celsius?",
771 "How many pounds is 5 kilograms?",
772 ]
773
774 for prompt in prompts:
775 async for message in query(prompt=prompt, options=options):
776 if isinstance(message, AssistantMessage):
777 for block in message.content:
778 if isinstance(block, ToolUseBlock):
779 print(f"[tool call] {block.name}({block.input})")
780 elif isinstance(message, ResultMessage) and message.subtype == "success":
781 print(f"Q: {prompt}\nA: {message.result}\n")
782
783
784 asyncio.run(main())
785 ```
786
787 ```typescript TypeScript theme={null}
788 import { query } from "@anthropic-ai/claude-agent-sdk";
789
790 const prompts = [
791 "Convert 100 kilometers to miles.",
792 "What is 72°F in Celsius?",
793 "How many pounds is 5 kilograms?"
794 ];
795
796 for (const prompt of prompts) {
797 for await (const message of query({
798 prompt,
799 options: {
800 mcpServers: { converter: converterServer },
801 allowedTools: ["mcp__converter__convert_units"]
802 }
803 })) {
804 if (message.type === "assistant") {
805 for (const block of message.message.content) {
806 if (block.type === "tool_use") {
807 console.log(`[tool call] ${block.name}`, block.input);
808 }
809 }
810 } else if (message.type === "result" && message.subtype === "success") {
811 console.log(`Q: ${prompt}\nA: ${message.result}\n`);
812 }
813 }
814 }
815 ```
816</CodeGroup>
817
818## Próximos pasos
819
820Las herramientas personalizadas envuelven funciones asincrónicas en una interfaz estándar. Puede mezclar los patrones en esta página en el mismo servidor: un único servidor puede contener una herramienta de base de datos, una herramienta de puerta de enlace de API y un renderizador de imágenes uno al lado del otro.
821
822Desde aquí:
823
824* Si su servidor crece a docenas de herramientas, vea [búsqueda de herramientas](/es/agent-sdk/tool-search) para diferir la carga hasta que Claude las necesite.
825* Para conectarse a servidores MCP externos (sistema de archivos, GitHub, Slack) en lugar de construir los suyos propios, vea [Conectar servidores MCP](/es/agent-sdk/mcp).
826* Para controlar qué herramientas se ejecutan automáticamente versus requerir aprobación, vea [Configurar permisos](/es/agent-sdk/permissions).
827
828## Documentación relacionada
829
830* [Referencia del SDK de TypeScript](/es/agent-sdk/typescript)
831* [Referencia del SDK de Python](/es/agent-sdk/python)
832* [Documentación de MCP](https://modelcontextprotocol.io)
833* [Descripción general del SDK](/es/agent-sdk/overview)