Integración de Herramientas y el Protocolo MCP
Un modelo fundacional puro, por muy avanzado que sea, vive en una burbuja. No sabe qué hora es, no puede consultar el inventario de tu base de datos y no puede desplegar código. Para que un agente interactúe con el mundo, necesita herramientas.
Para dummies: Brazos robóticos para cerebros digitales
En el mundo corporativo te hablarán de "orquestación de microservicios mediante capacidades cognitivas ampliadas". En código, esto se traduce en darle al LLM acceso a funciones específicas de tu sistema.
Imagina que el LLM es un cerebro brillante metido en un tarro. MCP (Model Context Protocol) es la API estándar que le enchufa brazos robóticos a ese tarro. Antes, cada proveedor de modelos tenía su propio formato privativo para llamar a funciones externas. Ahora, MCP estandariza cómo un modelo descubre y ejecuta herramientas, independientemente de si estás usando OpenAI, Anthropic o un modelo local.
El problema de las herramientas a medida
He lidiado con docenas de implementaciones donde los programadores crean wrappers gigantescos para cada endpoint de su API solo para que el LLM lo entienda. Eso escala fatal. Si cambias el contrato de la API, el agente se rompe y empieza a alucinar parámetros.
Adoptar MCP te obliga a pensar en tus herramientas como servidores independientes. El agente actúa como cliente MCP, consulta qué herramientas están disponibles en el servidor y luego pide ejecutar una de ellas pasándole un JSON estandarizado.
Ejemplo Práctico: Un servidor MCP básico
Vamos a ver cómo levantar un servidor MCP mínimo en Python usando el SDK oficial. Este servidor expone una herramienta para leer el contenido de un archivo local.
from mcp.server import Server, NotificationOptions
from mcp.server.models import InitializationOptions
import mcp.types as types
import mcp.server.stdio
import os
# Inicializamos el servidor con un nombre identificativo
server = Server("mi-servidor-local")
@server.list_tools()
async def handle_list_tools() -> list[types.Tool]:
"""
Le dice al agente qué herramientas tenemos disponibles.
"""
return [
types.Tool(
name="leer_archivo",
description="Lee el contenido de un archivo de texto en disco.",
inputSchema={
"type": "object",
"properties": {
"ruta": {"type": "string", "description": "Ruta absoluta al archivo"}
},
"required": ["ruta"]
}
)
]
@server.call_tool()
async def handle_call_tool(
name: str, arguments: dict | None
) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
"""
Ejecuta la herramienta solicitada por el agente.
"""
if name == "leer_archivo":
ruta = arguments.get("ruta")
if not ruta or not os.path.exists(ruta):
return [types.TextContent(type="text", text="Error: Archivo no encontrado.")]
try:
with open(ruta, 'r', encoding='utf-8') as f:
contenido = f.read()
return [types.TextContent(type="text", text=contenido)]
except Exception as e:
return [types.TextContent(type="text", text=f"Error leyendo el archivo: {str(e)}")]
raise ValueError(f"Herramienta desconocida: {name}")
async def main():
# El servidor se comunica con el agente a través de la entrada/salida estándar (stdio)
async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
await server.run(
read_stream,
write_stream,
InitializationOptions(
server_name="mi-servidor-local",
server_version="1.0.0",
capabilities=server.get_capabilities(
notification_options=NotificationOptions(),
experimental_capabilities={},
)
)
)
if __name__ == "__main__":
import asyncio
asyncio.run(main())
Este enfoque aísla los permisos. Puedes correr este script en un entorno restringido donde solo tenga acceso de lectura a una carpeta específica, sin preocuparte de que el agente en sí intente hacer algo extraño con el sistema operativo de la máquina principal. Así es como aseguras despliegues reales.