En payments-mcp la misma lógica de pagos se ofrece a dos tipos de clientes: los servicios internos, por una API REST, y los agentes de IA, por un servidor MCP. La parte REST es terreno conocido: OAuth2 con client credentials, un JWT firmado y scopes por ruta. Con un agente la pregunta cambia. El cliente es un modelo que lee texto de terceros (la nota de un pedido, un correo, la descripción de un producto) y cualquiera de esos textos puede intentar darle instrucciones. La seguridad no puede depender de que el modelo se porte bien.
Cómo encuentra el agente a quién pedir el token
La especificación de MCP delega la autorización en OAuth. Si alguien llama a POST /mcp sin un token válido, el servidor responde 401 con una cabecera WWW-Authenticate que apunta a su Protected Resource Metadata (RFC 9728): un JSON que dice qué recurso es, quién emite sus tokens y qué scopes existen. El cliente lo lee, va al servidor de autorización y pide el token por su cuenta. El agente no lleva URLs de autenticación configuradas a mano.
{
"resource": "https://payments.example.com/mcp",
"authorization_servers": ["https://payments.example.com"],
"scopes_supported": ["payments:read", "payments:write", "payments:refund"],
"bearer_methods_supported": ["header"]
}Un token, un recurso
El cliente pide el token indicando para qué recurso lo quiere (resource, RFC 8707) y ese valor termina en el claim aud. La API REST solo acepta tokens con su propia audiencia y el endpoint MCP solo los suyos. Un token emitido para el agente no sirve contra la API REST, y al revés tampoco.
Parece un detalle, pero evita el *token passthrough*: un servidor MCP que reenvía a otra API el token que recibió se convierte en un intermediario con más poder del que debería (el problema del *confused deputy*). La verificación también fija el algoritmo (ES256) en vez de confiar en la cabecera del propio token, que es la puerta clásica de los ataques con alg: none.
Lo que el modelo no ve, no lo puede llamar
El endpoint MCP no guarda estado: cada petición construye un servidor nuevo a partir del token verificado, así que cualquier pod puede atenderla sin sesiones pegajosas. Al construirlo, cada tool se registra solo si el token trae su scope.
const can = (scope: Scope): boolean => auth.scopes.has(scope);
if (can('payments:read')) registerReadTools(server, deps);
if (can('payments:write')) registerCreatePayment(server, deps);
if (can('payments:refund')) registerRefundPayment(server, deps);El agente de la tienda A tiene read y write, pero no refund, así que refund_payment ni siquiera aparece en tools/list. Un prompt malicioso no puede convencer al modelo de usar una herramienta que no existe para él.
La segunda decisión va en la misma línea: ninguna tool recibe el comercio como argumento. El merchantId sale del token y las tools lo capturan al crearse. Si un texto inyectado le pide al modelo «consulta los pagos del comercio B», no hay ningún parámetro que manipular.
Mover dinero con dos llaves
Un reembolso no se puede deshacer, así que refund_payment funciona en dos pasos. Sin confirm: true devuelve una vista previa: aplica las mismas validaciones que el reembolso real, no tiene ningún efecto y le indica al modelo que pida aprobación. Solo una segunda llamada con los mismos argumentos y confirm: true mueve el dinero.
El agente de demostración añade un segundo candado en el cliente: antes de ejecutar una tool marcada como destructiva con confirm: true, le pregunta a la persona. Si no hay forma de preguntar, la rechaza por defecto. El servidor no confía en el cliente y el cliente no confía en el modelo.
Las descripciones de las tools también son parte del contrato, porque el modelo las lee. create_payment exige una idempotencyKey y explica qué hacer ante un timeout: reintentar con la misma clave y, si el pago queda pending, consultarlo más tarde en vez de cobrar otra vez con una clave nueva.
Lecciones
- La autorización la impone el servidor. Las instrucciones al modelo ayudan, pero no protegen.
- Un token por recurso (aud): un token nunca debe servir donde no se pidió.
- Filtrar las tools por scope reduce la superficie de ataque: el modelo solo ve lo que puede hacer.
- La identidad (para qué comercio se actúa) sale del token, nunca de un argumento que el modelo pueda elegir.
- Para lo irreversible: vista previa, confirmación explícita y aprobación humana.