Introducción #
El Model Context Protocol (MCP) es un protocolo estándar que permite a las aplicaciones de IA interactuar con herramientas y fuentes de datos externas de forma estructurada. Cuando se combina con Azure API Management (APIM), se crea una arquitectura robusta, segura y escalable para exponer servicios MCP a clientes y agentes de IA.
Este artículo explica cómo funciona esta arquitectura y los beneficios que aporta.
¿Qué es MCP (Model Context Protocol)? #
MCP es un protocolo abierto que estandariza la comunicación entre agentes de IA y herramientas externas. Permite que:
- Agentes de IA puedan descubrir y ejecutar herramientas disponibles
- Servidores MCP expongan funcionalidades específicas (búsquedas, APIs, bases de datos)
- Clientes MCP invoquen estas herramientas de forma estructurada
Componentes de MCP #
- MCP Server: Expone herramientas y recursos disponibles
- MCP Client: Conecta con servidores y ejecuta herramientas
- Herramientas (Tools): Funciones específicas que el servidor expone
- Recursos: Datos o contexto que el servidor puede proporcionar
¿Qué es Azure API Management (APIM)? #
Azure API Management es una puerta de enlace completamente administrada que permite:
- Publicar APIs de forma segura
- Aplicar políticas de seguridad, rate limiting, transformación
- Monitorizar y analizar el uso de las APIs
- Gestionar autenticación (API keys, OAuth 2.0, Entra ID)
- Versionar y documentar APIs
Arquitectura: MCP Server + APIM + WAF #
La arquitectura típica combina estos componentes:
Internet
│
│ HTTPS
▼
┌──────────────────────────┐
│ Azure Application │
│ Gateway + WAF │
│ │
│ ┌────────────────────┐ │
│ │ Reglas WAF: │ │
│ │ - OWASP Top 10 │ │
│ │ - Bot Protection │ │
│ │ - DDoS Protection │ │
│ │ - Geo-filtering │ │
│ └────────────────────┘ │
└───────────┬──────────────┘
│
┌───────────────┼───────────────┐
│ VNet (10.0.0.0/16) │
│ │ │
│ ▼ │
│ ┌────────────────────────┐ │
│ │ Subnet APIM │ │
│ │ (10.0.1.0/24) │ │
│ │ │ │
│ │ ┌──────────────────┐ │ │
│ │ │ Azure API │ │ │
│ │ │ Management │ │ │
│ │ │ (Internal Mode) │ │ │
│ │ │ │ │ │
│ │ │ ┌────────────┐ │ │ │
│ │ │ │ Políticas: │ │ │ │
│ │ │ │ - Auth │ │ │ │
│ │ │ │ - Rate Lim │ │ │ │
│ │ │ │ - Logging │ │ │ │
│ │ │ └────────────┘ │ │ │
│ │ └──────────┬───────┘ │ │
│ └─────────────┼──────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────┐ │
│ │ Subnet Backend │ │
│ │ (10.0.2.0/24) │ │
│ │ │ │
│ │ ┌──────────────────┐ │ │
│ │ │ MCP Server │ │ │
│ │ │ (App Service) │ │ │
│ │ │ + Private │ │ │
│ │ │ Endpoint │ │ │
│ │ └────────┬─────────┘ │ │
│ └───────────┼────────────┘ │
└──────────────┼────────────────┘
│
│ HTTPS (salida controlada)
▼
┌──────────────┐
│ API Externa │
│ (Setlist.fm)│
└──────────────┘
Beneficios de usar APIM con MCP #
1. Seguridad Mejorada #
- Múltiples métodos de autenticación: API keys, OAuth 2.0, Entra ID
- Protección del backend: El servidor MCP real nunca está expuesto directamente
- Validación de tokens: APIM valida tokens antes de reenviar solicitudes
- Secrets Management: Las API keys del backend se almacenan como Named Values seguros
2. Control de Acceso y Rate Limiting #
Se pueden aplicar políticas de acceso y rate limit en APIM.
Ejemplo de política de rate limit:
<!-- Limitar 100 llamadas por minuto por cliente -->
<rate-limit-by-key calls="100" renewal-period="60"
counter-key="@(context.Subscription.Key)" />
<!-- Cuotas mensuales -->
<quota-by-key calls="10000" renewal-period="2592000"
counter-key="@(context.Subscription.Key)" />
3. Observabilidad #
APIM proporciona visibilidad completa del tráfico y comportamiento de las APIs MCP:
- Application Insights: Integración nativa que registra automáticamente métricas de rendimiento, tiempos de respuesta, tasas de error y dependencias. Permite identificar cuellos de botella y optimizar el rendimiento.
- Logging estructurado: Todas las solicitudes y respuestas se registran con contexto completo (timestamp, usuario, operación, parámetros). Facilita la auditoría y el cumplimiento normativo.
- Trazabilidad end-to-end: Request IDs únicos que permiten seguir una solicitud a través de todos los componentes de la arquitectura (WAF → APIM → MCP Server → Backend API).
- Alertas proactivas: Configuración de alertas basadas en umbrales personalizados (latencia > 2s, tasa de error > 5%, cuota casi agotada), permitiendo respuesta rápida ante incidentes.
- Dashboards personalizables: Visualización en tiempo real de métricas clave de negocio y técnicas mediante Azure Monitor y Workbooks.
- Análisis de tendencias: Identificación de patrones de uso, picos de tráfico y comportamientos anómalos para planificación de capacidad.
4. Transformación y Enriquecimiento #
APIM actúa como una capa de mediación inteligente que puede modificar, enriquecer y adaptar el tráfico sin cambiar el código del servidor MCP o del cliente:
Casos de uso principales:
- Normalización de formatos: Convertir entre diferentes versiones de protocolo o esquemas de datos
- Enriquecimiento contextual: Añadir información del tenant, usuario o sesión a las solicitudes
- Filtrado de datos sensibles: Eliminar campos confidenciales de las respuestas antes de enviarlas al cliente
- Adaptación de protocolos: Transformar entre REST, SOAP, GraphQL según necesidades del cliente
- Inyección de metadatos: Agregar versiones de API, timestamps, o identificadores de correlación
- Validación de esquemas: Verificar que requests/responses cumplan con el contrato definido
Ejemplos de transformación:
<!-- Añadir headers personalizados -->
<set-header name="X-Tenant-Id" exists-action="override">
<value>@(context.User?.Id)</value>
</set-header>
<!-- Transformar respuestas -->
<set-body>
@{
var response = context.Response.Body.As<JObject>();
response["apiVersion"] = "1.0";
return response.ToString();
}
</set-body>
5. Resiliencia #
APIM implementa patrones de diseño que garantizan alta disponibilidad y recuperación ante fallos:
Patrones de resiliencia implementados:
- Retry automático: Reintentos configurables con backoff exponencial ante errores transitorios (timeouts, errores 5xx)
- Circuit breaker: Prevención de cascada de fallos al detener temporalmente llamadas a servicios que están fallando
- Timeout configurables: Límites de tiempo para evitar que solicitudes lentas bloqueen recursos
- Fallback responses: Respuestas alternativas cuando el backend no está disponible (respuestas en caché, respuestas por defecto)
- Health checks: Monitorización continua del estado de los backends para enrutamiento inteligente
- Balanceo de carga: Distribución de tráfico entre múltiples instancias del MCP Server para evitar sobrecarga
- Degradación elegante: El servicio continúa funcionando con capacidades reducidas en caso de fallo parcial
Ejemplos de configuración:
<!-- Retry automático -->
<retry condition="@(context.Response.StatusCode >= 500)"
count="3" interval="1" delta="1">
<forward-request />
</retry>
<!-- Circuit breaker -->
<cache-lookup-value key="circuit-breaker" variable-name="circuitState" />
6. Versionado y Compatibilidad #
APIM facilita la evolución del MCP Server sin interrumpir a los clientes existentes:
Estrategias de versionado:
- Múltiples versiones simultáneas: v1, v2, v3 del mismo MCP Server pueden coexistir, permitiendo a cada cliente actualizar a su ritmo
- Versionado por URL: Rutas diferenciadas (
/v1/mcp,/v2/mcp) que enrutan a diferentes backends o aplican transformaciones distintas - Versionado por header: Clientes especifican versión deseada mediante headers HTTP (ej:
API-Version: 2.0) - Versionado por query string: Parámetro de versión en la URL (ej:
?api-version=2024-12-01)
Beneficios de gestión de versiones:
- Migración gradual: Los clientes pueden actualizar cuando estén listos, sin forzar actualizaciones masivas
- Testing en producción: Nuevas versiones pueden probarse con un subconjunto de usuarios (canary deployments)
- Rollback instantáneo: Si una nueva versión presenta problemas, se puede revertir cambiando el enrutamiento en APIM sin redesplegar código
- Deprecación controlada: Políticas pueden advertir a clientes sobre versiones obsoletas e incluso forzar migración tras un período de gracia
- Documentación por versión: Cada versión mantiene su propia documentación OpenAPI/Swagger
- Compatibilidad hacia atrás: Transformaciones en APIM pueden mantener compatibilidad con clientes legacy mientras se moderniza el backend
7. Costos Optimizados #
APIM ayuda a reducir costos operativos y de infraestructura de múltiples formas:
Optimizaciones de infraestructura:
- Consolidación de servicios: Un solo APIM puede exponer múltiples MCP servers, reduciendo la necesidad de infraestructura duplicada para cada servicio
- Caché inteligente: Respuestas frecuentes se almacenan en caché (configurable por operación), reduciendo drásticamente las llamadas al backend y mejorando tiempos de respuesta
- Reducción de tráfico: Compresión automática de respuestas (gzip, brotli) reduce consumo de ancho de banda
- Escalado automático: APIM escala según demanda, pagando solo por la capacidad necesaria
Optimizaciones de costos de APIs externas:
- Menor consumo de APIs de pago: El caché reduce llamadas a APIs externas (como Setlist.fm), disminuyendo costos por volumen
- Rate limiting inteligente: Previene sobreconsumoo accidental que podría generar cargos inesperados
- Batching de solicitudes: Políticas pueden agrupar múltiples solicitudes en una sola llamada al backend
Eficiencia operativa:
- Menos mantenimiento: Centralización de seguridad, logging y políticas reduce complejidad operativa
- Reutilización de componentes: Políticas, backends y configuraciones se comparten entre APIs
- Reducción de tiempo de desarrollo: Capacidades empresariales (autenticación, rate limiting, transformación) sin código custom
- Menos incidentes: Patrones de resiliencia y observabilidad reducen tiempo de inactividad y tiempo medio de resolución (MTTR)
Modelo de costos predecible:
- APIM ofrece SKUs con precios fijos mensuales, facilitando la planificación presupuestaria
- Métricas detalladas permiten identificar operaciones costosas y optimizarlas
- Cuotas por suscripción previenen uso excesivo y costos desbordados
Beneficios de la Privatización #
A la hora de nuestra infraestructura recomiendo utilizar una infraestructura en HUB and Spoke con todos los recursos privatizados. Hub and Spoke permite centralizar nuestra infraestructura con el consiguiente ahorro de costes.
-
Seguridad Máxima
- Sin exposición directa a internet
- Tráfico completamente dentro de VNet
- Control granular con NSGs
-
Cumplimiento Normativo
- GDPR, HIPAA, PCI-DSS
- Auditorías simplificadas
- Trazabilidad de red completa
-
Reducción de Superficie de Ataque
- Servicios backend inaccesibles desde internet
- WAF como único punto de entrada
- Segmentación de red por capas
-
Prevención de Exfiltración de Datos
- Azure Firewall controla salidas
- Logs de todas las conexiones
- Bloqueo de destinos no autorizados
-
Mejora del Rendimiento
- Latencia reducida (tráfico interno)
- No depende de internet público
- QoS garantizada
Flujo de Tráfico en Entorno Privatizado #
- Cliente externo → WAF (IP pública)
- WAF valida y filtra → APIM (IP privada 10.0.1.x)
- APIM aplica políticas → MCP Server via Private Endpoint (IP privada 10.0.2.x)
- MCP Server → API externa via Azure Firewall (controlado)
- Respuesta sigue el camino inverso
Todo el tráfico entre componentes internos permanece en la VNet, sin salir a internet.
Service Endpoints vs Private Endpoints #
| Característica | Service Endpoints | Private Endpoints |
|---|---|---|
| Dirección IP | IP pública del servicio | IP privada en VNet |
| Tráfico | Por backbone Azure (no internet) | 100% privado en VNet |
| DNS | No requiere cambios | Requiere Private DNS Zone |
| Costo | Gratis | ~$7/mes por endpoint |
| Seguridad | Buena | Excelente |
| Aislamiento | Por subnet | Por recurso individual |
| Recomendado para | Dev/Test | Producción |
Desplegando nuestro agente #
SetList FM #
La API de setlist.fm es una API REST pública que proporciona acceso a una extensa base de datos de setlists de conciertos en vivo de artistas de todo el mundo. Lo hemos utilizado como ejemplo, en un entorno productivo real se usará una MCP programático.
Flujo de Comunicación #
Queda fuera de este documento el despliegue de la infraestructura, aunque detallare los componentes y su utilización.
Los componentes que deberás de desplegar son:
- Application Gateway: Se utilizará para exponer de forma pública nuestra aplicación. Será desplegado en una red virtual (VNet) de Azure.
- WAF Policy: Define reglas de seguridad para el Web Application Firewall y protege frente a vulnerabilidades web comunes.
- APIM: Azure API Management configurado en modo interno para gestionar, securizar y monitorizar el tráfico de APIs entre los componentes.
- DNS Private Resolver: habilita la resolución DNS para los endpoints privados dentro de la red virtual.
- Azure Private Endpoint: Proporciona conectividad privada a servicios de Azure a través de la red virtual.
Configuración de APIM #
Para empezar crearemos la API en APIM que haga llamadas a SetList FM con la siguiente configuración:
Configuración de APIM para publicar MCP #
En APIM, pulsaremos en MCP Server y “Expose an API as MCP Server”
Indicaremos el API añadido en el punto 1, nombre y descripción:
En API Operations indicaremos las Tools que tendremos disponible, para esta demo he seleccionado Search for Artist y Search for SetList.
En este punto ya tenemos APIM configurado para servir respuestas, pero todavía no lo hemos securizado. Cualquier usuario con el JSON de configuración de MCP puede agregar y explotar este servidor.
Configuración del Cliente MCP #
Para este ejemplo he utilizado VS Code, aunque se podría utilizar cualquier otro aplicativo que conecte con agentes.
El cliente MCP se configura para conectarse al endpoint de APIM en lugar del servidor MCP directamente:
Para configurar el agente necesitamos la URL de APIM (Luego mas adelante añadiremos el APP Gateway y el WAF delante).
También configuraremos una Subscription Key para acceder a nuestra API (en APIM):
Recuerda: El Header Name lo agregamos cuando dimos de alta el API (en el paso 1). Este valor actua como API KEY, pero como hemos comentado antes, si tenemos el JSON de configuración de agente MCP, tenemos acceso a la API. Mas adelante añadiremos una nueva capa de seguridad mediante autenticación OAuth.
Para configurar VS Studio, crearemos un fichero mcp.json dentro de .vscode con el siguiente contenido:
{
"servers": {
"setlistfm": {
"url": "https://demoapim.sbsconsulting.es/setlistfm-mcp/mcp",
"type": "http"
"headers": {
"Ocp-Apim-Subscription-Key": "afde7e15641c4c27819e567faff46658"
}
}
}
}
Cuando guardemos el fichero lo arrancaremos en start o en el menu contextual MCP Server: Start Server:
Una vez arrancado podemos ver las tools:
y ya podemos probar a hacer una pregunta para verificar el correcto funcionamiento:
#searchForArtists coldplay
Cuando el agente nos pida permiso para ejecutarse, lo permitimos.
Descubrimiento de Herramientas en VSCode: #
Cuando el cliente MCP se conecta:
- Cliente → envía solicitud
tools/lista APIM - APIM → aplica políticas de seguridad (valida token OAuth/API key)
- APIM → reenvía solicitud al MCP Server backend
- MCP Server → responde con lista de herramientas disponibles
- APIM → aplica políticas de salida (logging, transformación)
- Cliente ← recibe lista de herramientas
Proceso de Invocación de Herramientas #
Cuando el agente de IA necesita ejecutar una herramienta:
- Cliente → envía
tools/callcon parámetros - APIM → valida autenticación y rate limits
- APIM → registra la solicitud en Application Insights
- MCP Server → ejecuta la herramienta (ej: buscar artistas en Setlist.fm)
- Backend API → procesa la solicitud real
- MCP Server → formatea respuesta en formato MCP
- APIM → aplica políticas de salida
- Cliente ← recibe resultado estructurado
Políticas de APIM para MCP #
Las políticas de APIM añaden capas de funcionalidad al servidor MCP:
Política de Entrada (Inbound) #
<policies>
<inbound>
<base />
<trace source="inbound-start">
<message>@("API Inbound processing started for request: " + context.RequestId + " | Method: " + context.Request.Method + " | URL: " + context.Request.Url.ToString())</message>
</trace>
<set-header name="Authorization" exists-action="override">
<value>Bearer {{setlistfm-api-key}}</value>
</set-header>
<set-header name="x-api-key" exists-action="override">
<value>{{setlistfm-api-key}}</value>
</set-header>
<set-header name="Accept" exists-action="override">
<value>application/json</value>
</set-header>
<set-header name="User-Agent" exists-action="override">
<value>setlistfm-mcp/1.0</value>
</set-header>
<set-variable name="requestUrl" value="@{
return context.Request.Url != null ? context.Request.Url.ToString() : "";
}" />
<set-variable name="requestBody" value="@{
return context.Request.Body != null ? context.Request.Body.As<string>(preserveContent: true) : "";
}" />
</inbound>
<backend>
<base />
</backend>
### Política de Salida (Outbound)
```xml
<outbound>
<base />
<!--<cache-store duration="600" />-->
<set-variable name="responseBody" value="@{
return context.Response.Body != null ? context.Response.Body.As<string>(preserveContent: true) : "";
}" />
<trace source="SucceedInformation" severity="error">
<message>API SucceedInformation</message>
<metadata name="Request.URL" value="@((string)context.Variables["requestUrl"])" />
<metadata name="Request.Body" value="@((context.Request.Body == null || String.IsNullOrEmpty((string)context.Variables["requestBody"])) ? "-none-" : (string)context.Variables["requestBody"])" />
<metadata name="Response.Body" value="@((context.Response.Body == null || String.IsNullOrEmpty((string)context.Variables["responseBody"])) ? "-none-" : (string)context.Variables["responseBody"])" />
</trace>
</outbound>
Política de manejo de errores #
<on-error>
<base />
<trace source="error-occurred">
<message>@("ERROR in request " + context.RequestId + " | Error: " + context.LastError.Message + " | Source: " + context.LastError.Source + " | Reason: " + context.LastError.Reason)</message>
</trace>
<set-variable name="requestBody" value="@{
return context.Request.Body != null ? context.Request.Body.As<string>(preserveContent: true) : "";
}" />
<set-variable name="responseBody" value="@{
return context.Response.Body != null ? context.Response.Body.As<string>(preserveContent: true) : "";
}" />
<trace source="ErrorInformation" severity="error">
<message>API aErrorInformation</message>
<metadata name="Request.URL" value="@((string)context.Variables["requestUrl"])" />
<metadata name="Request.Body" value="@((context.Request.Body == null || String.IsNullOrEmpty((string)context.Variables["requestBody"])) ? "-none-" : (string)context.Variables["requestBody"])" />
<metadata name="Response.Body" value="@((context.Response.Body == null || String.IsNullOrEmpty((string)context.Variables["responseBody"])) ? "-none-" : (string)context.Variables["responseBody"])" />
</trace>
</on-error>
</policies>
Explicación de la Política #
Esta política implementa un sistema completo de logging y trazabilidad para el servidor MCP:
Sección Inbound (Entrada):
- Tracing inicial: Registra el inicio del procesamiento con el Request ID, método HTTP y URL completa para seguimiento
- Headers para Setlist.fm:
- Inyecta el
Authorizationheader con el Bearer token usando la API key almacenada en Named Values ({{setlistfm-api-key}}) - Configura
x-api-keycomo header alternativo para autenticación - Define
Accept: application/jsonpara especificar el formato de respuesta deseado - Establece
User-Agentpersonalizado para identificar las llamadas
- Inyecta el
- Captura de datos: Almacena la URL y el body de la request en variables para logging posterior (usando
preserveContent: truepara no consumir el stream)
Sección Backend:
- Usa la configuración base sin modificaciones adicionales, delegando al backend configurado
Sección Outbound (Salida):
- Captura de respuesta: Almacena el body de la respuesta en una variable para logging
- Logging de éxito: Registra mediante
tracetoda la información de la transacción:- URL de la solicitud
- Body de la request (o “-none-” si está vacío)
- Body de la response (o “-none-” si está vacío)
- Cache comentado: Hay una directiva de caché deshabilitada que podría almacenar respuestas durante 600 segundos (10 minutos)
Sección On-Error (Manejo de Errores):
Cuando ocurre un error:
- Logging detallado del error: Registra el Request ID, mensaje de error, fuente y razón del fallo
- Captura de contexto: Obtiene request y response bodies (si existen) para diagnóstico
- Trace estructurado: Registra toda la información para análisis post-mortem:
- URL donde ocurrió el error
- Request body enviado
- Response body recibido (si hay)
Beneficios de esta política:
- ✅ Trazabilidad completa: Cada request tiene un ID único que se puede seguir en los logs
- ✅ Debugging facilitado: En caso de error, se tiene acceso al request/response completo
- ✅ Auditoría: Registro de todas las interacciones con el backend
- ✅ Seguridad: Las API keys nunca se exponen al cliente, se inyectan desde Named Values seguros
- ❌ Overhead: Capturar y registrar todos los bodies puede impactar performance en requests grandes
Recomendación: Para producción, considera habilitar el caché (
<cache-store>) para reducir llamadas al backend y mejorar tiempos de respuesta.
Ejemplo de implementación con OAuth #
Para este ejemplo añadiremos la siguiente política y haremos llamadas desde una aplicación Python:
<policies>
<inbound>
<validate-jwt header-name="Authorization" failed-validation-httpcode="401" failed-validation-error-message="Unauthorized due Benoit APIM Policy" require-expiration-time="true" require-scheme="Bearer" require-signed-tokens="true">
<openid-config url="https://login.microsoftonline.com/OAUTH_TENANT_ID/v2.0/.well-known/openid-configuration" />
<audiences>
<audience>api://OAUTH_APP_ID</audience>
</audiences>
<issuers>
<issuer>https://sts.windows.net/OAUTH_TENANT_ID/</issuer>
</issuers>
</validate-jwt>
<!-- Set the subscription key header for the backend service -->
<set-header name="Ocp-Apim-Subscription-Key" exists-action="override">
<value>SETLISTAPI_SUBSCRIPTION_KEY</value>
</set-header>
<base />
</inbound>
</policies>
La aplicación Python carga dos variables de entorno, SETLISTAPI_MCP_ENDPOINT que contiene la URL de nuestro agente MCP y SETLISTAPI_SUBSCRIPTION_KEY que contiene la Key de la Subscription creada anteriormente. Finalmente hace dos peticiones: “🔗 Search for artists with ‘Coldplay’ in the name” y “🔗 Get a list of setlists for {artistName}”
import asyncio
import os
from dotenv import load_dotenv
from fastmcp.client import Client
from fastmcp.client.transports import StreamableHttpTransport
from render import render_artist_table, render_setlist
load_dotenv()
SETLISTAPI_MCP_ENDPOINT = str(os.getenv("SETLISTAPI_MCP_ENDPOINT"))
SETLISTAPI_SUBSCRIPTION_KEY = str(os.getenv("SETLISTAPI_SUBSCRIPTION_KEY"))
print(f"🔗 Testing connection to {SETLISTAPI_MCP_ENDPOINT}...")
async def main():
try:
async with Client(transport=StreamableHttpTransport(
SETLISTAPI_MCP_ENDPOINT,
headers={"Ocp-Apim-Subscription-Key": SETLISTAPI_SUBSCRIPTION_KEY},
), ) as client:
assert await client.ping()
print("✅ Successfully authenticated!")
tools = await client.list_tools()
print(f"🔧 Available tools ({len(tools)}):")
for tool in tools:
print(f" - {tool.name}")
# print(f" {tool.description}")
print(f" Input Schema: {tool.inputSchema}")
print("-------" * 18)
print("🔗 Search for artists with 'Coldplay' in the name")
searchForArtists = await client.call_tool(
"searchForArtists", arguments={'artistName': 'Coldplay'}
)
artist_payload = searchForArtists.content[0].text if searchForArtists.content else ""
print(render_artist_table(artist_payload))
print("-------" * 18)
artistName = "Linkin Park"
print(f"🔗 Get a list of setlists for {artistName}")
searchForSetlists = await client.call_tool(
"searchForSetlists", arguments={'artistName': artistName, 'p': 1}
)
setlist_payload = searchForSetlists.content[0].text if searchForSetlists.content else ""
print(render_setlist(setlist_payload))
except Exception as e:
print(f"❌ failure : {e}")
raise
finally:
print("👋 Closing client...")
await client.close()
if __name__ == "__main__":
asyncio.run(main())
Al ejecutarla podemos observar la siguiente salida:
Ahora implementamos la política:
<validate-jwt header-name="Authorization" failed-validation-httpcode="401" failed-validation-error-message="Unauthorized due Benoit APIM Policy" require-expiration-time="true" require-scheme="Bearer" require-signed-tokens="true">
<openid-config url="https://login.microsoftonline.com/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx/v2.0/.well-known/openid-configuration" />
<audiences>
<audience>api://77f06ac8-2346-4325-9650-9561b8dff872</audience>
</audiences>
<issuers>
<issuer>https://sts.windows.net/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx/</issuer>
</issuers>
</validate-jwt>
<!-- Set the subscription key header for the backend service -->
<set-header name="Ocp-Apim-Subscription-Key" exists-action="override">
<value>afde7e15641c4c27819e567faff46658</value>
</set-header>
Volvemos a ejecutar la llamada desde el código Python y vemos como nos da un error de autenticación:
El error se produce porque ahora la llamada al MCP publicado en el APIM hay que autenticarlo. El siguiente código python implementa la autenticación.
import asyncio
import os
from dotenv import load_dotenv
from fastmcp.client import Client
from fastmcp.client.transports import StreamableHttpTransport
from render import render_artist_table, render_setlist
load_dotenv()
SETLISTAPI_MCP_ENDPOINT = str(os.getenv("SETLISTAPI_MCP_ENDPOINT"))
SETLISTAPI_SUBSCRIPTION_KEY = str(os.getenv("SETLISTAPI_SUBSCRIPTION_KEY"))
print(f"🔗 Testing connection to {SETLISTAPI_MCP_ENDPOINT}...")
async def azure_default_credential_token():
print("Using DefaultAzureCredential")
from azure.identity import DefaultAzureCredential
credential = DefaultAzureCredential()
scope = f"api://{os.getenv("OAUTH_APP_ID")}/.default"
access_token = credential.get_token(scope)
return access_token.token
async def azure_client_secret_credential_token():
print("Using ClientSecretCredential")
from azure.identity import ClientSecretCredential
client_id = os.getenv("OAUTH_APP_ID", "")
# az ad app credential reset --id xxxxxxx
client_secret = os.getenv("OAUTH_CLIENT_SECRET", "")
tenant_id = os.getenv("OAUTH_TENANT_ID", "")
credential = ClientSecretCredential(tenant_id, client_id, client_secret)
scope = f"api://{os.getenv("OAUTH_APP_ID")}/.default"
access_token = credential.get_token(scope)
return access_token.token
async def msal_token():
print("Using MSAL ConfidentialClientApplication")
from msal import ConfidentialClientApplication
scope = f"api://{os.getenv("OAUTH_APP_ID")}/.default"
client_id = os.getenv("OAUTH_APP_ID")
client_secret = os.getenv("OAUTH_CLIENT_SECRET")
tenant_id = os.getenv("OAUTH_TENANT_ID")
app = ConfidentialClientApplication(
client_id,
client_credential=client_secret,
authority=f"https://login.microsoftonline.com/{tenant_id}"
)
result = app.acquire_token_for_client(scopes=[scope])
return result.get("access_token")
async def main(access_token: str):
print("👋 Starting client...")
print(f"Using access token: {access_token}")
try:
async with Client(transport=StreamableHttpTransport(
SETLISTAPI_MCP_ENDPOINT,
headers={"Authorization": f"Bearer {access_token}"},
), ) as client:
assert await client.ping()
print("✅ Successfully authenticated!")
tools = await client.list_tools()
print(f"🔧 Available tools ({len(tools)}):")
for tool in tools:
print(f" - {tool.name}")
# print(f" {tool.description}")
print(f" Input Schema: {tool.inputSchema}")
print("🔗 Search for artists with 'Coldplay' in the name")
searchForArtists = await client.call_tool("searchForArtists", arguments={'artistName': 'Coldplay'})
print(render_artist_table(searchForArtists.content[0].text))
print("🔗 Get a list of setlists for Blondshell")
searchForSetlists = await client.call_tool("searchForSetlists", arguments={'artistName': 'Wolf Alice', 'p': 1})
print(render_setlist(searchForSetlists.content[0].text))
except Exception as e:
print(f"❌ failure : {e}")
raise
finally:
print("👋 Closing client...")
await client.close()
if __name__ == "__main__":
# check the arguments to choose the token method
import sys
access_token = ""
if len(sys.argv) > 1:
method = sys.argv[1]
if method == "default_credential":
access_token = asyncio.run(azure_default_credential_token())
elif method == "client_secret":
access_token = asyncio.run(azure_client_secret_credential_token())
elif method == "msal":
access_token = asyncio.run(msal_token())
else:
print("Unknown method. Use 'default_credential', 'client_secret' or 'msal'.")
sys.exit(1)
else:
print("No method provided. Use 'default_credential', 'client_secret' or 'msal'.")
sys.exit(1)
asyncio.run(main(access_token))
Antes de hacer la llamada, tenemos que autenticarnos. Lo hacemos mediante az login con un usuario con permisos y hacemos la llamada desde esta implementación de Python, pasándole las credenciales por defecto (las coge de az login).
FAQ #
¿Por que requiero Set Header y OpenID config en mi politica? ❗ Sin el set-header, tu API exigirá dos autenticaciones simultáneas.
- JWT de Entra ID
- Subscription Key
A menos que desactives “Subscription required” en el producto APIM o pases la API Key desde código al hacer la llamada.
¿Donde obtengo los valores para la política Oauth
Tienes que registrar una Azure Application.