El gateway MCP de LiteLLM: la segunda puerta de entrada, el catálogo de herramientas que nadie factura y por qué filtrarlo puede empeorar la selección
Sexto artículo del track operativo de la capa de control. Los cinco anteriores tratan el gateway como la pieza por la que pasan los tokens: el par con Langfuse, el día 2, las claves virtuales, humanos y agentes y el enrutado por prefijo. Este trata la otra puerta. Verificado contra LiteLLM 1.102.0, commit del 10 de septiembre de 2026.
TL;DR
El gateway MCP no es un módulo, es una segunda superficie completa. Un endpoint MCP nativo con cuatro grafías de ruta, un modo de proxy con tres herramientas fijas, cuatro rutas REST, unas treinta y cinco rutas de administración y un juego largo de endpoints de descubrimiento OAuth. Sigue viviendo bajo _experimental y son casi 20.000 líneas.
Corrijo lo que dije en el artículo anterior: el acceso NO es abierto por defecto. En la 1.102.0, una clave sin permisos declarados y sin equipo resuelve cero servidores. Lo que sí abre es un flag por servidor, allow_all_keys, y ese flag sobrescribe el ámbito explícito de la clave salvo que se active un ajuste que por defecto está desactivado.
Los permisos se aplican en la ejecución, no solo en el listado. Llamar a una herramienta que no aparecía en tools/list devuelve 403. Hay tres comprobaciones encadenadas antes de la llamada, incluida una sobre los argumentos permitidos. El mismo predicado gobierna listado y llamada, por diseño declarado.
La jerarquía es de intersecciones, con dos escapes y un fallo abierto. Los grupos de acceso de la clave son aditivos por encima del techo de clave y equipo. El techo de organización sustituye en lugar de intersecar cuando no hay nada por debajo. Y ante un error indeterminado al resolver permisos de herramienta, el código devuelve “sin restricción”. Es una decisión deliberada y hay que declararla en el análisis de riesgos.
El coste por herramienta vale cero y por eso su métrica desaparece. Hay exactamente dos métricas de Prometheus para MCP, y la de gasto solo se incrementa si el coste es mayor que cero. Con la configuración por defecto, esa serie nunca existe. No hay métrica de listado, ni de latencia, ni de errores, ni de salud de servidor.
El coste que sí importa no está atribuido a MCP en ningún sitio. Son los tokens de las definiciones de herramientas, reenviados en cada turno dentro del prompt_tokens de la llamada al modelo. Cinco servidores MCP típicos suman 58 herramientas y unos 55.000 tokens. La fila de gasto de la herramienta, con su coste cero, no tiene nada que ver con eso.
Y el filtro semántico del gateway no actúa en la puerta MCP. Solo se aplica cuando es LiteLLM quien llama al modelo. Un cliente conectado a /mcp recibe el catálogo entero, con el filtro activado o sin él. Y aunque actuase, filtrar por petición reescribe el prefijo cacheado del prompt, que es lo contrario de lo que hacen Anthropic y OpenAI con sus propias búsquedas de herramientas.
La observabilidad buena viene apagada. Sin LITELLM_OTEL_V2, una llamada a herramienta sale como un span genérico con el modelo puesto a MCP: nombre. Con ella activada hay span propio y atributos correctos, y los argumentos y el resultado siguen ocultos salvo opt-in. Y en ninguno de los dos casos se propaga contexto de traza hacia el servidor MCP upstream: la traza se corta en el gateway.
Los argumentos de herramienta sí se escriben, y se escriben en claro. Van completos a la columna metadata de la tabla de gasto, sin pasar por el interruptor que gobierna el almacenamiento de prompts ni por la función de redacción, que solo conoce messages y response. Quien activó la redacción por RGPD probablemente cree estar cubierto.
La rama 1.83 concentró cuatro vulnerabilidades graves, y una es del propio gateway MCP. El endpoint de probar conexión antes de guardar aceptaba comando, argumentos y entorno del transporte stdio en el cuerpo de la petición. Una clave de bajo privilegio obtenía ejecución de comandos en el host del proxy.
Y la especificación se movió debajo. La revisión vigente desde julio de 2026 eliminó las sesiones de protocolo, el handshake de inicialización y la cabecera de sesión. LiteLLM 1.102.0 anuncia la revisión de junio de 2025 y su enumeración de versiones ni siquiera contempla las dos posteriores.
Estás aquí: la puerta que no se eligió
El gateway se eligió por lo que cuenta el artículo de la centralita: licencia, encaje en el cluster, madurez. Lo que se evaluó fue el tráfico de inferencia.
Después, alguien conecta un agente, el agente necesita herramientas, y el mismo proceso que ya está autenticado y desplegado resulta que también sabe hablar MCP. La decisión de convertirlo en la puerta de las herramientas casi nunca se toma: se hereda. Y esa puerta no tiene ni los mismos controles, ni la misma observabilidad, ni el mismo modelo de coste.
Cuando MCP crece cubrió la identidad de los servidores MCP propios. MCP por dentro cubrió el protocolo y su instrumentación. Este artículo cubre la pieza en medio.
La analogía: la centralita que también reparte llaves
Una centralita telefónica de un edificio de oficinas. Su trabajo original es enrutar llamadas: decide quién puede llamar al exterior, cuánto gasta cada departamento, y deja registro de cada conferencia.
Con el tiempo, al operador de centralita se le da también el armario de las llaves. Ya está en recepción, ya sabe quién es cada cual, parece el sitio natural. Y a partir de ese momento hay dos servicios en el mismo mostrador con reglas muy distintas.
Las llamadas están tarifadas al minuto y aparecen en la factura. Las llaves no tienen tarifa, así que en la factura del departamento salen a cero, y alguien podría concluir que el armario de llaves no cuesta nada. Cuesta: cuesta el tiempo del operador, cuesta que el catálogo de llaves haya que leerlo entero cada vez que alguien pregunta qué hay disponible, y cuesta que nadie compruebe que la etiqueta de una llave sigue diciendo lo que decía el día que se autorizó su préstamo.
La analogía se acaba en el sitio interesante. Cuando alguien coge una llave, en el registro queda escrito qué llave era y para qué dijo que la quería. Ese registro es simultáneamente la prueba de la acción y un dato que a lo mejor no debería estar guardado en claro.
Las rutas reales
Lo primero que sorprende al mirar el código es la cantidad de grafías por las que se llega al mismo sitio.
El endpoint nativo
La sub-aplicación MCP se monta en /mcp y dentro tiene cuatro montajes: la raíz como comodín, /mcp, /{nombre}/mcp y /sse.
Y hay además una ruta declarada a mano para /mcp a secas en la aplicación principal, con un comentario que explica por qué: el montaje no puede casar su propio prefijo desnudo, y la redirección 307 resultante rompe a los clientes MCP que están detrás de un proxy que termina TLS. Es el tipo de detalle que se descubre en producción un viernes.
/mcp/sse tiene una sorpresa. No usa transporte SSE clásico: usa el mismo gestor de sesiones HTTP en modo sin estado. El objeto SseServerTransport se construye y no se referencia en ninguna otra línea del fichero. Es código muerto, y el endpoint /mcp/sse/messages que ese objeto anuncia no existe como ruta funcional.
La ruta /{nombre}/mcp resuelve en un orden documentado: alias de servidor, lista separada por comas con un máximo de 16 elementos, conjunto de herramientas, grupo de acceso. Si nada casa, 404.
Y /mcp/proxy expone una superficie fija de tres herramientas. Es el patrón de divulgación progresiva, y volverá a aparecer más abajo porque es la mitigación más importante del artículo.
Las rutas REST
Cuatro, bajo el prefijo /mcp-rest:
| Método | Ruta | Autenticación |
|---|---|---|
| GET | /mcp-rest/tools/list | Clave virtual |
| POST | /mcp-rest/tools/call | Clave virtual |
| POST | /mcp-rest/test/connection | Clave más rol de administrador |
| POST | /mcp-rest/test/tools/list | Clave más rol de administrador |
Las variantes sin -rest que aparecen en los tipos internos no tienen router que las registre. El comodín /mcp/* las absorbe como JSON-RPC.
Las dos últimas son las que protagonizan el apartado de seguridad.
Administración y descubrimiento
Unas treinta y cinco rutas bajo /v1/mcp, casi todas con autenticación de clave virtual. Tres detalles que hay que conocer:
GET /v1/mcp/registry.json no lleva dependencia de autenticación. Devuelve 404 si el registro público no está habilitado, que es la protección real.
POST /v1/mcp/server exige rol de administrador del proxy. POST /v1/mcp/server/register no: es el camino de propuesta, que exige una clave con equipo, prohíbe explícitamente el transporte stdio y deja la entrada en revisión pendiente.
Y encima hay un juego largo de endpoints .well-known de OAuth, con registro dinámico de clientes, autorización, token, revocación e introspección, más sus variantes por servidor.
Configuración y transporte
El esquema de mcp_servers en config.yaml tiene cincuenta y tantos campos, todos opcionales. Un aviso para quien venga de versiones anteriores: spec_version ya no existe en la 1.102.0.
El transporte por defecto cuando se omite es http, es decir, streamable HTTP. Los otros dos valores son sse y stdio. La cadena streamable_http no es válida en el fichero de configuración, aunque sí se acepta como sinónimo al importar ficheros de conectores.
El prefijado de herramientas usa el formato {servidor}{separador}{herramienta} con separador - por defecto, configurable por variable de entorno leída en tiempo de importación. El prefijo elegido es el alias, o el nombre, o el identificador, en ese orden. Hay un modo corto opcional que deriva tres caracteres del hash del identificador de servidor y resuelve colisiones rehasheando.
El control de acceso, y la corrección al artículo anterior
En el artículo de humanos y agentes cerré la lista de trampas con esta: “el acceso a servidores MCP es abierto por defecto si ningún nivel define lista”. Verificado en la 1.102.0, es falso.
La función que resuelve los servidores permitidos hace lo contrario. Si el equipo no declara nada, hereda lo de la clave; si la clave tampoco declara nada, el conjunto base es vacío y el ámbito se marca como no acotado con cero servidores. No hay ninguna rama que expanda a “todo el registro” salvo la de rol de administración.
Lo que sí abre, y es la parte importante, es otra cosa.
allow_all_keys
Es un flag por servidor, en configuración, en API y en base de datos, con valor por defecto falso. Cuando se pone a verdadero, ese servidor se une al conjunto de servidores abiertos y lo ve cualquier clave.
El detalle que hay que conocer: por defecto sobrescribe el ámbito explícito de la clave. Una clave que declara acceso solo al servidor A también verá el servidor B si B tiene el flag. Solo se respeta el ámbito de la clave si se activa general_settings.mcp_allow_all_keys_respects_mcp_scope, que por defecto está desactivado.
Hay un segundo caso de apertura: los sujetos anónimos, sin identificador de usuario ni rol ni clave, reciben además los servidores configurados con delegación de autenticación hacia el upstream y los de paso directo. Es coherente con el diseño de esos dos modos, y es una superficie que hay que conocer.
require_key_mcp_access_defined
Vive en general_settings, por defecto falso, y no requiere licencia. Lo que invierte es solo la herencia de clave vacía hacia equipo: con el flag activo, una clave sin lista propia obtiene conjunto vacío en lugar de heredar la del equipo.
No afecta a claves que sí declaran servidores, que siguen intersecando con el equipo. Y no cierra los grants por grupo de acceso, que son aditivos y son la vía de escape que hay que auditar aparte.
Existe un hermano, require_end_user_mcp_access_defined, con la misma forma y aplicado al usuario final.
La jerarquía
El docstring de la función es normativo y dice que todas las reglas son intersecciones. En detalle:
- Clave y equipo: intersección cuando ambos declaran. Herencia en una dirección cuando uno está vacío.
- Grupos de acceso de la clave: unión aditiva por encima del resultado anterior.
- Usuario final: intersección si declara algo.
- Agente: intersección.
- Usuario interno: techo que solo estrecha.
- Organización: techo que interseca si hay restricciones por debajo, y que sustituye convirtiéndose en el techo cuando no las hay.
Para sujetos sin clave, admitidos por SSO o por el gateway, el modelo cambia a unión por fuente en lugar de intersección.
El fallo abierto
Este es el punto que hay que llevar al análisis de riesgos y no dejarlo en una nota técnica.
Ante un fallo indeterminado al resolver los permisos de herramienta de una clave o un JWT, el código devuelve “sin restricción”, es decir, todas las herramientas permitidas. El techo de organización se omite en la misma situación. Para sujetos sin clave el comportamiento es el contrario, fallo cerrado.
Hay un caso intermedio bien resuelto: cuando un permiso está nombrado pero no se puede leer, se lanza un error específico y se deniega en ambos casos.
La doctrina está documentada en el docstring de la clase, lo que es de agradecer. Y sigue siendo un comportamiento que, en un sistema bajo ENS, hay que declarar por escrito en lugar de descubrirlo en una auditoría.
Lo que sí está bien
Los permisos se aplican en la llamada, no solo en el listado. La ruta de ejecución encadena tres comprobaciones antes de contactar con el servidor: herramientas permitidas o prohibidas, permiso de herramienta por clave y equipo, y validación de los argumentos contra los permitidos. Las tres devuelven 403.
Listado y llamada comparten el mismo predicado, y el docstring lo declara como invariante explícito. Un cliente que llame directamente a una herramienta que no aparecía en su listado recibe 403, no se ejecuta. Lo mismo por REST y por la ruta de la API de respuestas.
Las cabeceras x-mcp-servers y x-mcp-access-groups solo estrechan, y fallan cerrado si no resuelven.
El reenvío de cabeceras hacia el upstream es una lista blanca explícita por servidor, con una decisión centralizada sobre cuándo retirar el Authorization del llamante, y una protección contra reenvío cruzado: en un listado multiservidor, la cabecera global se retiene si más de un servidor la consumiría. Eso es RFC 9700 aplicado.
La factura que no aparece
Aquí está, en mi opinión, lo más importante del artículo, y no tiene que ver con seguridad.
Cada herramienta que el gateway agrega ocupa espacio en el contexto del modelo. Nombre, descripción y esquema de parámetros viajan en el bloque tools de la petición, en cada turno, y se pagan como tokens de entrada del modelo real.
Las cifras publicadas por Anthropic en noviembre de 2025 son la mejor referencia pública que hay. Cinco servidores MCP conectados suman 58 herramientas y unos 55.000 tokens. El desglose: GitHub, 35 herramientas y unos 26.000 tokens. Slack, 11 herramientas y unos 21.000. Sentry, cinco y unos 3.000. Grafana, cinco y unos 3.000. Splunk, dos y unos 2.000.
Con la búsqueda de herramientas activada, ese conjunto baja de unos 72.000 a unos 8.700 tokens, y el contexto útil pasa de 122.800 a 191.300.
El segundo coste de un catálogo grande no es económico y no se factura: es que el modelo elija peor. Tiene sección propia más abajo, porque de los dos es el que decide si el agente sirve para algo.
Cómo medirlo con lo que ya hay
El gateway guarda datos suficientes, aunque no los cruce por ti.
En la fila de gasto del listado, con tipo de llamada list_mcp_tools, enriquece los metadatos con allowed_server_count, tool_count_total, per_server_tool_counts y per_server_list_outcomes.
En las filas de llamada al modelo, la columna proxy_server_request conserva el cuerpo de la petición con el bloque tools dentro.
El método práctico: emparejar por session_id las filas de listado con las filas de LLM de la misma sesión, leer el número total de herramientas de la primera, y pasar el array tools de la segunda por litellm.token_counter(model=..., tools=...), que acepta ese argumento. Multiplicar por el número de turnos, porque el bloque se reenvía en todos.
Para el coste relativo, una comparación más rápida: prompt_tokens medio de las claves con MCP habilitado frente a las que no, mismo modelo y mismo periodo.
La selección de herramientas
El apartado anterior trata el catálogo como una factura. Este lo trata como lo que determina si el agente acierta. Es terreno donde el discurso va muy por delante de la medición, así que vale la pena separar lo medido de lo que se repite.
Cuántas herramientas aguanta un modelo
La cifra oficial de Anthropic es que la capacidad de Claude para escoger la herramienta correcta se degrada al pasar de 30 a 50 herramientas disponibles. La misma documentación da el criterio inverso, que es el más útil: el catálogo completo sin más es la opción correcta con menos de diez herramientas, cuando todas se usan en cada petición, o cuando las definiciones suman menos de cien tokens.
Hay además un techo administrativo antes que semántico. VS Code con Copilot corta en 128 herramientas por petición y devuelve error al superarlo.
La curva completa se cita mucho más de lo que se ha medido. El dato primario más sólido que encontré es de mayo de 2025: variando el número de candidatos de 1 a 11.100 sobre un registro de más de 4.400 servidores, por debajo de unas 30 herramientas la tasa de éxito supera el 90 %, entre 31 y 70 aparecen fallos intermitentes, y pasado el centenar la degradación pasa a estar dominada por la recuperación. Es un mapa de calor cualitativo, no una tabla numérica.
La cifra que más circula (78 % con diez herramientas, 40 % con cien, 13,6 % con setecientas) sale de una charla de conferencia sin paper, sin repositorio y sin metodología publicada. No la lleve a una presentación de cliente.
Y hay una advertencia que afecta a todo lo demás. Una auditoría de 496 tareas de cuatro bancos de pruebas de tool calling encontró un 18,5 % de desalineación entre la etiqueta y la realidad. En el subconjunto de BFCL v4, el 80 % de los fallos asignados al agente venían de comparaciones de estado frágiles. En uno de los bancos con juez automático, 23 reejecuciones idénticas dieron un rango del 57,9 % al 76,8 %, casi 19 puntos de amplitud, suficiente para reordenar la clasificación. Cualquier cifra absoluta de esta sección merece un margen de error generoso.
Cómo se falla
El trabajo con la taxonomía más limpia evalúa 36 servidores, 220 herramientas y 1.000 tareas contra veinte modelos de frontera, con una media de 15,2 herramientas expuestas por tarea de las que 4,1 son relevantes.
| Modo de fallo | Proporción |
|---|---|
| No usar herramienta cuando hacía falta | 10,5 % |
| Elegir la herramienta equivocada | 9,0 % |
| Parámetros mal formados | 6,9 % |
| No recuperarse de un error | 10,3 % |
| Fallos cognitivos, no de herramienta | 63,3 % |
Lo interesante está en el desglose por modelo: el modo de fallo dominante cambia con el modelo, no con el tamaño del catálogo. En uno de ellos, el 40,1 % de los fallos es sencillamente no llamar a ninguna herramienta. En otros domina el parámetro mal formado. En otro, la herramienta equivocada. No hay un fallo canónico que atacar.
Sobre herramientas que hacen lo mismo, que es el caso real de un gateway con quince servidores, hay una medida buena. Un estudio con diez grupos de cinco herramientas funcionalmente equivalentes y mil pares de consultas cifra el sesgo de selección entre 0,3 y 0,4, es decir, habría que redistribuir entre el 30 y el 40 % de la masa de probabilidad para que herramientas equivalentes se eligieran por igual. El mismo trabajo muestra que un entrenamiento continuado sesgado lleva la selección de un endpoint concreto del 0,6 % al 12,8 %.
Ese estudio aporta otro dato que reordena prioridades: revolver la descripción de una herramienta mueve la distribución de selección de forma sustancial, mientras que cambiar solo el nombre tiene efectos mínimos e inconsistentes. Aplicado al prefijado servidor-herramienta del gateway: cuesta tokens y probablemente no cambia a qué herramienta va el modelo. No existe ningún estudio que aísle el efecto del espaciado de nombres, y es un hueco real en la literatura.
Si la descripción es lo que pesa, el estado del parque es el problema. Una auditoría de 856 herramientas de 103 servidores MCP encontró limitaciones no declaradas en el 89,8 %, guías de uso ausentes en el 89,3 % y parámetros opacos en el 84,3 %. Solo el 2,9 % estaban libres de problemas en todos sus componentes.
El óptimo no es un número
El trabajo de mayo de 2026 que cité arriba propone una métrica corregida por azar y la usa para dimensionar la lista de forma adaptativa. Sobre 370 funciones alcanza un 90,3 % de cobertura viendo una media de 7,4 herramientas, frente a un 90,8 % viendo 50 fijas.
Pero el mismo paper publica su propio contraejemplo, y es honesto citarlo: sobre otro banco de 3.251 herramientas, el K fijo de 5 gana en agregado, 64,7 % frente a 61,9 %. La ventaja adaptativa se concentra en las consultas difíciles, donde la herramienta correcta está entre la sexta y la vigésima por similitud: ahí encuentra un 16,7 % de los casos donde el K fijo encuentra un 0 %.
El trabajo de marzo de 2026 sobre 121 herramientas sitúa la meseta en K=3, con un 97,1 % que no mejora al subir a K=5. Con la salvedad de que mide recuperación contra etiquetas, no acierto del modelo.
Y un trabajo de julio de 2026 da la formulación que me parece correcta: el ranking por puntuación es inconsistente con la adquisición óptima cuando los costes son heterogéneos, de modo que el K óptimo es función del coste, no una constante. Recortar de 7 a 4,4 herramientas expuestas manteniendo el éxito es posible; fijar un número universal, no.
Filtrar puede empeorar el resultado
Esta es la parte que no aparece en ningún blog de producto. Un banco de pruebas de junio de 2026 con tres tamaños de registro (25, 100 y 250 herramientas), seis métodos de filtrado, siete modelos y 26.460 ejecuciones:
| Estrategia | Éxito | Herramientas visibles | Tokens |
|---|---|---|---|
| Exponer el catálogo entero | 32,1 % | 125,00 | 56.062 |
| Palabra clave, top-5 | 22,1 % | 4,80 | 3.200 |
| Palabra clave, top-10 | 22,4 % | 9,54 | 5.356 |
| Consciente del estado | 24,0 % | 25,95 | 13.522 |
| Camino causal completo | 24,0 % | 26,24 | 13.697 |
Cuatro de las cinco estrategias de filtrado rinden peor que no filtrar. La quinta, que sí gana con holgura, expone 0,99 herramientas por paso y exige contratos de precondición y efecto escritos a mano para cada herramienta, más una búsqueda del camino causal hasta el estado objetivo. Es un planificador con el objetivo dado, no un filtro de recuperación, y no es comparable con nada de lo que ofrece un gateway.
Hay más evidencia en la misma dirección. Un trabajo sobre un corpus de 43.000 herramientas midió que la tasa de finalización del agente cae al usar conjuntos recuperados frente a conjuntos anotados a mano: el filtro introduce su propio techo de recall. Y el top-K rompe las tareas que necesitan varias herramientas: en un banco de 16.464 endpoints, un recuperador afinado alcanza un Recall@3 del 68,6 % pero solo un 39,7 % de las consultas tienen su conjunto completo de herramientas dentro de las tres primeras. Casi 29 puntos de brecha entre “está la que necesitaba” y “están todas las que necesitaba”.
Filtrar tampoco es gratis en seguridad. Un trabajo de marzo de 2026 ataca la capa de recuperación inyectando herramientas adversarias que saturan el top-k: con tasas de inyección del 1,2 % logra dominar el top-k entre el 91 % y el 97 % de las veces. Es una superficie de ataque que exponer el catálogo entero no tiene.
El contrapunto más elegante lo dan los propios números de Anthropic. Con el mismo catálogo de más de cincuenta herramientas, la ganancia de la búsqueda de herramientas cae de 25 puntos en un modelo a 8,6 en el siguiente, porque el baseline sin filtrar sube del 49 % al 79,5 %. El margen que deja el filtrado se redujo a un tercio en una generación de modelos. Cualquier decisión de arquitectura tomada aquí tiene fecha de caducidad corta.
La trampa de la caché de prompts
Aquí está lo que hace que un filtro top-K casero sea mala idea aunque funcionara.
Las definiciones de herramientas no van en cualquier sitio del prompt: van al principio. Anthropic documenta el orden del prefijo cacheable como tools, luego system, luego messages, con una jerarquía donde cada nivel se apoya en el anterior. Y la regla de invalidación es explícita: modificar definiciones de herramientas, sean nombres, descripciones o parámetros, invalida la caché entera, y un cambio en un nivel invalida ese nivel y todos los posteriores.
OpenAI dice lo mismo y añade un detalle: el prefijo debe permanecer sin cambios, y el orden cuenta. El mismo conjunto de herramientas servido en distinto orden es un fallo de caché.
Con vLLM sirviendo un modelo abierto no hay bloque de herramientas propio. El parámetro tools lo serializa la plantilla de chat dentro del primer mensaje de sistema, detrás del texto del system prompt. Para la caché automática de prefijo el efecto es idéntico, porque sigue estando cerca del inicio absoluto del prompt.
La consecuencia es la que importa. Un filtro que devuelve un conjunto distinto de herramientas en el turno siguiente no invalida solo el bloque de herramientas: invalida todo lo que va detrás, que es el system prompt y la conversación entera, que es justo la parte que crece con los turnos. En precios de lista, un token de prefijo pasa de costar 0,1 veces leído de caché a 1,25 veces escrito. Un agente con sesenta mil tokens de prefijo estable pasa de unos seis mil tokens equivalentes por turno a unos setenta y cinco mil, más el prefill que se vuelve a pagar en latencia.
Los dos proveedores grandes han llegado por separado a la misma solución, y es la contraria a filtrar en el prefijo. Anthropic excluye las herramientas diferidas del prefijo y, cuando el modelo descubre una por búsqueda, la inyecta en línea dentro de la conversación: “el prefijo queda intacto, así que la caché de prompts se preserva”. OpenAI: “todas las herramientas se cargan al final de la ventana de contexto del modelo (…) esto permite preservar la caché de una petición a otra”. La recomendación operativa de Anthropic es el patrón de núcleo estable: dejar sin diferir las tres a cinco herramientas más usadas.
El trabajo académico más cercano, de enero de 2026, enuncia el problema sin medirlo y recomienda exactamente eso, mantener un conjunto fijo de funciones reutilizables de propósito general e implementar la capacidad dinámica por generación de código.
La especificación de MCP de julio de 2026 recoge las dos mitades del asunto. Los servidores “deberían devolver las herramientas de tools/list en un orden determinista para permitir el cacheo en el cliente y mejorar la tasa de acierto de la caché de prompts del modelo”. Y añade ttlMs y cacheScope obligatorios en los resultados de listado, para que el cliente pueda congelar el catálogo durante una ventana conocida en lugar de reconsultarlo y propagar cualquier variación al prompt.
Nadie ha medido publicadamente el coste en euros de este efecto. El único intento que encontré es una auditoría autopublicada de diez mil turnos que lo cifra en un 2,4 % a 3,5 % de los fallos de caché, con metodología no replicable. Es una laguna de la literatura, y el orden de magnitud del párrafo anterior sale de aplicar los precios de lista, no de una medición.
Y ahora, LiteLLM
Con todo lo anterior en la cabeza, las palancas del gateway se leen de otra manera.
El filtro semántico no actúa en el endpoint MCP. Se registra como callback genérico de LiteLLM, y su único despachador es el hook de precall del proxy, que además descarta todo lo que no sea completion, acompletion o aresponses. El camino de tools/list no invoca ese hook en ningún punto; hay un comentario en el código que lo admite al pasar, al explicar por qué la firma JWT del listado se hace aparte. Un cliente MCP externo conectado a /mcp recibe el catálogo entero, con el filtro activado o sin él.
| Palanca | Sirve a clientes MCP externos | Rompe la caché de prompts |
|---|---|---|
allowed_tools por servidor | Sí | No, es estático |
| Overrides de nombre y descripción | Sí | No, es estático |
| Selección por ruta o cabecera | Sí | No, si es estable por agente |
/mcp/proxy y herramientas virtuales | Sí | No, la superficie es fija |
| Filtro semántico | No | Sí, reescribe el bloque por petición |
Cuando sí actúa, el filtro tiene detalles que hay que conocer antes de activarlo. Embebe solo la descripción de cada herramienta, con el nombre como respaldo si falta; el esquema de parámetros no entra. De la consulta embebe únicamente el último mensaje de usuario: en el turno doce de una conversación de agente, eso puede ser “sí, adelante”. Los embeddings de las herramientas se cachean e indexan al arranque; el de la consulta se calcula en cada petición, con una llamada síncrona dentro de una corrutina.
Sus modos de fallo son casi todos abiertos, que es lo razonable: si el modelo de embeddings no responde, si nada supera el umbral, si los nombres no casan con el catálogo, devuelve todas las herramientas. La excepción es el desbordamiento de contexto, que falla cerrado con un 400, y que si ocurre al construir el índice durante el arranque se memoriza y hace fallar todas las peticiones posteriores hasta que se reinicie. Los valores por defecto son text-embedding-3-small, top_k de 10 y umbral de 0,3, bajo litellm_settings.mcp_semantic_tool_filter.
/mcp/proxy sí sirve a clientes externos, y ahí está su valor real. Sustituye el catálogo por tres herramientas con identificadores opacos. Cómo busca search_tools merece una línea, porque no es lo que uno supone: por defecto no es expresión regular ni encaje semántico, es un contador de palabras clave, cuántos tokens de la consulta aparecen como subcadena en el nombre y la descripción concatenados. Solo pasa a embeddings si se configura un modelo en mcp_tool_search. El tope de resultados está escrito a fuego en cinco en este modo, y el cliente no puede pedir más. A cambio, call_tool pasa por las mismas comprobaciones de permisos que una llamada normal más una validación del esquema de argumentos que la ruta normal no hace en ese punto.
Hay una variante relacionada: con mcp_tool_search_enabled en los permisos de la clave, el catálogo se sustituye por cuatro herramientas virtuales, y ahí la de búsqueda sí expone top_k al cliente.
El orden del catálogo no es determinista, y eso rompe la caché del cliente sin que nadie filtre nada. La lista de servidores permitidos sale de iterar un conjunto de cadenas de Python, así que con la semilla de hash aleatoria el orden difiere entre procesos, entre workers de uvicorn y entre reinicios. No hay ninguna ordenación en el camino de listado, y no hay caché de herramientas: cada tools/list consulta a los upstream en vivo, de modo que el orden dentro de cada servidor también depende de lo que devuelva el upstream esa vez. El gateway no ofrece hoy la garantía que recomienda la especificación de julio. El arreglo, ordenar el catálogo antes de servirlo, cabe en una línea.
Tampoco hay paginación hacia el cliente. El catálogo se devuelve entero, siempre.
Y la palanca que casi nadie usa es la estática. Cada servidor MCP admite tool_name_to_display_name y tool_name_to_description, y esos overrides sustituyen de verdad lo que se envía al cliente en tools/list, no solo lo que ve el administrador en la interfaz. El enrutado de tools/call traduce del nombre nuevo al real antes de cualquier comprobación de permisos. Con eso se puede recortar una descripción de cuatrocientos tokens escrita por un tercero, renombrar una herramienta ambigua para que no compita con otra parecida, y hacerlo sin tocar la caché de prompts, porque es estático y vale para todos los turnos.
Es además la mitigación del rug pull que quedaba pendiente del apartado de seguridad: si la descripción que ve el modelo es la que ha escrito el operador, lo que cambie el servidor de aguas arriba deja de llegar al contexto. Dos límites. Se configuran por base de datos o por API de gestión, no por config.yaml. Y la traducción inversa del nombre es por igualdad exacta y no está desambiguada entre servidores, así que dos servidores con el mismo nombre de visualización enrutan de forma inestable, agravado por el orden no determinista del párrafo anterior.
Junto a eso, allowed_tools por servidor sigue siendo el instrumento romo y eficaz: filtra el listado que ve el cliente, reduce tokens, y es estático.
La escalera, en orden
- Recortar.
allowed_toolspor servidor, más lista blanca por clave. Barato, estático, no rompe la caché y reduce la superficie de permisos a la vez. - Reescribir las descripciones con los overrides. Es donde está la evidencia, porque la descripción pesa y el nombre casi no, y de paso fija el contrato frente al rug pull.
- Partir por ruta. Cada agente a
/{servidor}/mcpo a un conjunto de herramientas, nunca al endpoint agregado. Estable por agente, así que la caché sobrevive. - Ordenar el catálogo antes de servirlo si se controla el despliegue, mientras el gateway no lo haga.
/mcp/proxycuando el catálogo pase de treinta o cincuenta herramientas. Es divulgación progresiva y funciona con clientes externos, que es más de lo que se puede decir del filtro semántico.- El filtro semántico, solo cuando quien llama al modelo es LiteLLM. Con
top_kgeneroso, sabiendo que embebe únicamente el último mensaje de usuario y que el desbordamiento de contexto es un fallo cerrado y pegajoso. - Lo que no hay que hacer: un filtro top-K propio que reescriba el array
toolsen cada turno. Cuesta el prefijo entero y, según cuatro de las cinco estrategias medidas, probablemente empeore la selección.
El coste declarado, que vale cero
El cálculo tiene cuatro escalones de precedencia: coste fijado por un hook posterior a la llamada, coste por herramienta en tool_name_to_cost_per_query, coste por defecto del servidor, y 0.0.
Sin configurar mcp_server_cost_info, todo el tráfico MCP se contabiliza a cero y no consume presupuesto de clave ni de equipo.
Hay exactamente dos métricas de Prometheus para MCP:
litellm_mcp_tool_calls_totallitellm_mcp_tool_call_spend_metric
Ambas con las mismas ocho etiquetas: nombre de herramienta, nombre de servidor, hash de clave, alias de clave, equipo, alias de equipo, usuario y usuario final.
Y la segunda solo se incrementa si el coste es mayor que cero. Con la configuración por defecto, esa serie temporal nunca aparece en Prometheus. No es que valga cero: es que no existe.
Tampoco hay métrica de tools/list, ni de latencia MCP, ni de errores MCP, ni de salud de servidor upstream. Para eso quedan las métricas genéricas de petición.
En los informes, /spend/calculate no tiene rama MCP: acepta modelo y mensajes, o una respuesta de completado, y siempre llama al cálculo de coste de LLM. El informe por equipo sí incluye las llamadas MCP, agrupadas por la columna model, que para estas filas vale MCP: nombre_de_la_herramienta. Es decir: cada herramienta aparece como si fuera un modelo, con cero tokens de entrada y salida. Solo el endpoint de sesiones separa los dos tipos de llamada de forma explícita.
La observabilidad, y la traza que se corta
Hay dos pilas de OpenTelemetry en LiteLLM y cuál corre depende de una variable de entorno.
Sin LITELLM_OTEL_V2=true, corre el logger antiguo, que no tiene tratamiento de MCP: la única aparición de MCP en todo el fichero es la clave de metadatos en una lista. La llamada a herramienta sale como un span genérico litellm_request con el modelo puesto a MCP: nombre.
Con la v2 activada sí hay span propio, con rol dedicado, kind cliente y padre en el span de petición del proxy. El nombre del span es tools/call nombre-herramienta. Los atributos son correctos y siguen las convenciones:
| Atributo | Valor |
|---|---|
gen_ai.operation.name | execute_tool |
mcp.method.name | tools/call |
mcp.session.id | Identificador de sesión MCP |
gen_ai.tool.name | Nombre de la herramienta |
gen_ai.tool.call.arguments | Solo con captura de contenido activada |
gen_ai.tool.call.result | Solo con captura de contenido activada |
server.address, server.port | De la URL upstream, redactada |
litellm.cost.total | El coste que probablemente vale cero |
La captura de contenido está apagada por defecto. Sin ella no se ven ni los argumentos ni el resultado en la traza.
Un apunte para quien trabaje con convenciones semánticas: mcp.tool.name no existe. El registro MCP tiene exactamente cuatro atributos (mcp.method.name, mcp.session.id, mcp.protocol.version y mcp.resource.uri), los cuatro en desarrollo, y el nombre de la herramienta va en gen_ai.tool.name. LiteLLM lo hace bien. Y las convenciones GenAI se extrajeron a un repositorio propio que a día de hoy no tiene ningún release, así que no hay número de versión que citar.
En Langfuse, el mapeador vendor devuelve un diccionario vacío para cualquier span que no sea de llamada a LLM. Una llamada MCP aparece como span crudo con sus atributos, sin campos nativos de entrada, salida ni modelo.
La traza se corta en el gateway
El contexto entrante sí se lee. El traceparent que el cliente pone en params._meta, según la propuesta de propagación de la especificación, se extrae y se convierte en un enlace del span, nunca en su padre. El motivo está documentado en el código: parentear al trace remoto dejaría el span colgando de una traza cuya raíz nunca llega al backend. El baggage del cliente se descarta a propósito para evitar falsificación de atributos de identidad.
El contexto saliente no se propaga. No existe ninguna llamada de inyección de propagador en todo el árbol. Las cabeceras que salen hacia el servidor MCP upstream se construyen con autenticación y las cabeceras extra autorizadas, y nada más.
La consecuencia operativa: no hay traza de extremo a extremo. Si el servidor MCP de aguas arriba está instrumentado, sus spans viven en otra traza y no hay forma de correlacionarlos automáticamente.
Auditoría y el conflicto con privacidad
Una llamada a herramienta deja una fila de gasto normal, con tipo de llamada call_mcp_tool, gasto igual al coste por consulta, modelo MCP: nombre, cero tokens en las tres columnas de tokens, una columna dedicada mcp_namespaced_tool_name, y los campos habituales de clave, equipo, usuario y sesión.
Y en la columna metadata, en JSON, la estructura completa de la llamada: nombre, argumentos, resultado, servidor, nombre con espacio de nombres, identificador de sesión MCP, modo de autenticación y recurso del servidor con la URL redactada.
El problema
Esa estructura se escribe en la columna de metadatos sin pasar por el interruptor de privacidad. La asignación es directa y no está condicionada por la función que gobierna si se almacenan prompts y respuestas, que sí controla las columnas messages y response.
Y la función de redacción tampoco lo cubre: solo toca messages y response. Una búsqueda de MCP en el fichero de redacción no devuelve nada.
El resultado práctico: un operador que activa turn_off_message_logging por RGPD sigue enviando los argumentos completos de cada herramienta a todos los destinos de logging que tenga configurados. Al SIEM, a Langfuse, a S3.
Si un argumento lleva un DNI, una dirección, un número de expediente o una historia clínica, se ha exportado.
Hay un agravante que no depende de LiteLLM. La revisión vigente de la especificación introduce una extensión que permite espejar argumentos de herramienta en cabeceras HTTP visibles a balanceadores, proxies y cortafuegos de aplicación. La especificación recomienda no marcar así los parámetros sensibles, y es una recomendación, y la decide el servidor upstream.
El encaje con el ENS
El conflicto es real y tiene forma conocida: el registro de la actividad (op.exp.8) exige que la llamada quede trazada y sea no repudiable; la protección de datos de carácter personal (mp.info.1) exige que ese registro no se convierta en un repositorio secundario de datos personales con finalidad distinta. El argumento de la herramienta es simultáneamente la prueba de la acción y el dato. Redactarlo destruye el no repudio; guardarlo en claro crea el repositorio.
La salida defendible que se puede montar hoy: hash del argumento en el registro de auditoría, argumento en claro con cifrado (mp.info.3), retención separada y control de acceso propio. El hash sostiene el no repudio y el claro solo se abre bajo procedimiento.
Hay otras dos medidas que este material toca de lleno. La gestión de cambios (op.exp.5) porque el alta de un servidor MCP y, sobre todo, el cambio de la descripción de una herramienta ya aprobada son cambios en la configuración de seguridad. Y el mantenimiento y las actualizaciones (op.exp.4) porque, como se ve en el apartado siguiente, una versión antigua del gateway es un incumplimiento con nombre y número.
Dos avisos de honestidad. Los códigos van según la numeración del Anexo II del RD 311/2022 y hay que contrastarlos contra el texto vigente antes de citarlos en un documento de cumplimiento. Y sobre NIS2: a fecha de este artículo la directiva sigue sin transponer en España, con el anteproyecto de ley de coordinación y gobernanza de la ciberseguridad en tramitación y un dictamen motivado de la Comisión por el retraso. Lo exigible hoy en el sector público y sus proveedores es el ENS.
Lo que no queda registrado
Un hueco administrativo: el alta, la modificación y la baja de servidores MCP no generan registro de auditoría. Hay tres comentarios TODO en el código de gestión que lo dicen, uno de ellos con un if is_audit_logging_enabled(): pass. En el mismo bloque de borrado hay otros dos TODO sobre no limpiar los permisos huérfanos en claves y equipos.
Seguridad
La vulnerabilidad que hay que conocer
CVE-2026-42271, publicada el 8 de mayo de 2026. Afecta a LiteLLM desde 1.74.2 hasta versiones anteriores a 1.83.7. CVSS 3.1 de 8,8.
Los endpoints POST /mcp-rest/test/connection y POST /mcp-rest/test/tools/list, que existen para previsualizar un servidor MCP antes de guardarlo, aceptaban command, args y env del transporte stdio en el cuerpo de la petición. Una clave de bajo privilegio ejecutaba comandos arbitrarios en el host del proxy. Afecta también a Red Hat OpenShift AI en varias ramas.
Es la historia del artículo condensada en una línea: el gateway que se pone para contener MCP fue el agujero.
No viene sola. La rama 1.83 concentra otras tres graves:
| CVE | Componente | CVSS | Corregida en |
|---|---|---|---|
CVE-2026-42208 | Inyección SQL en la validación de clave, sin autenticación previa | 9,8 | 1.83.7 |
CVE-2026-35030 | Caché de userinfo OIDC con clave token[:20], colisión y suplantación | 9,1 | 1.83.0 |
CVE-2026-40217 | Ejecución remota por reescritura de bytecode en el endpoint de prueba de guardrails | 8,8 | sin versión registrada |
Más dos escaladas de privilegios corregidas en 1.83.10 y 1.83.14.
La conclusión operativa es corta: un despliegue por debajo de 1.83.14 está expuesto a ejecución remota o a una inyección SQL sin autenticar. La rama actual es 1.102.0.
Y una nota de contexto: el ecosistema MCP entero tiene el mismo patrón. La ejecución remota sin autenticación en MCP Inspector, la inyección de comandos en mcp-remote al conectar a un servidor no confiable, el DNS rebinding desactivado por defecto en los SDK de Python y TypeScript hasta finales de 2025, los escapes de directorio en los servidores de referencia. Conectar a un servidor MCP no confiable compromete al cliente, no solo al revés.
El rug pull que no cubre nadie
Documentado desde abril de 2025: un servidor puede cambiar la descripción de una herramienta después de que el cliente la haya aprobado. La descripción es lo que el modelo lee para decidir cuándo y cómo usarla, así que cambiarla es cambiar el comportamiento sin volver a pedir permiso.
La especificación vigente no lo aborda. Define notificación de cambio de lista y campos de frescura de caché, que son mecanismos de actualidad, no de integridad. El único aviso normativo cercano es que los clientes deben considerar las anotaciones de herramienta como no confiables salvo que vengan de servidores confiables.
LiteLLM tampoco lo cubre. El único “pinning” del código es del identificador de servidor de configuración, para que no cambie al editar el YAML. No hay hash ni comparación de la descripción ni del esquema de entrada entre listados sucesivos. Búsqueda de términos relacionados en el módulo MCP: nada funcional.
El control existe fuera del gateway. mcp-scan, de los mismos investigadores que documentaron el ataque, implementa fijado por hash de la descripción precisamente para detectar esto.
En términos de ENS es un cambio no autorizado en la configuración de seguridad que hoy no se detecta. Y el arreglo casero cabe en un cron: guardar el hash de descripción y esquema de cada herramienta aprobada, comparar contra tools/list periódicamente, alertar en la diferencia.
Tres huecos más en el código
Sin validación de URL hacia el upstream. El campo url de un servidor nuevo no tiene validador. Las utilidades de protección contra SSRF existen en el árbol y se usan en el descubrimiento OAuth y en la descarga de especificaciones OpenAPI, pero no en el egreso de tools/call. Un servidor apuntando a un rango privado o a la IP de metadatos de la nube se acepta tal cual.
El transporte stdio es ejecución local en el host del proxy. Hay una lista blanca de comandos: npx, uvx, python, python3, node, docker, deno, ampliable por variable de entorno. El comentario del propio código admite el riesgo residual: los runtimes permitidos pueden ejecutar código por argumentos. Con docker y npx dentro, es ejecución arbitraria de facto para cualquier administrador del proxy.
Dos modos de servidor saltan la autenticación del gateway. El paso directo y la delegación de autenticación al upstream omiten por completo la validación de clave virtual. Están bien endurecidos (fallo cerrado si el objetivo es mixto o no resoluble, comparación estricta contra valores truthy, bloqueo explícito del flujo de credenciales de cliente para no prestar la cuenta de servicio del proxy), y el resultado sigue siendo que esos servidores no tienen control de acceso del gateway.
El límite que trunca en silencio
La paginación de tools/list se detiene y devuelve lo acumulado en tres casos: cursor repetido, mil páginas, o vencimiento de un plazo global. El docstring lo declara: un upstream lento o defectuoso produce un catálogo parcial en lugar de un error.
Los dos primeros registran un aviso. El tercero, el plazo, sale sin registrar absolutamente nada. Un servidor lento hace que parte de sus herramientas desaparezcan del catálogo sin que quede rastro.
Corrección sobre la fuga de slots
En el artículo anterior di por buena la incidencia 34534, según la cual cada llamada a herramienta MCP adquiere un slot de max_parallel_requests y no lo libera. Trazando el código de la 1.102.0, ese diagnóstico ya no se sostiene: el camino de liberación existe, tanto en éxito como en fallo, y el limitador está registrado como callback de litellm y no solo como hook de proxy.
Con una honestidad: no encontré test de regresión que cubra el ciclo completo para MCP, y la confirmación definitiva es un proxy real con max_parallel_requests: 2 y cinco llamadas MCP seguidas. Si alguien lo reproduce hoy, me interesa saberlo.
La especificación se movió debajo
La revisión vigente de MCP es 2026-07-28, publicada el 28 de julio de 2026, y entre medias hubo otra, 2025-11-25. Los cambios de la última no son cosméticos:
- Se eliminan las sesiones de protocolo y la cabecera de sesión. El estado cruzado pasa a ser un identificador explícito en los argumentos de la herramienta.
- Se elimina el handshake de inicialización. Cada petición lleva versión y capacidades en
_meta. MCP pasa a ser sin estado. - Aparece
server/discovercomo llamada obligatoria. - La elicitación cambia de forma: fuera las peticiones iniciadas por el servidor, entra un patrón de varias vueltas donde el servidor devuelve un resultado de tipo “se requiere entrada” y el cliente reintenta con las respuestas.
- Se eliminan
ping,logging/setLevely la notificación de cambio de raíces. Se elimina la reanudación de streams SSE. - Cabeceras HTTP estándar obligatorias en POST, lo que permite a un gateway o a un cortafuegos de aplicación decidir sin parsear el JSON.
- En autorización: validación obligatoria del emisor en el cliente, y el registro dinámico de clientes queda deprecado en favor de documentos de metadatos de identificador de cliente.
- Raíces, muestreo y logging quedan deprecados, con ventana mínima de doce meses.
LiteLLM 1.102.0 anuncia 2025-06-18. Su enumeración de versiones tiene tres valores y ninguno es posterior a esa fecha. El literal se envía en el initialize contra el upstream.
Un matiz que no está mal del todo: la versión que el gateway anuncia a sus clientes no está fijada en el código de LiteLLM, la negocia el SDK. Y hay implementación parcial de la revisión intermedia, porque los manejadores de elicitación y muestreo la referencian en sus docstrings. Es decir: implementación parcial de una revisión mientras se anuncia otra anterior.
En interoperabilidad práctica no duele todavía, porque el ecosistema de clientes sigue mayoritariamente en las revisiones de 2025. Duele en planificación: el rediseño sin estado de julio invalida el modelo de sesiones del gateway, y eso es trabajo por delante.
Dos apuntes de ecosistema para quien esté eligiendo. Hay clientes que hablan streamable HTTP remoto de forma nativa (Claude y Claude Code, ChatGPT con OAuth y registro dinámico, VS Code con Copilot, Cursor, Zed, Kiro) y otros que siguen exigiendo stdio o un puente local con mcp-remote, entre ellos Windsurf, Cline y Continue.
La ejecución de herramientas desde el propio proxy
Además de servir MCP hacia fuera, el proxy puede consumirlo él mismo. Se activa con un bloque mcp en el cuerpo, con server_url igual al centinela litellm_proxy, y funciona tanto en /v1/responses como en /chat/completions.
El flujo: separa las herramientas del gateway, las lista, filtra por permitidas, deduplica, las traduce a formato de función de OpenAI y las concatena. Llama al modelo. Si hay llamadas a herramienta y la auto-ejecución está permitida, las ejecuta contra los servidores MCP. Construye el seguimiento y hace una segunda llamada.
Dos límites que hay que conocer antes de diseñar sobre esto.
El bucle es de un solo salto. No hay iteración multipaso. Se ejecutan las llamadas de la primera respuesta y se devuelve el seguimiento. Un plan que encadene herramientas exige que el cliente vuelva a llamar.
La auto-ejecución falla cerrado y en bloque. Exige que todas las referencias MCP tengan aprobación puesta a “nunca”; una sola que requiera aprobación desactiva la auto-ejecución de toda la petición.
Y un tercero, que es el que muerde: el estado multi-turno de /v1/responses se rehidrata desde la tabla de gasto. La consulta es literal, busca la sesión del request_id anterior y recupera todas las filas de esa sesión ordenadas por hora.
Con disable_spend_logs: true no se escribe nada, la consulta sale vacía y el historial se pierde en silencio, sin error 4xx. El propio código lo reconoce: no espera reintentos porque los despliegues que no escriben registros de gasto no tienen nada que esperar. Es un acoplamiento entre una opción de rendimiento y una función de conversación que nadie espera encontrar.
Salud y operación diaria
Los health checks existen, son bajo demanda y no son periódicos. GET /v1/mcp/server/health acepta identificadores repetidos y devuelve estado por servidor. La implementación abre un cliente y ejecuta una operación vacía dentro de la sesión, con un timeout de diez segundos.
Se salta el chequeo por completo si el servidor requiere autenticación por usuario, y esos devuelven estado desconocido siempre.
Lo sorprendente: las columnas de estado, última comprobación y error existen en la base de datos desde una migración de mayo de 2025, y la función de chequeo devuelve un objeto sin escribir en la base de datos. El estado se recalcula en cada petición. No hay trabajo periódico de salud de MCP.
Cuando un servidor upstream está caído, el comportamiento es degradar y no bloquear. El listado hace fan-out en paralelo y cada servidor que falla aporta lista vacía más un resultado clasificado en seis categorías. Esos resultados llegan al cliente en el _meta de la respuesta bajo una clave de vendor, sin filtrar texto ni URL del upstream. El motivo del diseño está en el código: una contribución vacía sin señal hace que un upstream roto sea indistinguible de un servidor sano sin herramientas.
No hay caché de la lista anterior. Un servidor caído desaparece de las herramientas de ese turno.
Sobre el alta y la baja sin reiniciar, hay dos mundos. Los servidores en base de datos se pueden crear, editar y borrar por API, y cada operación recarga el registro con intercambio atómico. La propagación entre réplicas va por publicación en Redis sobre el canal de cambio de configuración, si hay Redis, y por sondeo cada 30 segundos si no lo hay.
Los servidores declarados en config.yaml se cargan una sola vez, en el arranque. No hay relectura. Cambiarlos exige reinicio.
Y hay una trampa de precedencia entre los dos mundos, con dos avisos distintos en el código. Una fila de base de datos con el mismo identificador que una entrada de configuración oculta la de configuración por completo. Y al revés, un identificador de configuración que coincida con el nombre o alias de un servidor de base de datos captura sus permisos, porque los identificadores se resuelven antes que los nombres. Ambos casos emiten un aviso en el log, una vez.
Un config.yaml endurecido
general_settings:
# El acceso no es abierto por defecto, pero esto cierra la herencia
# clave-vacia hacia equipo, que es la que sorprende.
require_key_mcp_access_defined: true
require_end_user_mcp_access_defined: true
# Sin esto, allow_all_keys sobrescribe el ambito explicito de la clave.
mcp_allow_all_keys_respects_mcp_scope: true
litellm_settings:
# No cubre los argumentos MCP. Ver el apartado de auditoria.
turn_off_message_logging: true
callbacks: ["langfuse_otel"]
mcp_servers:
inventario:
url: https://mcp-inventario.interna.svc/mcp
transport: http
auth_type: bearer_token
auth_value: os.environ/MCP_INVENTARIO_TOKEN
# Lista blanca explicita. Se aplica tambien en tools/call.
allowed_tools:
- buscar_articulo
- consultar_stock
# Nunca a true en un servidor con datos.
allow_all_keys: false
access_groups: ["operaciones"]
timeout: 20
max_concurrent_requests: 8
mcp_info:
description: "Inventario de almacen, solo lectura"
# Sin esto, el gasto MCP es cero y su metrica no existe.
mcp_server_cost_info:
default_cost_per_query: 0.0005
tool_name_to_cost_per_query:
consultar_stock: 0.0002
Y en el entorno del proceso:
# Sin esto no hay span MCP, solo un litellm_request generico.
LITELLM_OTEL_V2=true
# Los hashes de bloque completos, no truncados, si se correlaciona
# con eventos del motor.
LITELLM_MCP_CLIENT_TIMEOUT=45
LITELLM_MCP_TOOL_LISTING_TIMEOUT=20
Para los clientes, la regla es no usar el endpoint agregado. Cada agente apunta a /{servidor}/mcp o a un conjunto de herramientas, o al modo proxy de tres herramientas si el catálogo es grande.
Checklist
- Comprobar la versión. Por debajo de 1.83.14 hay ejecución remota y una inyección SQL sin autenticar con CVE asignado.
- Inventariar qué servidores tienen
allow_all_keysy activar el ajuste que hace que respete el ámbito de la clave. - Auditar los grupos de acceso por separado, porque son aditivos por encima del techo de clave y equipo.
- Declarar por escrito el comportamiento de fallo abierto ante errores de resolución de permisos de herramienta.
- Medir el coste real: número de herramientas por servidor desde la fila de listado, y tokens del bloque
toolsdesde el cuerpo guardado de una petición. - Elegir una estrategia de catálogo antes de conectar el tercer servidor: modo proxy de tres herramientas, filtro semántico, o selección por ruta. No el endpoint agregado.
- Declarar
mcp_server_cost_infoaunque sea con un valor simbólico, o la métrica de gasto no existirá. - Activar el logger v2 de OTel, y decidir a conciencia si se activa la captura de contenido.
- Asumir que la traza se corta en el gateway y planificar la correlación con el upstream por otra vía.
- Tratar la columna de metadatos de la tabla de gasto como un repositorio de datos personales: cifrado, retención propia y control de acceso separado.
- Montar la comprobación de rug pull, porque no la hace nadie: hash de descripción y esquema de cada herramienta aprobada, comparación periódica contra el listado.
- Restringir por red el egreso hacia servidores MCP, porque el gateway no valida la URL de destino.
- No habilitar transporte stdio salvo necesidad, y si se habilita, revisar qué hay en la lista blanca de comandos.
- Si se usa
/v1/responsescon estado multi-turno, no activardisable_spend_logs.
Trampas y cosas que no son lo que parecen
- El acceso MCP no es abierto por defecto en la 1.102.0. Lo dije mal en el artículo anterior. Lo que abre es
allow_all_keyspor servidor. allow_all_keyssobrescribe el ámbito explícito de la clave salvo que se active un ajuste que viene desactivado.require_key_mcp_access_definedsolo cierra la herencia de clave a equipo. Los grants por grupo de acceso siguen siendo aditivos.- Ante un fallo indeterminado al resolver permisos de herramienta, el código falla abierto para claves y JWT.
- El coste por defecto de una llamada MCP es 0.0, y la métrica de gasto solo se emite si el coste es mayor que cero, así que la serie no existe.
- Las llamadas MCP aparecen en el informe por equipo como si fueran modelos, con nombre
MCP: herramientay cero tokens. - El logger de OTel con soporte MCP está apagado por defecto. Sin la variable de entorno no hay span MCP.
- No se propaga contexto de traza hacia el servidor MCP upstream. No hay traza de extremo a extremo.
mcp.tool.nameno existe en las convenciones semánticas. El nombre va engen_ai.tool.name.- La redacción de mensajes no cubre los argumentos de herramienta. Se escriben en claro en la tabla de gasto.
- El alta, la modificación y la baja de servidores MCP no generan registro de auditoría. Hay tres TODO en el código.
- El filtro semántico de herramientas no se aplica en
tools/list. Solo en/chat/completionsy/v1/responses. - El orden del catálogo no es determinista, porque sale de iterar un conjunto de cadenas. Rompe la caché de prompts del cliente aunque nadie filtre nada.
search_toolsdel modo proxy cuenta palabras clave por defecto, no usa expresiones regulares ni embeddings, y su tope de cinco resultados está escrito a fuego.- Los overrides de descripción sí sustituyen lo que ve el cliente, y son la única palanca de catálogo que no rompe la caché. Solo se configuran por base de datos o API.
spec_versionya no existe en el esquema de configuración./mcp/sseno usa transporte SSE, y el endpoint de mensajes que anuncia su objeto es código muerto.- La paginación del listado trunca en silencio al vencer el plazo, sin dejar ninguna línea de log.
- Una fila de base de datos oculta una entrada de configuración con el mismo identificador, y al revés un identificador de configuración puede capturar los permisos de un servidor de base de datos.
- Los servidores declarados en
config.yamlexigen reinicio. Solo los de base de datos se recargan en caliente. - El health check no escribe su resultado en la base de datos pese a que las columnas existen, y no hay trabajo periódico.
- El gateway no valida la URL del servidor MCP upstream. Las utilidades contra SSRF existen y no se usan en ese camino.
dockerynpxestán en la lista blanca de comandos stdio.- El bucle de herramientas de
/v1/responseses de un solo salto, ydisable_spend_logs: truerompe su multi-turno sin error. - LiteLLM anuncia la revisión de junio de 2025 del protocolo, dos por detrás de la vigente.
Cierre
Hay un hilo que recorre los seis artículos de este track y que aquí queda más a la vista que en ninguno. El gateway se elige por una función y acaba haciendo cinco, y cada función nueva llega con sus propios valores por defecto, que casi nunca son los que uno habría elegido.
En el caso de MCP la asimetría es doble. Por un lado, la parte que parece peligrosa está mejor construida de lo que se supone: el control de acceso se aplica en la ejecución y no solo en el listado, la jerarquía es de intersecciones, el reenvío de cabeceras es lista blanca, y el acceso por defecto es cerrado, al contrario de lo que yo mismo escribí hace dos días. Por otro, la parte que nadie mira está peor: los argumentos de las herramientas salen en claro saltándose el interruptor de privacidad, el gasto vale cero y su métrica no llega a existir, la traza se corta, y el cambio de descripción de una herramienta aprobada no lo detecta nadie en toda la cadena, ni la especificación ni el gateway.
La decisión de arquitectura que sale de esto es que la puerta MCP hay que tratarla como lo que es, una interconexión de sistemas con terceros, y no como una función más del proxy de LLM. Eso significa inventario propio, alta formal, control de cambios sobre la descripción de cada herramienta, aislamiento de red hacia el upstream y un registro que se diseñe sabiendo que va a contener datos personales.
Y significa aceptar que el coste que de verdad se paga por conectar quince servidores MCP no está en ninguna columna de la tabla de gasto. Está repartido en el prompt_tokens de cada llamada al modelo, en cada turno, y en la precisión que se pierde cuando el catálogo es demasiado grande para que el modelo elija bien. Ese número se puede calcular con lo que el gateway ya guarda. Lo primero es querer calcularlo.
Ver también
- Humanos y agentes en el mismo gateway — la otra cara del mismo problema, cuando el agente es una clase de tráfico y no una identidad.
- Enrutado por prefijo — el artículo hermano, donde la corrección va sobre lo que el gateway no hace.
- Claves virtuales, presupuestos y límites — la jerarquía de identidad sobre la que se apoyan estos permisos.
- LiteLLM y Langfuse: el par operativo — el camino de trazas al que llegan, o no llegan, los spans MCP.
- Cuando MCP crece: autenticación con Keycloak — la identidad de los servidores MCP propios, aguas arriba de este gateway.
- MCP por dentro y su observabilidad profunda — el protocolo, las seis primitivas y las convenciones semánticas.
- El contratista con la llave maestra: aislar agentes de IA — el aislamiento de red que este artículo da por necesario y no explica.
- Controles técnicos para ENS, 42001 y el AI Act — el marco de cumplimiento en el que encajan el registro de actividad y la gestión de cambios.
- Hardening de secretos en un stack LLM soberano — las credenciales upstream que el gateway custodia.
- El segundo vector de coste de los agentes IA — qué cuesta un bucle de herramientas que falla a medias.
Fuentes
- LiteLLM, MCP Gateway: https://docs.litellm.ai/docs/mcp.
- LiteLLM, código:
litellm/proxy/_experimental/mcp_server/server.py,mcp_server_manager.py,rest_endpoints.py,auth/user_api_key_auth_mcp.py,cost_calculator.py,faults/list_outcomes.py,litellm/proxy/management_endpoints/mcp_management_endpoints.py,litellm/proxy/spend_tracking/spend_tracking_utils.py,litellm/litellm_core_utils/redact_messages.py,litellm/integrations/otel/,litellm/responses/litellm_completion_transformation/session_handler.py: https://github.com/BerriAI/litellm. - LiteLLM, incidencia 34534, fuga de slot de concurrencia en llamadas MCP: https://github.com/BerriAI/litellm/issues/34534.
- NVD,
CVE-2026-42271, ejecución de comandos por los endpoints de prueba de MCP: https://nvd.nist.gov/vuln/detail/CVE-2026-42271. - NVD,
CVE-2026-42208, inyección SQL sin autenticación en la validación de clave: https://nvd.nist.gov/vuln/detail/CVE-2026-42208. - NVD,
CVE-2026-35030, colisión de clave de caché de userinfo OIDC: https://nvd.nist.gov/vuln/detail/CVE-2026-35030. - NVD,
CVE-2025-49596, ejecución remota en MCP Inspector: https://nvd.nist.gov/vuln/detail/CVE-2025-49596. - NVD,
CVE-2025-6514, inyección de comandos enmcp-remote: https://nvd.nist.gov/vuln/detail/CVE-2025-6514. - NVD,
CVE-2025-66416, protección de DNS rebinding desactivada por defecto en el SDK de Python: https://nvd.nist.gov/vuln/detail/CVE-2025-66416. - Invariant Labs, MCP Security Notification: Tool Poisoning Attacks (tool poisoning, rug pull, shadowing): https://invariantlabs.ai/blog/mcp-security-notification-tool-poisoning-attacks.
- Invariant Labs, Introducing mcp-scan (fijado de herramientas por hash): https://invariantlabs.ai/blog/introducing-mcp-scan.
- Model Context Protocol, changelog de la revisión 2026-07-28: https://modelcontextprotocol.io/specification/2026-07-28/changelog.
- Model Context Protocol, changelog de la revisión 2025-11-25: https://modelcontextprotocol.io/specification/2025-11-25/changelog.
- Model Context Protocol, Security Best Practices (confused deputy, indicadores de recurso, validación de audiencia): https://modelcontextprotocol.io/docs/2026-07-28/tutorials/security/security_best_practices.
- NSA, Model Context Protocol: Security Design Considerations for AI-Driven Automation, mayo de 2026: https://media.defense.gov/2026/Jun/02/2003943289/-1/-1/0/CSI_MCP_SECURITY.PDF.
- CISA y agencias aliadas, Careful Adoption of Agentic AI Services, mayo de 2026: https://www.cisa.gov/resources-tools/resources/careful-adoption-agentic-ai-services.
- Anthropic, Advanced tool use (coste en tokens de las definiciones, búsqueda de herramientas, precisión de selección): https://www.anthropic.com/engineering/advanced-tool-use.
- Anthropic, Code execution with MCP: https://www.anthropic.com/engineering/code-execution-with-mcp.
- Anthropic, Prompt caching (jerarquía
tools→system→messagese invalidación): https://platform.claude.com/docs/en/build-with-claude/prompt-caching. - Anthropic, Tool search tool (herramientas diferidas fuera del prefijo, umbral de 30 a 50 herramientas): https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-search-tool.
- OpenAI, Prompt caching (el orden de las herramientas cuenta): https://developers.openai.com/api/docs/guides/prompt-caching.
- OpenAI, Tool search (las herramientas descubiertas se cargan al final del contexto): https://developers.openai.com/api/docs/guides/tools-tool-search.
- vLLM, Automatic Prefix Caching: https://docs.vllm.ai/en/latest/features/automatic_prefix_caching.html.
- Microsoft, Use tools in chat (límite de 128 herramientas por petición en VS Code): https://code.visualstudio.com/docs/copilot/agents/agent-tools.
- RAG-MCP, prueba de estrés de 1 a 11.100 servidores candidatos, mayo de 2025: https://arxiv.org/abs/2505.03275.
- Benchmarking the Benchmarks, desalineación del 18,5 % entre etiqueta y realidad, junio de 2026: https://arxiv.org/html/2607.02577v1.
- MCP-Atlas, taxonomía de fallos sobre 220 herramientas y veinte modelos, febrero de 2026: https://arxiv.org/html/2602.00933v3.
- BiasBusters, sesgo de selección entre herramientas equivalentes y peso de la descripción frente al nombre: https://arxiv.org/html/2510.00307.
- MCP Tool Descriptions Are Smelly, auditoría de 856 herramientas de 103 servidores, febrero de 2026: https://arxiv.org/html/2602.14878v1.
- ToolMenuBench, seis estrategias de filtrado y 26.460 ejecuciones, junio de 2026: https://arxiv.org/html/2606.15508.
- Retrieval Models Aren’t Tool-Savvy (ToolRet), corpus de 43.000 herramientas, marzo de 2025: https://arxiv.org/abs/2503.01763.
- Tools Are Not Islands, brecha entre Recall@3 y conjunto completo, julio de 2026: https://arxiv.org/html/2607.25718.
- ToolFlood, saturación adversaria de la capa de recuperación, marzo de 2026: https://arxiv.org/html/2603.13950.
- Scores Are Not Decisions, el K óptimo como función del coste, julio de 2026: https://arxiv.org/html/2607.27083v1.
- Don’t Break the Cache, caché de prompts en tareas agénticas de horizonte largo, enero de 2026: https://arxiv.org/html/2601.06007v2.
- How Many Tools Should an LLM Agent See? A Chance-Corrected Answer, mayo de 2026: https://arxiv.org/html/2605.24660v1.
- Semantic Tool Discovery for LLMs, marzo de 2026: https://arxiv.org/abs/2603.20313.
- OpenTelemetry, convenciones semánticas de GenAI y MCP: https://github.com/open-telemetry/semantic-conventions-genai.
- Boletín Oficial del Estado, Real Decreto 311/2022, Esquema Nacional de Seguridad: https://www.boe.es/buscar/act.php?id=BOE-A-2022-7191.
- IBM ContextForge, gateway y registro MCP: https://github.com/IBM/mcp-context-forge.
- Docker MCP Gateway: https://github.com/docker/mcp-gateway.
- agentgateway: https://github.com/agentgateway/agentgateway.