Módulo 3: Tres Primitivas — Resources, Tools, Prompts
Tools: Funciones que el Modelo Puede Invocar
Tools: Funciones que el Modelo Puede Invocar
Descripción de la cápsula
Si Resources son los ojos del modelo (le permiten ver datos), Tools son las manos — le permiten hacer cosas. Un tool es una función que el modelo puede invocar a través del MCP server para ejecutar acciones con efectos reales: crear archivos, insertar registros en una base de datos, enviar mensajes, llamar APIs externas, ejecutar cálculos.
La diferencia fundamental con Resources es que los tools tienen side effects. Cuando un tool se ejecuta, algo cambia en el mundo. Por eso los tools requieren un nivel adicional de diseño: necesitan schemas que definan sus inputs, validación de parámetros, manejo de errores, y un modelo de permisos donde el usuario aprueba la ejecución.
Tools es la primitiva más usada en MCP servers reales. Si miras cualquier MCP server popular — Filesystem, GitHub, Slack — la mayoría de sus capabilities son tools. Entender cómo diseñar e implementar tools es la habilidad más valiosa de este módulo.
¿Qué es un Tool?
Definición formal
Un Tool en MCP es una función con un nombre, una descripción, un schema de input (JSON Schema), y un handler que ejecuta la lógica. El flujo es:
- El server registra los tools disponibles con sus schemas
- El host descubre los tools y se los presenta al modelo
- El modelo decide invocar un tool basado en la conversación
- El host solicita aprobación al usuario
- El server ejecuta el tool y retorna el resultado
Definición práctica
Un tool responde a: "¿Qué acciones puede ejecutar el modelo a través de este server?"
Ejemplos de tools:
├── create_file → Crea un archivo nuevo
├── insert_user → Inserta un usuario en la database
├── send_slack_message → Envía un mensaje a Slack
├── run_query → Ejecuta una query SQL
├── deploy_app → Despliega la aplicación
└── resize_image → Redimensiona una imagen
Características clave
| Característica | Detalle |
|---|---|
| Nombre | Identificador único del tool (snake_case por convención) |
| Descripción | Texto que el modelo usa para decidir cuándo invocar el tool |
| Input Schema | JSON Schema que define los parámetros requeridos y opcionales |
| Handler | Función que ejecuta la lógica del tool |
| Side effects | Los tools pueden y suelen modificar estado |
| Aprobación | El usuario debe aprobar la ejecución (en la mayoría de hosts) |
Anatomía de un Tool
Estructura de registro
Cuando registras un tool, defines:
interface ToolDefinition {
name: string; // "create_file"
description: string; // "Crea un archivo nuevo con el contenido especificado"
inputSchema: { // JSON Schema para los inputs
type: "object";
properties: {
path: { type: "string"; description: "Ruta del archivo" };
content: { type: "string"; description: "Contenido del archivo" };
};
required: ["path", "content"];
};
}
Estructura de respuesta
Cuando un tool se ejecuta, retorna:
interface ToolResult {
content: Array<{
type: "text" | "image" | "resource";
text?: string;
data?: string; // base64 para imágenes
mimeType?: string;
}>;
isError?: boolean; // true si la ejecución falló
}
El ciclo de vida completo
1. Registro
Server: "Tengo un tool 'create_file' que acepta path y content"
2. Descubrimiento
Host al modelo: "Tienes disponible create_file(path, content)"
3. Decisión del modelo
Usuario: "Crea un archivo hello.txt con 'Hola Mundo'"
Modelo: "Voy a usar create_file con path='hello.txt' y content='Hola Mundo'"
4. Aprobación
Host al usuario: "Claude quiere ejecutar create_file. ¿Aprobar?"
Usuario: "Sí"
5. Ejecución
Host al Server: tools/call { name: "create_file", arguments: { path: "hello.txt", content: "Hola Mundo" } }
Server: *ejecuta la función* → retorna resultado
6. Respuesta
Server al Host: { content: [{ type: "text", text: "Archivo hello.txt creado exitosamente" }] }
Modelo al usuario: "He creado el archivo hello.txt con el contenido 'Hola Mundo'"
La importancia del Input Schema
El input schema no es un detalle técnico — es lo que le dice al modelo qué parámetros necesita y cómo usarlos. Un schema bien diseñado resulta en mejor uso del tool.
Schema pobre vs Schema rico
// ❌ Schema pobre — el modelo no sabe qué poner
{
properties: {
data: { type: "string" }
}
}
// ✅ Schema rico — el modelo sabe exactamente qué necesita
{
properties: {
filePath: {
type: "string",
description: "Ruta completa del archivo a crear (e.g., 'src/utils/helpers.ts')"
},
content: {
type: "string",
description: "Contenido completo del archivo"
},
overwrite: {
type: "boolean",
description: "Si true, sobreescribe archivos existentes. Default: false",
default: false
}
},
required: ["filePath", "content"]
}
Regla: Las descripciones en el schema son instrucciones para el modelo. Sé específico, incluye ejemplos, y marca claramente qué es requerido y qué es opcional.
Implementación en TypeScript
Tool básico con Zod
El SDK de TypeScript usa Zod para definir schemas — es más ergonómico que JSON Schema y provee validación automática:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
const server = new McpServer({
name: "my-server",
version: "1.0.0",
});
server.tool(
"create_file",
"Crea un archivo nuevo con el contenido especificado",
{
filePath: z.string().describe("Ruta del archivo a crear"),
content: z.string().describe("Contenido del archivo"),
overwrite: z.boolean().default(false).describe("Sobreescribir si existe"),
},
async ({ filePath, content, overwrite }) => {
const fs = await import("fs/promises");
const path = await import("path");
if (!overwrite) {
try {
await fs.access(filePath);
return {
content: [{ type: "text", text: `Error: el archivo ${filePath} ya existe. Usa overwrite: true para sobreescribir.` }],
isError: true,
};
} catch {
// El archivo no existe, podemos continuar
}
}
const dir = path.dirname(filePath);
await fs.mkdir(dir, { recursive: true });
await fs.writeFile(filePath, content, "utf-8");
return {
content: [{
type: "text",
text: `Archivo creado exitosamente: ${filePath} (${content.length} caracteres)`,
}],
};
}
);
Tool con operaciones de base de datos
server.tool(
"insert_user",
"Inserta un nuevo usuario en la base de datos",
{
name: z.string().min(1).describe("Nombre del usuario"),
email: z.string().email().describe("Email del usuario"),
role: z.enum(["admin", "user", "viewer"]).default("user").describe("Rol del usuario"),
},
async ({ name, email, role }) => {
try {
const result = await db.query(
"INSERT INTO users (name, email, role) VALUES ($1, $2, $3) RETURNING id",
[name, email, role]
);
return {
content: [{
type: "text",
text: JSON.stringify({
success: true,
userId: result.rows[0].id,
message: `Usuario '${name}' creado con rol '${role}'`,
}, null, 2),
}],
};
} catch (error) {
return {
content: [{
type: "text",
text: `Error al crear usuario: ${error instanceof Error ? error.message : "Error desconocido"}`,
}],
isError: true,
};
}
}
);
Tool con llamada a API externa
server.tool(
"send_notification",
"Envía una notificación a un canal de Slack",
{
channel: z.string().describe("Nombre del canal (e.g., '#general')"),
message: z.string().describe("Mensaje a enviar"),
urgent: z.boolean().default(false).describe("Marcar como urgente"),
},
async ({ channel, message, urgent }) => {
const prefix = urgent ? "🚨 URGENTE: " : "";
const response = await fetch("https://slack.com/api/chat.postMessage", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.SLACK_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
channel,
text: `${prefix}${message}`,
}),
});
const result = await response.json();
if (!result.ok) {
return {
content: [{ type: "text", text: `Error al enviar: ${result.error}` }],
isError: true,
};
}
return {
content: [{
type: "text",
text: `Mensaje enviado a ${channel}: "${message}"${urgent ? " (urgente)" : ""}`,
}],
};
}
);
Implementación en Python
Tool básico con FastMCP
from mcp.server.fastmcp import FastMCP
server = FastMCP("my-server")
@server.tool()
async def create_file(file_path: str, content: str, overwrite: bool = False) -> str:
"""Crea un archivo nuevo con el contenido especificado.
Args:
file_path: Ruta del archivo a crear
content: Contenido del archivo
overwrite: Si True, sobreescribe archivos existentes
"""
import os
if not overwrite and os.path.exists(file_path):
raise ValueError(f"El archivo {file_path} ya existe. Usa overwrite=True para sobreescribir.")
os.makedirs(os.path.dirname(file_path), exist_ok=True)
with open(file_path, "w") as f:
f.write(content)
return f"Archivo creado exitosamente: {file_path} ({len(content)} caracteres)"
Tool con validación
@server.tool()
async def insert_user(name: str, email: str, role: str = "user") -> str:
"""Inserta un nuevo usuario en la base de datos.
Args:
name: Nombre del usuario (mínimo 1 carácter)
email: Email válido del usuario
role: Rol del usuario (admin, user, viewer). Default: user
"""
if role not in ("admin", "user", "viewer"):
raise ValueError(f"Rol inválido: {role}. Opciones: admin, user, viewer")
if "@" not in email:
raise ValueError(f"Email inválido: {email}")
import json
user_id = await db.execute(
"INSERT INTO users (name, email, role) VALUES ($1, $2, $3) RETURNING id",
name, email, role
)
return json.dumps({
"success": True,
"userId": user_id,
"message": f"Usuario '{name}' creado con rol '{role}'",
}, indent=2)
Para tools que llaman APIs externas en Python, usa httpx (async HTTP client). El patrón es el mismo: validar inputs, ejecutar la llamada, manejar errores, retornar resultado formateado.
Patrones de diseño para Tools
Patrón 1: CRUD completo
Si expones operaciones CRUD, crea un tool por operación:
// Crear
server.tool("create_task", "Crea una nueva tarea", { ... }, async (args) => { ... });
// Leer (podría ser Resource, pero como tool permite queries complejas)
server.tool("search_tasks", "Busca tareas con filtros", { ... }, async (args) => { ... });
// Actualizar
server.tool("update_task", "Actualiza una tarea existente", { ... }, async (args) => { ... });
// Eliminar
server.tool("delete_task", "Elimina una tarea por ID", { ... }, async (args) => { ... });
Patrón 2: Tool con confirmación
Para operaciones destructivas, retorna un preview antes de ejecutar:
server.tool(
"delete_files",
"Elimina archivos que coincidan con un patrón",
{
pattern: z.string().describe("Glob pattern de archivos a eliminar"),
dryRun: z.boolean().default(true).describe("Si true, solo muestra qué se eliminaría"),
},
async ({ pattern, dryRun }) => {
const files = await glob(pattern);
if (dryRun) {
return {
content: [{
type: "text",
text: `Se eliminarían ${files.length} archivos:\n${files.join("\n")}\n\nUsa dryRun: false para ejecutar.`,
}],
};
}
for (const file of files) {
await fs.unlink(file);
}
return {
content: [{ type: "text", text: `${files.length} archivos eliminados.` }],
};
}
);
Error Handling en Tools
El manejo de errores no es un afterthought — es parte del diseño. Un tool que falla sin explicación es inútil.
Errores esperados vs inesperados
server.tool(
"get_user_data",
"Obtiene datos completos de un usuario",
{ userId: z.string().describe("ID del usuario") },
async ({ userId }) => {
try {
const user = await db.findUser(userId);
if (!user) {
// Error esperado — el usuario no existe
return {
content: [{ type: "text", text: `Usuario '${userId}' no encontrado.` }],
isError: true,
};
}
return {
content: [{ type: "text", text: JSON.stringify(user, null, 2) }],
};
} catch (error) {
// Error inesperado — fallo de DB, red, etc.
return {
content: [{
type: "text",
text: `Error interno al buscar usuario: ${error instanceof Error ? error.message : "Error desconocido"}`,
}],
isError: true,
};
}
}
);
El campo isError
Cuando un tool retorna isError: true, el modelo sabe que algo falló y puede:
- Informar al usuario del error
- Intentar con parámetros diferentes
- Sugerir acciones alternativas
Siempre usa isError: true cuando la operación no fue exitosa.
Troubleshooting
"El modelo no invoca el tool"
Causa: La descripción del tool no es clara o no coincide con lo que el usuario está pidiendo.
Solución: Mejora la descripción del tool:
// ❌ Descripción vaga
"Procesa datos"
// ✅ Descripción específica
"Inserta un nuevo registro de venta en la base de datos con cliente, producto y monto"
"Los parámetros llegan incorrectos"
Causa: El schema no tiene descripciones claras o le faltan constraints.
Solución:
// ❌ Sin descripción — el modelo adivina
{ path: z.string() }
// ✅ Con descripción y ejemplo
{ path: z.string().describe("Ruta absoluta del archivo (e.g., '/home/user/data.json')") }
"El tool falla con 'invalid arguments'"
Causa: Los argumentos no pasan la validación de Zod.
Solución:
# Usa MCP Inspector para ver exactamente qué envía el modelo
npx @modelcontextprotocol/inspector
# Verifica que el schema coincide con lo que esperas
# Campos required vs optional, tipos, defaults
"Timeout al ejecutar un tool"
Causa: La operación dentro del tool tarda demasiado.
Solución:
// Agrega timeout a operaciones externas
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 30000);
try {
const response = await fetch(url, { signal: controller.signal });
// ...
} finally {
clearTimeout(timeoutId);
}
"El tool se ejecuta pero el resultado no se muestra"
Causa: El formato del resultado no es correcto.
Solución:
// Asegúrate de retornar la estructura correcta
return {
content: [
{
type: "text", // ← type es requerido
text: "resultado" // ← text es requerido para type: "text"
}
],
};
Ejercicios
Ejercicio 1: Diseñar schemas de tools (Fácil)
Diseña el input schema (nombre, descripción, parámetros) para estos 3 tools:
- Un tool que convierte temperaturas entre Celsius y Fahrenheit
- Un tool que cuenta palabras en un texto
- Un tool que genera un password seguro
Ver solución
// 1. Convertir temperatura
server.tool(
"convert_temperature",
"Convierte temperatura entre Celsius y Fahrenheit",
{
value: z.number().describe("Valor de temperatura a convertir"),
from: z.enum(["celsius", "fahrenheit"]).describe("Unidad de origen"),
},
async ({ value, from }) => {
const result = from === "celsius"
? { fahrenheit: (value * 9/5) + 32 }
: { celsius: (value - 32) * 5/9 };
return { content: [{ type: "text", text: JSON.stringify(result) }] };
}
);
// 2. Contar palabras
server.tool(
"count_words",
"Cuenta el número de palabras, caracteres y líneas en un texto",
{
text: z.string().describe("Texto a analizar"),
},
async ({ text }) => {
const result = {
words: text.split(/\s+/).filter(Boolean).length,
characters: text.length,
lines: text.split("\n").length,
};
return { content: [{ type: "text", text: JSON.stringify(result) }] };
}
);
// 3. Generar password
server.tool(
"generate_password",
"Genera un password seguro con los criterios especificados",
{
length: z.number().min(8).max(128).default(16).describe("Longitud del password"),
includeSymbols: z.boolean().default(true).describe("Incluir símbolos (!@#$...)"),
includeNumbers: z.boolean().default(true).describe("Incluir números"),
},
async ({ length, includeSymbols, includeNumbers }) => {
let chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
if (includeNumbers) chars += "0123456789";
if (includeSymbols) chars += "!@#$%^&*()_+-=[]{}";
const password = Array.from({ length }, () => chars[Math.floor(Math.random() * chars.length)]).join("");
return { content: [{ type: "text", text: `Password generado: ${password}` }] };
}
);
Ejercicio 2: Implementar un Tool con validación (Medio)
Implementa un tool create_directory en TypeScript que:
- Reciba la ruta del directorio a crear
- Valide que la ruta no contenga ".." (prevenir path traversal)
- Cree la estructura de directorios recursivamente
- Retorne confirmación con la ruta creada
Ver solución
import * as fs from "fs/promises";
import * as path from "path";
server.tool(
"create_directory",
"Crea un directorio y sus padres si no existen",
{
dirPath: z.string().describe("Ruta del directorio a crear (e.g., 'src/components/ui')"),
},
async ({ dirPath }) => {
if (dirPath.includes("..")) {
return {
content: [{ type: "text", text: "Error: la ruta no puede contener '..' por seguridad" }],
isError: true,
};
}
const absolutePath = path.resolve(dirPath);
try {
await fs.mkdir(absolutePath, { recursive: true });
const stats = await fs.stat(absolutePath);
return {
content: [{
type: "text",
text: JSON.stringify({
success: true,
path: absolutePath,
created: stats.birthtime.toISOString(),
}, null, 2),
}],
};
} catch (error) {
return {
content: [{
type: "text",
text: `Error al crear directorio: ${error instanceof Error ? error.message : "desconocido"}`,
}],
isError: true,
};
}
}
);
Ejercicio 3: Tool CRUD en Python (Medio)
Implementa un tool manage_todo en Python que soporte crear y completar tareas en una lista in-memory:
Ver solución
import json
from datetime import datetime
todos: list[dict] = []
@server.tool()
async def manage_todo(action: str, title: str = "", todo_id: int = -1) -> str:
"""Gestiona una lista de tareas (crear, completar, listar).
Args:
action: Acción a realizar (create, complete, list)
title: Título de la tarea (requerido para create)
todo_id: ID de la tarea (requerido para complete)
"""
if action == "create":
if not title:
raise ValueError("El título es requerido para crear una tarea")
todo = {
"id": len(todos) + 1,
"title": title,
"completed": False,
"created_at": datetime.now().isoformat(),
}
todos.append(todo)
return json.dumps({"message": f"Tarea creada: {title}", "todo": todo}, indent=2)
elif action == "complete":
if todo_id < 0:
raise ValueError("El todo_id es requerido para completar una tarea")
for todo in todos:
if todo["id"] == todo_id:
todo["completed"] = True
return json.dumps({"message": f"Tarea completada: {todo['title']}", "todo": todo}, indent=2)
raise ValueError(f"Tarea con ID {todo_id} no encontrada")
elif action == "list":
return json.dumps({"total": len(todos), "todos": todos}, indent=2)
else:
raise ValueError(f"Acción inválida: {action}. Opciones: create, complete, list")
Ejercicio 4: Tool con dryRun (Difícil)
Implementa un tool rename_files en TypeScript que:
- Reciba un directorio, un patrón de búsqueda y un patrón de reemplazo
- En modo
dryRun: true, muestre qué archivos cambiarían de nombre - En modo
dryRun: false, ejecute el renombrado
Ver solución
import * as fs from "fs/promises";
import * as path from "path";
server.tool(
"rename_files",
"Renombra archivos en un directorio reemplazando un patrón en el nombre",
{
directory: z.string().describe("Ruta del directorio"),
searchPattern: z.string().describe("Texto a buscar en los nombres de archivo"),
replaceWith: z.string().describe("Texto de reemplazo"),
dryRun: z.boolean().default(true).describe("Si true, solo muestra preview sin ejecutar"),
},
async ({ directory, searchPattern, replaceWith, dryRun }) => {
const files = await fs.readdir(directory);
const changes: Array<{ original: string; newName: string }> = [];
for (const file of files) {
if (file.includes(searchPattern)) {
changes.push({
original: file,
newName: file.replace(searchPattern, replaceWith),
});
}
}
if (changes.length === 0) {
return {
content: [{
type: "text",
text: `No se encontraron archivos con '${searchPattern}' en ${directory}`,
}],
};
}
if (dryRun) {
return {
content: [{
type: "text",
text: `Preview de renombrado (dryRun):\n\n${changes.map(c => ` ${c.original} → ${c.newName}`).join("\n")}\n\nTotal: ${changes.length} archivos. Usa dryRun: false para ejecutar.`,
}],
};
}
for (const change of changes) {
await fs.rename(
path.join(directory, change.original),
path.join(directory, change.newName)
);
}
return {
content: [{
type: "text",
text: `${changes.length} archivos renombrados:\n${changes.map(c => ` ${c.original} → ${c.newName}`).join("\n")}`,
}],
};
}
);
Resumen
En esta cápsula aprendiste:
- Tools son funciones ejecutables que el modelo puede invocar a través del MCP server
- Tienen side effects — pueden crear archivos, insertar datos, enviar mensajes
- Requieren un input schema que define parámetros con tipos, descripciones y validación
- El flujo incluye aprobación del usuario antes de ejecutar
- Zod (TypeScript) y type hints + docstrings (Python) definen los schemas
- El error handling es parte del diseño, no un afterthought
- La descripción del tool es crucial — es lo que le dice al modelo cuándo usarlo
- Patrones como dryRun, CRUD completo y progress hacen los tools más robustos
Próxima cápsula: Prompts — templates reutilizables con parámetros. La primitiva menos intuitiva para developers pero fundamental para estandarizar interacciones.
Recursos adicionales
- MCP Specification — Tools - Especificación oficial de Tools
- Zod Documentation - Librería de validación usada en el SDK de TypeScript
- JSON Schema - Formato subyacente de los input schemas
- MCP TypeScript SDK — Tools - Implementación de Tools en TS
- MCP Python SDK — Tools - Implementación de Tools en Python
- Building Effective Tools (Anthropic) - Best practices para tool design