\
CONTEXT/academy
Lección 03 · ≈ 15 min

Herramientas custom y MCP

Definir tools propias in-process y conectar servidores MCP externos.

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 en allowed_tools para 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 Done sin línea Tool:), casi siempre es que olvidaste listar mcp__dice__roll_dice en allowed_tools. Ojo: permission_mode="acceptEdits" no pre-aprueba tools MCP (solo ediciones de fichero) — para las MCP el camino es allowed_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_tools tiene que casar exactamente con la clave del dict. Si registras {"playwright": ...} pero pides mcp__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 nombre mcp__{servidor}__{tool}.

Qué llevas hasta aquí

  • Defines tools propias con @tool/tool() (nombre, descripción, esquema, handler) y las empaquetas con create_sdk_mcp_server.
  • Se las pasas a query() por mcp_servers; se llaman mcp__servidor__tool y la clave del dict es el {servidor}, que debe casar con el prefijo en allowed_tools.
  • Verificas que Claude usó tu tool con la línea Tool: mcp__dice__roll_dice del stream; recuerda que allowed_tools pre-aprueba las MCP (no acceptEdits).
  • Devuelve is_error en 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