SincroDev Logo SincroDev

MCP en la práctica: cómo conectar un LLM con herramientas reales


En el artículo sobre la arquitectura de MCP vimos los conceptos: Hosts, Clients, Servers, Tools y Resources. Ahora toca ensuciarse las manos.

Este artículo cubre cómo construir un servidor MCP funcional en Python, conectarlo con un Host como Claude Desktop, y exponer herramientas que el LLM pueda usar para interactuar con sistemas reales.

¿Qué vamos a construir?

Un servidor MCP que expone tres herramientas:

  • consultar_cliente: busca un cliente por nombre en una base de datos.
  • estado_pedido: devuelve el estado de un pedido por ID.
  • crear_nota: guarda una nota en un archivo local.

El LLM no accede directamente a la base de datos ni al sistema de archivos. Le pide al servidor MCP que lo haga y recibe el resultado.

Requisitos previos

  • Python 3.10+
  • El paquete mcp (SDK oficial de Anthropic para Python)
  • Un Host compatible (Claude Desktop, Cursor, VS Code con extensión, etc.)
pip install mcp

Estructura del proyecto

mi-servidor-mcp/
├── server.py          # Servidor MCP principal
├── db.py              # Simulación de base de datos
└── notas/             # Carpeta donde se guardan notas

Paso 1: definir las herramientas

Cada herramienta MCP es una función Python decorada que el SDK expone automáticamente al Host.

# server.py
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("mi-servidor")

@mcp.tool()
def consultar_cliente(nombre: str) -> str:
    """Busca un cliente por nombre y devuelve sus datos."""
    from db import buscar_cliente
    cliente = buscar_cliente(nombre)
    if not cliente:
        return f"No se encontró ningún cliente con nombre '{nombre}'."
    return f"Cliente: {cliente['nombre']}, Email: {cliente['email']}, Plan: {cliente['plan']}"

@mcp.tool()
def estado_pedido(pedido_id: str) -> str:
    """Consulta el estado actual de un pedido por su ID."""
    from db import obtener_pedido
    pedido = obtener_pedido(pedido_id)
    if not pedido:
        return f"No existe el pedido '{pedido_id}'."
    return f"Pedido {pedido_id}: {pedido['estado']} (última actualización: {pedido['fecha']})"

@mcp.tool()
def crear_nota(titulo: str, contenido: str) -> str:
    """Crea una nota en el sistema de archivos local."""
    import os
    ruta = os.path.join("notas", f"{titulo.replace(' ', '_').lower()}.md")
    os.makedirs("notas", exist_ok=True)
    with open(ruta, "w") as f:
        f.write(f"# {titulo}\n\n{contenido}\n")
    return f"Nota guardada en {ruta}"

Lo importante aquí:

  • El docstring de cada función es lo que el LLM lee para decidir cuándo usarla. Debe ser claro y descriptivo.
  • Los type hints de los parámetros definen el esquema que el Host presenta al modelo.
  • El return siempre es texto que el LLM procesa para formular su respuesta al usuario.

Paso 2: datos de prueba

# db.py
CLIENTES = [
    {"nombre": "Ana García", "email": "ana@ejemplo.com", "plan": "Pro"},
    {"nombre": "Carlos López", "email": "carlos@ejemplo.com", "plan": "Free"},
    {"nombre": "María Torres", "email": "maria@ejemplo.com", "plan": "Enterprise"},
]

PEDIDOS = {
    "PED-001": {"estado": "Enviado", "fecha": "2026-03-20"},
    "PED-002": {"estado": "En preparación", "fecha": "2026-03-22"},
    "PED-003": {"estado": "Entregado", "fecha": "2026-03-18"},
}

def buscar_cliente(nombre: str):
    for c in CLIENTES:
        if nombre.lower() in c["nombre"].lower():
            return c
    return None

def obtener_pedido(pedido_id: str):
    return PEDIDOS.get(pedido_id)

En producción reemplazas esto por consultas a PostgreSQL, una API REST o cualquier fuente de datos real.

Paso 3: exponer Resources (opcional)

Además de herramientas, puedes exponer recursos que el LLM puede leer como contexto. Útil para archivos de configuración, documentación interna o datos que no requieren ejecución.

@mcp.resource("config://planes")
def planes_disponibles() -> str:
    """Lista de planes y precios actuales."""
    return """
    Free: 0€/mes - 1 usuario, funciones básicas
    Pro: 29€/mes - 5 usuarios, automatizaciones
    Enterprise: 99€/mes - usuarios ilimitados, soporte prioritario
    """

La diferencia clave: las herramientas ejecutan acciones (el LLM decide cuándo llamarlas), los recursos son datos pasivos que el Host puede inyectar como contexto.

Paso 4: arrancar el servidor

# Al final de server.py
if __name__ == "__main__":
    mcp.run(transport="stdio")

Para probar localmente antes de conectar al Host:

python server.py

El servidor queda escuchando por stdin/stdout, listo para que un Host se conecte.

Paso 5: conectar con Claude Desktop

En el archivo de configuración de Claude Desktop (claude_desktop_config.json):

{
  "mcpServers": {
    "mi-servidor": {
      "command": "python",
      "args": ["/ruta/completa/a/server.py"]
    }
  }
}

Al reiniciar Claude Desktop, verás las herramientas disponibles. Puedes pedirle cosas como:

  • “¿Qué plan tiene Ana García?”
  • “¿En qué estado está el pedido PED-002?”
  • “Crea una nota con el resumen de la reunión de hoy.”

El LLM decide qué herramienta llamar, el servidor la ejecuta y devuelve el resultado.

Flujo completo de una petición

Usuario: "¿Qué plan tiene el cliente Carlos?"

Claude (Host) → detecta que necesita consultar_cliente

MCP Client → envía JSON-RPC al Server: tools/call("consultar_cliente", {"nombre": "Carlos"})

Server → ejecuta buscar_cliente("Carlos") → devuelve datos

Claude ← recibe: "Cliente: Carlos López, Email: carlos@ejemplo.com, Plan: Free"

Claude → responde al usuario: "Carlos López está en el plan Free."

Patrones útiles en producción

Validación de entrada

No confíes ciegamente en lo que envía el LLM. Valida parámetros dentro de cada herramienta:

@mcp.tool()
def estado_pedido(pedido_id: str) -> str:
    """Consulta el estado de un pedido. El ID debe tener formato PED-XXX."""
    if not pedido_id.startswith("PED-"):
        return "Error: el ID de pedido debe empezar con 'PED-'."
    # ...resto de la lógica

Limitar alcance

Cada servidor debe hacer pocas cosas bien. Si necesitas herramientas de base de datos Y herramientas de email, crea dos servidores separados. El Host gestiona múltiples conexiones sin problema.

Manejo de errores

Devuelve mensajes claros en caso de fallo. El LLM necesita entender qué salió mal para comunicárselo al usuario:

@mcp.tool()
def consultar_cliente(nombre: str) -> str:
    """Busca un cliente por nombre."""
    try:
        cliente = buscar_cliente(nombre)
        if not cliente:
            return f"No se encontró cliente con nombre '{nombre}'."
        return f"Cliente: {cliente['nombre']}, Plan: {cliente['plan']}"
    except Exception as e:
        return f"Error al consultar la base de datos: {str(e)}"

Logging

Registra cada llamada a herramienta. En producción querrás saber qué pide el LLM, con qué frecuencia y qué falla:

import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("mcp-server")

@mcp.tool()
def consultar_cliente(nombre: str) -> str:
    """Busca un cliente por nombre."""
    logger.info(f"Consulta de cliente: {nombre}")
    # ...

Errores comunes

  1. Docstrings vagos: si la descripción de la herramienta no es clara, el LLM no sabrá cuándo usarla o pasará parámetros incorrectos.
  2. Herramientas demasiado genéricas: una función ejecutar_query(sql: str) es un riesgo de seguridad. Expón operaciones específicas y acotadas.
  3. No manejar el caso vacío: si la herramienta no encuentra datos, devuelve un mensaje explícito en vez de None o string vacío.
  4. Mezclar muchas responsabilidades en un servidor: dificulta mantenimiento y permisos. Un servidor por dominio.
  5. Olvidar que el usuario ve los errores: el LLM traduce tus mensajes de retorno. Si devuelves un stack trace, eso llega al usuario.

De demo a producción

Para llevar esto a un entorno real:

  • Reemplaza los datos simulados por conexiones a tu base de datos o APIs.
  • Añade autenticación si el servidor corre remoto (transporte SSE sobre HTTPS).
  • Implementa rate limiting en herramientas que llaman a APIs externas.
  • Versiona el servidor y documenta cada herramienta para tu equipo.
  • Monitoriza uso: qué herramientas se llaman más, cuáles fallan, tiempos de respuesta.

Conclusión

MCP convierte a un LLM de “chatbot que sabe cosas” a “asistente que hace cosas”. La arquitectura es simple (cliente-servidor con JSON-RPC), y el SDK de Python reduce la implementación a decoradores sobre funciones normales.

Lo potente no es la tecnología en sí, sino lo que habilita: conectar la inteligencia del modelo con los datos y acciones reales de tu negocio.

Lecturas relacionadas