El bug de OAuth que me enseñó que la URL de transporte no es el identificador de recurso

· 5 min de lectura

Dos rutas que parecían el mismo sitio

Cuando conecté mi servidor MCP a claude.ai me encontré con dos cadenas que parecían lo mismo y no lo eran.

El conector MCP es la pieza que deja a un agente en claude.ai consultar datos fiscales sin montar su propia integración. Para que un agente lo use hay OAuth por medio: un token, un metadato de recurso, una validación. Y fue montando esa parte de OAuth donde topé con las dos cadenas.

La URL de transporte del conector es /api/mcp/mcp. Esa es la que recibe las peticiones de verdad: la ruta por la que un agente entra y consulta la base de datos fiscal. Si entras al /api/mcp a secas, la API contesta un 404. Le falta el segmento [transport]. Sin ese último tramo, la ruta directamente no existe.

El identificador de recurso OAuth, en cambio, es a propósito el /api/mcp a secas. Ese es el aud del token y el resourceUrl que publico en mi PRM, el Protected Resource Metadata.

Conviven, entonces, dos cadenas casi idénticas. Una termina en /mcp/mcp; la otra, en /mcp. Y lo primero que pensé fue: «esto es un bug».

Por qué quise armonizarlas

Parecía el descuido clásico. Dos rutas que se parecen tanto que una tenía que ser un error de la otra. ¿Quién deja a propósito un identificador sin el último segmento?

Mi razonamiento era lineal. Si el identificador OAuth es /api/mcp, la URL de transporte debería ser /api/mcp. O al revés: si el transporte es /api/mcp/mcp, el identificador debería ser /api/mcp/mcp. Cualquier opción, con tal de no dejar dos cadenas que a simple vista no coinciden.

La tentación tenía nombre propio: armonizar. Un solo cambio, dos rutas consistentes, y el código queda más limpio. Al fin y al cabo, ¿no es lo mismo un recurso y el sitio donde se sirve ese recurso? Si apuntas al mismo lugar, ¿por qué escribirlo distinto?

Estuve a punto de hacerlo. La intención era buena; el modelo mental, no. Lo peor es que la decisión no venía de leer el RFC, sino de una corazonada visual: dos cadenas que no cuadran, por definición, tenían que ser un descuido.

El error que no llegué a cometer

Y aquí está el fallo, que por suerte no llegó a producción.

El /api/mcp a secas devuelve 404. No es que apunte mal ni que esté roto: es que no es un endpoint. Es una identidad lógica, no una ruta de red. Si lo armonizo para que coincida con la URL de transporte, estoy mezclando dos cosas que no son lo mismo, y el resultado no se vuelve más limpio: se vuelve incorrecto.

Peor todavía: armonizar ambas habría roto el conector en vivo. claude.ai no compara las cadenas por su parecido ni por cómo se leen de izquierda a derecha. Valida el identificador contra el PRM. Si yo cambio el identificador a /api/mcp/mcp para que «cuadre» con la URL de transporte, el PRM deja de casar, la validación falla y la conexión se cae.

El bug no estaba en el código. Estaba en mi modelo mental. Asumí que una ruta de red y un identificador de recurso debían ser la misma cadena. No lo son. Y de haber aplicado el cambio sin pararme a entender por qué existían dos cadenas, habría roto algo que ya funcionaba.

La distinción que lo resuelve

La salida vino de los RFC. Un identificador de recurso OAuth — el aud del token y el resourceUrl del PRM — es un identificador de recurso en el sentido de RFC 9728 y RFC 8707. Una identidad lógica. No es el endpoint de red por el que viajan los bytes.

El matiz que lo aclara todo está en cómo OAuth separa el «quién» del «por dónde». RFC 8707 define cómo un cliente indica a qué recurso va dirigido el token que pide. RFC 9728 define cómo el servidor de recursos publica su metadato para que el cliente lo descubra. En los dos casos, el identificador de recurso es eso: un identificador, un nombre con el que OAuth se entiende a sí mismo. No una ruta de transporte.

Son dos preguntas distintas, y conviene no confundirlas.

El endpoint de transporte /api/mcp/mcp responde a «¿por dónde entro?». Dice la ruta concreta, incluido el segmento [transport], por la que llega la petición. Sin ese segmento la ruta no resuelve, y por eso el 404.

El identificador /api/mcp responde a otra cosa: «¿qué recurso estoy autorizando?». El aud del token dice para quién se emitió ese token; el resourceUrl del PRM dice qué recurso es este para el mundo OAuth. Es un nombre, no una dirección.

Dicho más claro: la URL de transporte es una dirección; el identificador de recurso es un nombre. Una dirección y un nombre pueden no coincidir y aun así referirse a lo correcto. En mi conector, el recurso lógico se llama /api/mcp y su ruta de transporte es /api/mcp/mcp. Las dos conviven porque cada una responde a una pregunta diferente.

Qué aprendí

Me quedé con una regla que ahora aplico a cada integración OAuth que toco: la URL de transporte no es el identificador de recurso. El aud y el resourceUrl son identidad, no enrutamiento.

Cuando veo dos cadenas que «deberían» coincidir y no coinciden, ya no salgo a armonizarlas. Primero me pregunto si estoy comparando dos conceptos que, por diseño, no tienen por qué ser la misma cadena. Casi siempre, la respuesta está en el RFC.

Fue un bug que no llegué a meter. Y precisamente por eso es de los que más me ha enseñado: no me corrigió el código, me corrigió la forma de leerlo. La próxima vez que dos rutas no cuadren, antes de tocarlas voy a preguntarme qué está respondiendo cada una.

Si construyes agentes y quieres ver el conector que me dio esta lección, está aquí:

https://www.conversoriaecnae.es/mcp

Brian Mena

Brian Mena

Ingeniero informatico construyendo productos digitales rentables: SaaS, directorios y agentes de IA. Todo desde cero, todo en produccion.

LinkedIn