Herramientas custom y MCP
Las tools integradas (Read, Edit, Bash…) cubren mucho, pero tu agente real necesitará las tuyas: pegar a tu API, consultar tu base de datos, ejecutar lógica de tu dominio. El SDK te deja definir tools in-process —funciones de tu propio código que Claude llama.
Definir una tool
Una tool son cuatro piezas, con @tool (Python) o tool() (TypeScript): nombre, descripción (Claude la lee para decidir cuándo llamarla), esquema de entrada y handler (la función async que se ejecuta).
Para que puedas correrlo de extremo a extremo sin credenciales ni API externa, la tool de ejemplo es autónoma: tira dados. Crea dice_agent.py (o .ts):
import random
from claude_agent_sdk import tool, create_sdk_mcp_server
@tool(
"roll_dice",
"Tira N dados de C caras y devuelve cada resultado y la suma",
{"count": int, "sides": int},
)
async def roll_dice(args):
rolls = [random.randint(1, args["sides"]) for _ in range(args["count"])]
return {
"content": [
{"type": "text", "text": f"Tiradas: {rolls} · suma: {sum(rolls)}"}
]
}
# Empaqueta la tool en un servidor MCP in-process
dice_server = create_sdk_mcp_server(
name="dice", version="1.0.0", tools=[roll_dice],
)
import { tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
import { z } from "zod";
const rollDice = tool(
"roll_dice",
"Tira N dados de C caras y devuelve cada resultado y la suma",
{ count: z.number().int(), sides: z.number().int() },
async (args) => {
const rolls = Array.from(
{ length: args.count },
() => Math.floor(Math.random() * args.sides) + 1
);
const sum = rolls.reduce((a, b) => a + b, 0);
return {
content: [{ type: "text", text: `Tiradas: [${rolls}] · suma: ${sum}` }],
};
}
);
const diceServer = createSdkMcpServer({
name: "dice", version: "1.0.0", tools: [rollDice],
});
El handler devuelve un content array (bloques text, image o resource). En TypeScript el esquema es un Zod; en Python, un dict de tipos.
Registrarla y correr el agente
Envuelves las tools en un servidor con create_sdk_mcp_server / createSdkMcpServer (corre dentro de tu proceso, no como proceso aparte) y se lo pasas a query() por mcp_servers. Añade esto al mismo fichero, debajo del servidor:
import asyncio
from claude_agent_sdk import (
query, ClaudeAgentOptions, AssistantMessage, ResultMessage,
)
async def main():
options = ClaudeAgentOptions(
mcp_servers={"dice": dice_server},
allowed_tools=["mcp__dice__roll_dice"], # pre-aprueba TU tool
)
async for message in query(prompt="Tira 3 dados de 6 caras.", options=options):
if isinstance(message, AssistantMessage):
for block in message.content:
if hasattr(block, "name"): # ToolUseBlock
print(f"Tool: {block.name} {block.input}")
elif hasattr(block, "text"):
print(block.text)
elif isinstance(message, ResultMessage):
print(f"Done: {message.subtype}")
asyncio.run(main())
import { query } from "@anthropic-ai/claude-agent-sdk";
for await (const message of query({
prompt: "Tira 3 dados de 6 caras.",
options: {
mcpServers: { dice: diceServer },
allowedTools: ["mcp__dice__roll_dice"], // pre-aprueba TU tool
},
})) {
if (message.type === "assistant") {
for (const block of message.message.content) {
if (block.type === "tool_use") {
console.log(`Tool: ${block.name}`, block.input);
} else if (block.type === "text") {
console.log(block.text);
}
}
} else if (message.type === "result") {
console.log(`Done: ${message.subtype}`);
}
}
Reconocerás el nombre: las tools de un servidor MCP se llaman
mcp__{servidor}__{tool}— exactamente igual que en el curso de MCP. La clave del dict (dice) es el{servidor}y el nombre de la tool (roll_dice) es el{tool}, de ahímcp__dice__roll_dice. Lístala enallowed_toolspara que corra sin prompt;mcp__dice__*cubre todas las del servidor.
Córrelo
python3 dice_agent.py # o: npx tsx dice_agent.ts
Qué verás, y qué confirma el éxito: en el stream aparece una línea Tool: mcp__dice__roll_dice {'count': 3, 'sides': 6}. Ese prefijo mcp__dice__ es la prueba de que Claude llamó a tu tool (no a una integrada), con los argumentos que dedujo del prompt. Después verás el texto con las tiradas y Done: success. Ese agente que ejecuta tu propio código es lo que has construido en esta lección.
Si en lugar de la tirada ves a Claude pidiéndote permiso en cada llamada (o un
Donesin líneaTool:), casi siempre es que olvidaste listarmcp__dice__roll_diceenallowed_tools. Ojo:permission_mode="acceptEdits"no pre-aprueba tools MCP (solo ediciones de fichero) — para las MCP el camino esallowed_tools.
Manejar errores sin romper el bucle
Un detalle importante para agentes robustos: si tu handler lanza una excepción sin capturar, el agent loop se para. Si en cambio devuelves is_error: True (Python) / isError: true (TS), Claude ve el error como dato y puede reintentar o explicar el fallo:
return {"content": [{"type": "text", "text": "API caída"}], "is_error": True}
Conectar servidores MCP externos
No todo lo construyes tú. El SDK también conecta servidores MCP externos (los del curso de MCP: Playwright, GitHub, tu base de datos) por la misma opción mcp_servers. La diferencia: en vez de un servidor in-process le pasas cómo arrancar el proceso (command + args). Aquí Playwright, y un query() que lo use:
async def main():
options = ClaudeAgentOptions(
mcp_servers={
"playwright": {"command": "npx", "args": ["@playwright/mcp@latest"]},
},
allowed_tools=["mcp__playwright__*"], # todas las tools del servidor
)
async for message in query(
prompt="Abre https://example.com y dime el título de la página.",
options=options,
):
if isinstance(message, ResultMessage) and message.subtype == "success":
print(message.result)
asyncio.run(main())
for await (const message of query({
prompt: "Abre https://example.com y dime el título de la página.",
options: {
mcpServers: {
playwright: { command: "npx", args: ["@playwright/mcp@latest"] },
},
allowedTools: ["mcp__playwright__*"], // todas las tools del servidor
},
})) {
if (message.type === "result" && message.subtype === "success") {
console.log(message.result);
}
}
Así tu agente combina tools propias in-process + servidores MCP de terceros, todo bajo el mismo control de permisos.
Cuidado con el desajuste nombre-servidor. El prefijo de
allowed_toolstiene que casar exactamente con la clave del dict. Si registras{"playwright": ...}pero pidesmcp__pw__*, ese permiso no casa con nada y Claude verá las tools pero no podrá usarlas. La clave del dict manda: es el{servidor}del nombremcp__{servidor}__{tool}.
Qué llevas hasta aquí
- Defines tools propias con
@tool/tool()(nombre, descripción, esquema, handler) y las empaquetas concreate_sdk_mcp_server. - Se las pasas a
query()pormcp_servers; se llamanmcp__servidor__tooly la clave del dict es el{servidor}, que debe casar con el prefijo enallowed_tools. - Verificas que Claude usó tu tool con la línea
Tool: mcp__dice__roll_dicedel stream; recuerda queallowed_toolspre-aprueba las MCP (noacceptEdits). - Devuelve
is_erroren vez de lanzar para que el bucle siga; y conecta también MCP externos.
En la última lección, llevar el agente a producción.
Fuente oficial: Give Claude custom tools · MCP in the SDK
¿Quieres llevar la cuenta de las lecciones que has terminado?
Entrar para guardar progreso