SpyBara
Go Premium

agent-sdk/custom-tools.md 2026-09-11 23:01 UTC to 2026-09-12 03:02 UTC

This page contains 2 additions and 2 deletions.

2026
Wed 9 22:58 Sat 12 03:02 Fri 18 23:58

Dale a Claude herramientas personalizadas

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.

Las 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.

Referencia rápida

Si desea... Haga esto
Definir una herramienta Use @tool (Python) o tool() (TypeScript) con un nombre, descripción, esquema y controlador. Vea Crear una herramienta personalizada.
Registrar una herramienta con Claude Envuelva en create_sdk_mcp_server / createSdkMcpServer y pase a mcpServers en query(). Vea Llamar a una herramienta personalizada.
Preaprobación de una herramienta Agregue a sus herramientas permitidas. Vea Configurar herramientas permitidas.
Eliminar una herramienta integrada del contexto de Claude Pase un array tools listando solo los integrados que desea. Vea Configurar herramientas permitidas.
Permitir que Claude llame herramientas en paralelo Establezca readOnlyHint: true en herramientas sin efectos secundarios. Vea Agregar anotaciones de herramientas.
Controlar el mensaje de error que Claude lee Devuelva isError: true para componer el mensaje en lugar de exponer la excepción sin procesar. Vea Manejar errores.
Devolver imágenes o archivos Use bloques image o resource en el array de contenido. Vea Devolver imágenes y recursos.
Devolver un resultado JSON legible por máquina Establezca structuredContent en el resultado. Vea Devolver datos estructurados.
Escalar a muchas herramientas Use búsqueda de herramientas para cargar herramientas bajo demanda.

Crear una herramienta personalizada

Una herramienta se define por cuatro partes, pasadas como argumentos al helper tool() en TypeScript o al decorador @tool en Python:

  • Nombre: un identificador único que Claude utiliza para llamar a la herramienta.
  • Descripción: qué hace la herramienta. Claude lee esto para decidir cuándo llamarla.
  • Esquema de entrada: los argumentos que Claude debe proporcionar. En TypeScript esto es siempre un esquema Zod, y los args del manejador se tipan automáticamente a partir de él. En Python esto es un diccionario que asigna 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 directamente cuando necesita enumeraciones, rangos, campos opcionales u objetos anidados.
  • Manejador: la función asincrónica que se ejecuta cuando Claude llama a la herramienta. Recibe los argumentos validados y debe devolver un objeto con:
    • content (requerido): una matriz de bloques de resultado, cada uno con un type de "text", "image", "audio", "resource" o "resource_link". Consulte Devolver imágenes y recursos para bloques que no sean texto.
    • structuredContent (opcional): un objeto JSON que contiene el resultado como datos legibles por máquina, devuelto junto con content. Consulte Devolver datos estructurados.
    • isError (opcional): establézcalo en true para señalar un fallo de herramienta para que Claude pueda reaccionar. Consulte Manejar errores.

Después de definir una herramienta, envuélvala en un servidor con createSdkMcpServer (TypeScript) o create_sdk_mcp_server (Python). El servidor se ejecuta en el proceso dentro de su aplicación, no como un proceso separado.

Ejemplo de herramienta meteorológica

Este ejemplo define una herramienta get_temperature y la envuelve en un servidor MCP. Solo configura la herramienta; para pasarla a query y ejecutarla, consulte Llamar a una herramienta personalizada a continuación.

from typing import Any
import httpx
from claude_agent_sdk import tool, create_sdk_mcp_server


# Define a tool: name, description, input schema, handler
@tool(
"get_temperature",
"Get the current temperature at a location",
{"latitude": float, "longitude": float},
)
async def get_temperature(args: dict[str, Any]) -> dict[str, Any]:
async with httpx.AsyncClient() as client:
response = await client.get(
"https://api.open-meteo.com/v1/forecast",
params={
"latitude": args["latitude"],
"longitude": args["longitude"],
"current": "temperature_2m",
"temperature_unit": "fahrenheit",
},
)
data = response.json()

# Return a content array - Claude sees this as the tool result
return {
"content": [
{
"type": "text",
"text": f"Temperature: {data['current']['temperature_2m']}°F",
}
]
}


# Wrap the tool in an in-process MCP server
weather_server = create_sdk_mcp_server(
name="weather",
version="1.0.0",
tools=[get_temperature],
)

Consulte la referencia de TypeScript tool() o la referencia de Python @tool para obtener detalles completos de parámetros, incluidos formatos de entrada de JSON Schema y estructura de valores de retorno.

Llamar a una herramienta personalizada

Pase 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.

Estos fragmentos reutilizan el weatherServer del ejemplo anterior para preguntarle a Claude cuál es el clima en una ubicación específica.

import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage


async def main():
options = ClaudeAgentOptions(
mcp_servers={"weather": weather_server},
allowed_tools=["mcp__weather__get_temperature"],
)

async for message in query(
prompt="What's the temperature in San Francisco?",
options=options,
):
# ResultMessage is the final message after all tool calls complete
if isinstance(message, ResultMessage) and message.subtype == "success":
print(message.result)


asyncio.run(main())

Combine este fragmento con las definiciones de herramienta y servidor del ejemplo de herramienta meteorológica en un archivo, luego ejecútelo con python weather.py para Python o npx tsx weather.ts para TypeScript. Claude llama a get_temperature y el script imprime una respuesta de una línea con la temperatura actual en San Francisco.

Agregar más herramientas

Un servidor contiene tantas herramientas como liste en su matriz 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.

El ejemplo a continuación define una segunda herramienta, get_precipitation_chance, y reemplaza la definición de weatherServer del ejemplo de herramienta meteorológica con una que lista ambas herramientas en la matriz.

# Define a second tool for the same server
@tool(
"get_precipitation_chance",
"Get the hourly precipitation probability for a location. "
"Optionally pass 'hours' (1-24) to control how many hours to return.",
{"latitude": float, "longitude": float},
)
async def get_precipitation_chance(args: dict[str, Any]) -> dict[str, Any]:
# 'hours' isn't in the schema - read it with .get() to make it optional
hours = args.get("hours", 12)
async with httpx.AsyncClient() as client:
response = await client.get(
"https://api.open-meteo.com/v1/forecast",
params={
"latitude": args["latitude"],
"longitude": args["longitude"],
"hourly": "precipitation_probability",
"forecast_days": 1,
},
)
data = response.json()
chances = data["hourly"]["precipitation_probability"][:hours]

return {
"content": [
{
"type": "text",
"text": f"Next {hours} hours: {'%, '.join(map(str, chances))}%",
}
]
}


# Rebuild the server with both tools in the array
weather_server = create_sdk_mcp_server(
name="weather",
version="1.0.0",
tools=[get_temperature, get_precipitation_chance],
)

Búsqueda de herramientas está habilitada de forma predeterminada y difiere las herramientas MCP del SDK: Claude ve el nombre de cada herramienta en una lista compacta y carga su esquema completo bajo demanda. Con la búsqueda de herramientas deshabilitada, cada herramienta en esta matriz consume espacio de ventana de contexto en cada turno. En TypeScript, pase alwaysLoad: true en el argumento extras de tool() o en las opciones de createSdkMcpServer() para mantener el esquema completo de una herramienta en el aviso inicial.

Agregar anotaciones de herramientas

Las anotaciones de herramientas son metadatos opcionales que describen cómo se comporta una herramienta. Páselas como el quinto argumento al helper 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.

Campo Predeterminado Significado
readOnlyHint false La herramienta no modifica su entorno. Controla si la herramienta puede llamarse en paralelo con otras herramientas de solo lectura.
destructiveHint true La herramienta puede realizar actualizaciones destructivas. Solo informativo.
idempotentHint false Las llamadas repetidas con los mismos argumentos no tienen efecto adicional. Solo informativo.
openWorldHint true La herramienta alcanza sistemas fuera de su proceso. Solo informativo.

Las anotaciones son metadatos, no cumplimiento. Una herramienta marcada como readOnlyHint: true aún puede escribir en el disco si eso es lo que hace el manejador. Mantenga la anotación precisa con respecto al manejador.

Este ejemplo agrega readOnlyHint a la herramienta get_temperature del ejemplo de herramienta meteorológica.

from claude_agent_sdk import tool, ToolAnnotations


@tool(
"get_temperature",
"Get the current temperature at a location",
{"latitude": float, "longitude": float},
annotations=ToolAnnotations(
readOnlyHint=True
),  # Lets Claude batch this with other read-only calls
)
async def get_temperature(args):
return {"content": [{"type": "text", "text": "..."}]}

Consulte ToolAnnotations en la referencia de TypeScript o Python.

Controlar el acceso a herramientas

El ejemplo de herramienta meteorológica registró un servidor y enumeró herramientas en allowedTools. Esta sección cubre cómo delimitar el acceso cuando tiene varias herramientas o desea restringir las integradas. Para saber cómo se construyen los nombres de herramientas, consulte Llamar a una herramienta personalizada.

Configurar herramientas permitidas

La opción tools y las listas permitidas/no permitidas afectan dos capas: disponibilidad, que controla si una herramienta aparece en el contexto de Claude, y permiso, que controla si una llamada se aprueba una vez que Claude intenta realizarla. tools y las entradas de disallowedTools con nombre simple cambian la disponibilidad. allowedTools y las reglas de disallowedTools con alcance cambian el permiso. Si nombra una de las herramientas de seguimiento de tareas en allowedTools, Claude Code también opta por la sesión.

Opción Capa Efecto
tools: ["Read", "Grep"] Disponibilidad Solo las herramientas integradas enumeradas están en el contexto de Claude. Las herramientas integradas no enumeradas se eliminan. Las herramientas MCP no se ven afectadas.
tools: [] Disponibilidad Todas las herramientas integradas se eliminan. Claude solo puede usar sus herramientas MCP.
herramientas permitidas Permiso Las herramientas enumeradas se ejecutan sin un aviso de permiso. Otras herramientas no enumeradas permanecen disponibles; las llamadas pasan por el flujo de permiso.
herramientas no permitidas Ambas Un nombre de herramienta simple como "Bash" elimina la herramienta del contexto de Claude, lo mismo que omitirla de tools. Una regla con alcance como "Bash(rm *)" deja la herramienta en contexto y deniega solo las llamadas coincidentes como se escriben.

Para eliminar una herramienta integrada por completo, omítala de tools o enumere su nombre simple en disallowedTools (Python: disallowed_tools); ambas mantienen la herramienta fuera del contexto para que Claude nunca intente usarla. Una regla de disallowedTools con alcance bloquea las llamadas coincidentes pero deja la herramienta visible, por lo que Claude puede desperdiciar un turno intentándolo. Consulte Configurar permisos para el orden de evaluación completo.

Manejar errores

Un error del controlador no detiene el bucle del agente. El servidor MCP en proceso del SDK detecta excepciones no capturadas y las devuelve como resultados de error, por lo que la forma en que informa un error determina lo que Claude lee, no si la consulta falla:

Qué sucede Resultado
El controlador lanza una excepción no capturada El servidor MCP la convierte en un resultado de error que lleva el mensaje de excepción sin procesar. Claude ve ese mensaje y el bucle del agente continúa.
El controlador captura el error y devuelve isError: true (TS) / "is_error": True (Python) Claude ve el mensaje que compone. Puede agregar contexto que la excepción sin procesar no tiene, como qué solicitud falló o qué intentar en su lugar.

En ambos casos Claude puede reintentar, probar una herramienta diferente o explicar el fallo. Capture errores usted mismo cuando el mensaje de excepción sin procesar no sea suficiente para que Claude actúe.

El ejemplo a continuación captura dos tipos de fallos dentro del controlador y compone el mensaje de error que Claude lee. Un estado HTTP que no es 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 Claude recibe un mensaje que describe el fallo en lugar de una cadena de excepción sin procesar.

import json
import httpx
from typing import Any
from claude_agent_sdk import tool


@tool(
"fetch_data",
"Fetch data from an API",
{"endpoint": str},  # Simple schema
)
async def fetch_data(args: dict[str, Any]) -> dict[str, Any]:
try:
async with httpx.AsyncClient() as client:
response = await client.get(args["endpoint"])
if response.status_code != 200:
# Return the failure as a tool result so Claude can react to it.
# is_error marks this as a failed call rather than odd-looking data.
return {
"content": [
{
"type": "text",
"text": f"API error: {response.status_code} {response.reason_phrase}",
}
],
"is_error": True,
}

data = response.json()
return {"content": [{"type": "text", "text": json.dumps(data, indent=2)}]}
except Exception as e:
# Composes the message Claude reads. An uncaught exception would
# reach Claude as the raw str(e) with no context.
return {
"content": [{"type": "text", "text": f"Failed to fetch data: {str(e)}"}],
"is_error": True,
}

Devolver imágenes y recursos

El array content en un resultado de herramienta acepta bloques text, image, audio, resource y resource_link. Puede mezclarlos en la misma respuesta. En TypeScript, el SDK guarda bloques de audio en disco y Claude recibe un bloque de texto con la ruta del archivo guardado; en Python, el SDK elimina bloques de audio del resultado de la herramienta y registra una advertencia.

Claude recibe cada bloque de enlace de recurso como un bloque de texto que contiene el nombre, URI y descripción del enlace. En TypeScript, su aplicación también recibe los enlaces mismos como resourceLinks en el tool_use_result del mensaje del usuario; en Python, el SDK los aplana a texto antes de que la CLI vea el resultado, por lo que la clave resourceLinks de Python nunca se produce para herramientas en proceso.

Imágenes

Un bloque de imagen lleva los bytes de la 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 devolverlos. El resultado se procesa como entrada visual.

Campo Tipo Notas
type "image"
data string Bytes codificados en base64. Solo base64 sin procesar, sin prefijo data:image/...;base64,
mimeType string Requerido. Por ejemplo image/png, image/jpeg, image/webp, image/gif
import base64
import httpx
from claude_agent_sdk import tool


# Define a tool that fetches an image from a URL and returns it to Claude
@tool("fetch_image", "Fetch an image from a URL and return it to Claude", {"url": str})
async def fetch_image(args):
async with httpx.AsyncClient() as client:  # Fetch the image bytes
response = await client.get(args["url"])

return {
"content": [
{
"type": "image",
"data": base64.b64encode(response.content).decode(
"ascii"
),  # Base64-encode the raw bytes
"mimeType": response.headers.get(
"content-type", "image/png"
),  # Read MIME type from the response
}
]
}

Recursos

Un bloque de recurso incrusta un contenido identificado por un URI. El URI es una etiqueta para que Claude la referencie; el contenido real se encuentra en el campo text o blob del bloque. Úselo cuando su herramienta produce algo que tiene sentido direccionar por nombre más adelante, como un archivo generado o un registro de un sistema externo.

Campo Tipo Notas
type "resource"
resource.uri string Identificador del contenido. Cualquier esquema de URI
resource.text string El contenido, si es texto. Proporcione este o blob, no ambos
resource.blob string El contenido codificado en base64, si es binario. Solo TypeScript: el SDK de Python elimina recursos binarios del resultado de la herramienta y registra una advertencia
resource.mimeType string Opcional

Este 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 adelante; el SDK no lee desde esa ruta.

return {
content: [
{
type: "resource",
resource: {
uri: "file:///tmp/report.md", // Label for Claude to reference, not a path the SDK reads
mimeType: "text/markdown",
text: "# Report\n..." // The actual content, inline
}
}
]
};

Estas formas de bloque provienen del tipo MCP CallToolResult. Consulte la especificación de MCP para la definición completa.

Devolver datos estructurados

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.

Cuando structuredContent está configurado, 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 a continuación representa un gráfico como un bloque de imagen y devuelve los puntos de datos detrás de él en structuredContent desde el mismo controlador. En el fragmento, chartPngBuffer es un Buffer que contiene los bytes PNG renderizados.

return {
  content: [
    {
      type: "image",
      data: chartPngBuffer.toString("base64"),
      mimeType: "image/png"
    }
  ],
  structuredContent: {
    series: "temperature_2m",
    unit: "fahrenheit",
    points: [62.1, 63.4, 65.0, 64.2]
  }
};

Ejemplo: convertidor de unidades

Esta herramienta convierte valores entre unidades de longitud, temperatura y peso. Un usuario puede preguntar "convertir 100 kilómetros a millas" o "¿cuántos grados Celsius son 72°F?", y Claude elige el tipo de unidad correcto y las unidades de la solicitud.

Demuestra dos patrones:

  • 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 esquema JSON Schema completo.
  • Manejo de entrada no compatible: cuando no se encuentra un par de conversión, el controlador devuelve isError: true para que Claude pueda indicar al usuario qué salió mal en lugar de tratar un fallo como un resultado normal.
from typing import Any
from claude_agent_sdk import tool, create_sdk_mcp_server


# z.enum() in TypeScript becomes an "enum" constraint in JSON Schema.
# The dict schema has no equivalent, so full JSON Schema is required.
@tool(
"convert_units",
"Convert a value from one unit to another",
{
"type": "object",
"properties": {
"unit_type": {
"type": "string",
"enum": ["length", "temperature", "weight"],
"description": "Category of unit",
},
"from_unit": {
"type": "string",
"description": "Unit to convert from, e.g. kilometers, fahrenheit, pounds",
},
"to_unit": {"type": "string", "description": "Unit to convert to"},
"value": {"type": "number", "description": "Value to convert"},
},
"required": ["unit_type", "from_unit", "to_unit", "value"],
},
)
async def convert_units(args: dict[str, Any]) -> dict[str, Any]:
conversions = {
"length": {
"kilometers_to_miles": lambda v: v * 0.621371,
"miles_to_kilometers": lambda v: v * 1.60934,
"meters_to_feet": lambda v: v * 3.28084,
"feet_to_meters": lambda v: v * 0.3048,
},
"temperature": {
"celsius_to_fahrenheit": lambda v: (v * 9) / 5 + 32,
"fahrenheit_to_celsius": lambda v: (v - 32) * 5 / 9,
"celsius_to_kelvin": lambda v: v + 273.15,
"kelvin_to_celsius": lambda v: v - 273.15,
},
"weight": {
"kilograms_to_pounds": lambda v: v * 2.20462,
"pounds_to_kilograms": lambda v: v * 0.453592,
"grams_to_ounces": lambda v: v * 0.035274,
"ounces_to_grams": lambda v: v * 28.3495,
},
}

key = f"{args['from_unit']}_to_{args['to_unit']}"
fn = conversions.get(args["unit_type"], {}).get(key)

if not fn:
return {
"content": [
{
"type": "text",
"text": f"Unsupported conversion: {args['from_unit']} to {args['to_unit']}",
}
],
"is_error": True,
}

result = fn(args["value"])
return {
"content": [
{
"type": "text",
"text": f"{args['value']} {args['from_unit']} = {result:.4f} {args['to_unit']}",
}
]
}


converter_server = create_sdk_mcp_server(
name="converter",
version="1.0.0",
tools=[convert_units],
)

Una vez que el servidor está definido, páselo a query de la misma manera que el ejemplo del clima. Este ejemplo envía tres solicitudes diferentes en un bucle para mostrar la misma herramienta manejando diferentes tipos de unidades. Para cada respuesta, inspecciona objetos AssistantMessage (que contienen las llamadas a herramientas que Claude realizó 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 frente a responder desde su propio conocimiento.

Debido a que tool search está activado de forma predeterminada, la salida también puede incluir una llamada a ToolSearch cuando Claude carga el esquema de herramienta diferido.

import asyncio
from claude_agent_sdk import (
query,
ClaudeAgentOptions,
ResultMessage,
AssistantMessage,
ToolUseBlock,
)


async def main():
options = ClaudeAgentOptions(
mcp_servers={"converter": converter_server},
allowed_tools=["mcp__converter__convert_units"],
)

prompts = [
"Convert 100 kilometers to miles.",
"What is 72°F in Celsius?",
"How many pounds is 5 kilograms?",
]

for prompt in prompts:
try:
async for message in query(prompt=prompt, options=options):
if isinstance(message, AssistantMessage):
for block in message.content:
if isinstance(block, ToolUseBlock):
print(f"[tool call] {block.name}({block.input})")
elif isinstance(message, ResultMessage) and message.subtype == "success":
print(f"Q: {prompt}\nA: {message.result}\n")
except Exception as error:
# A single-shot query() raises after yielding an error result. Only success
# results are printed above, so handle the failure here and continue with
# the next prompt.
print(f"Call failed: {error}")


asyncio.run(main())

Próximos pasos

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 API y un renderizador de imágenes uno al lado del otro.

Desde aquí:

  • Si su servidor crece a docenas de herramientas, consulte búsqueda de herramientas para diferir su carga hasta que Claude las necesite.
  • Para conectarse a servidores MCP externos (sistema de archivos, GitHub, Slack) en lugar de construir los suyos propios, consulte Conectar servidores MCP.
  • Para controlar qué herramientas se ejecutan automáticamente frente a las que requieren aprobación, consulte Configurar permisos.