Lo primero que hace todo el mundo con MCP es lo mismo: coger la API REST que ya tiene y exponer un endpoint por herramienta. Cuarenta rutas, cuarenta tools, listo en una tarde. Y entonces el agente empieza a comportarse como un becario con acceso de administrador y sin contexto: pide la lista completa de clientes para buscar uno, llama tres veces a la misma herramienta porque la primera respuesta le pareció rara, y en algún momento manda un correo que no debía. El problema no es el modelo. Es que una API está diseñada para un programador que leyó la documentación, y una herramienta MCP está diseñada para alguien que decide en un segundo, con lo que cabe en la ventana de contexto.
Una herramienta no es un endpoint
La granularidad correcta de una herramienta no es el recurso, es la intención. Un programador que quiere generar un presupuesto sabe encadenar GET /customers?email=, GET /products, POST /quotes y POST /quotes/:id/send. El agente también puede hacerlo, pero cada salto es una llamada más, más tokens, más latencia y una oportunidad más de equivocarse.
La versión que funciona agrupa los pasos en la unidad de trabajo que el negocio reconoce:
// En vez de cuatro herramientas CRUD
crear_presupuesto({
cliente_email: string,
articulos: Array<{ sku: string; cantidad: number }>,
descuento_pct?: number,
})
Esa herramienta por dentro hace las cuatro llamadas, valida que el cliente existe, comprueba que el descuento está dentro de lo permitido y devuelve el presupuesto. El agente toma una decisión en vez de cuatro, y tú controlas el orden en código en vez de rezar para que el modelo lo acierte.
La regla con la que me guío: si dos herramientas se llaman siempre juntas y en el mismo orden, son una sola herramienta. Si una herramienta necesita que el modelo invente un identificador interno, está mal diseñada.
El esquema es el prompt
Esto cuesta interiorizarlo: el esquema JSON de la herramienta es el prompt que el modelo lee para decidir. No es documentación, es instrucción. Un campo llamado status: string no dice nada; un enum sí:
{
estado: {
type: 'string',
enum: ['borrador', 'enviado', 'aceptado', 'rechazado'],
description: 'Estado del presupuesto. "enviado" solo tras aprobación del usuario.',
}
}
Lo mismo con los errores. Un 400 Bad Request con cuerpo vacío deja al modelo adivinando, y un modelo que adivina reintenta. Un error que explica qué falló y qué hacer convierte el reintento en una corrección:
{
"error": "descuento_no_permitido",
"mensaje": "El descuento máximo para este cliente es 15%. Pediste 30%.",
"siguiente_paso": "Reenvía con descuento_pct <= 15 o pide aprobación con solicitar_descuento_excepcional."
}
Escribo los mensajes de error pensando en que los va a leer el modelo, porque los va a leer el modelo.
El agente actúa en nombre de alguien
Este es el fallo que más veo y el que más caro sale. El servidor MCP se conecta a la API interna con una cuenta de servicio que lo puede todo, y los permisos del usuario que está hablando con el agente desaparecen por el camino. Resultado: cualquiera que pueda hablar con el bot puede leer cualquier cosa.
La herramienta tiene que ejecutarse con la identidad de quien la invoca. En la práctica: el token del usuario viaja en la sesión MCP, el servidor lo usa para llamar al sistema de abajo, y si ese usuario no puede ver los contratos de otra delegación, el agente tampoco.
Y una categoría aparte: las acciones con efectos. Enviar, cobrar, borrar, firmar. Ahí no basta con los permisos, hace falta una confirmación humana explícita antes de ejecutar. Mi patrón es dividir la herramienta en dos:
preparar_envio_presupuesto({ presupuesto_id }) // devuelve un resumen y un token de confirmación
confirmar_envio({ token }) // solo lo llama el flujo tras el "sí" del humano
El agente puede preparar todo lo que quiera; el botón lo aprieta una persona.
Si no es idempotente, mandas tres correos
Los agentes reintentan. Por diseño: se corta la conexión, el modelo no ve la respuesta, el orquestador reenvía. Si confirmar_envio no es idempotente, el cliente recibe el presupuesto tres veces y tú te enteras por el comercial.
La solución es la de siempre, la misma que usas en una pasarela de pagos: clave de idempotencia. El token de confirmación del ejemplo anterior sirve de clave — se consume una vez, y las llamadas siguientes con el mismo token devuelven el resultado de la primera en vez de ejecutar otra vez.
Lo que devuelves cuesta dinero
Un endpoint REST puede devolver 200 KB de JSON sin que a nadie le importe. En una herramienta MCP esa respuesta entra en la ventana de contexto del modelo: la pagas en tokens, la pagas en latencia y, peor, entierras el dato útil entre ruido.
Devuelve lo mínimo accionable. Para una lista, los cinco primeros resultados con los tres campos que importan y un total: 240 para que el modelo sepa que hay más. Para un objeto, los campos que alguien usaría al decidir, no la fila entera de la base de datos. Si el agente necesita el detalle, que lo pida con otra llamada: esa segunda llamada es más barata que arrastrar el detalle completo en cada paso de la conversación.
Lo que me llevo
Construyendo el servidor MCP de GenPM, el cambio de mentalidad que más rendimiento dio fue dejar de pensar "qué expongo" y empezar a pensar "qué decisiones quiero que el modelo tome". Cada herramienta es una decisión que delegas. Si la delegas mal, el modelo decide por ti, y lo hace con la información que tú le diste en el esquema.
Si tienes que quedarte con tres cosas: agrupa por intención, escribe los errores para que los lea el modelo, y no dejes que ninguna acción con efectos se ejecute sin que una persona haya dicho que sí.