Azure API Management acts as the AI Gateway for MCP servers.
This sample implements the latest MCP Authorization specification and demonstrates how to route multiple MCP server backends through a single APIM gateway with unified OAuth 2.0 authentication.
This is a sequence diagram to understand the flow.
This sample deploys three MCP server backends behind the APIM gateway, plus a fourth remote-hosted backend:
| Backend | Compute | APIM Path | Description |
|---|---|---|---|
| MCP Functions | Azure Functions (Python) | /mcp/sse |
Sample tools (hello_mcp, get_snippet, save_snippet) |
| Slack MCP | Azure Container App | /slack-mcp/sse |
Slack workspace integration via slack-mcp-server |
| Jira MCP | Azure Container App | /jira-mcp/sse |
Jira/Atlassian integration via mcp-atlassian |
| GitHub MCP | GitHub-hosted (remote) | N/A | GitHub integration via GitHub Copilot MCP — no APIM proxy needed |
All APIM-proxied backends share the same OAuth 2.0 authorization flow managed by APIM. MCP clients authenticate once and can access any backend.
The repo also includes an AI Foundry Agent Demo (agent-demo/) that connects all four MCP backends to a single conversational agent. See Agent Demo below.
Before deploying, you need the following accounts, credentials, and tooling.
- Azure Developer CLI (
azd) v1.5+ - Azure CLI (
az) - An Azure subscription with Contributor access
- Python 3.10+
- Go to api.slack.com/apps and click Create New App → From scratch.
- Under OAuth & Permissions, add the following Bot Token Scopes:
channels:history,channels:read,chat:write,groups:history,groups:readim:history,im:read,mpim:history,mpim:read,users:read
- Click Install to Workspace and authorize.
- Copy the Bot User OAuth Token (starts with
xoxb-).
- Go to id.atlassian.com/manage-profile/security/api-tokens.
- Click Create API token, give it a label, and copy the token.
- Note your Jira instance URL (e.g.
https://yourorg.atlassian.net) and the email address you log in with.
If you plan to use the Agent Demo with GitHub tools:
- Go to github.com/settings/tokens → Generate new token (classic) or Fine-grained token.
- Grant scopes for the operations you need (e.g.
repo,read:org,read:user). - Copy the token.
If you plan to use the Agent Demo:
- Create an Azure AI Foundry project.
- Deploy a gpt-4o model (or another supported model) in the project.
- Note the project endpoint URL from the project's Overview page.
az provider register --namespace Microsoft.App --waitazd auth login
azd initSet the credentials for the Slack and Jira MCP server backends:
# Slack MCP Server
azd env set SLACK_BOT_TOKEN <your-slack-bot-xoxb-token>
# Jira MCP Server
azd env set JIRA_URL <your-jira-instance-url> # e.g. https://yourorg.atlassian.net
azd env set JIRA_USERNAME <your-jira-email>
azd env set JIRA_API_TOKEN <your-jira-api-token>All four variables are required. If you skip them, the Slack/Jira Container Apps will deploy but fail to connect to their respective services.
azd upThis provisions all Azure resources and deploys the code. The output will display the APIM gateway URL and the three MCP SSE endpoints.
After azd up completes, the output will show:
SERVICE_API_ENDPOINTS:
- https://<apim-name>.azure-api.net/mcp/sse
- https://<apim-name>.azure-api.net/slack-mcp/sse
- https://<apim-name>.azure-api.net/jira-mcp/sse
-
In a new terminal, install and run MCP Inspector:
npx @modelcontextprotocol/inspector
-
CTRL-click to load the MCP Inspector web app from the URL displayed (e.g.
http://127.0.0.1:6274). -
Set the transport type to SSE.
-
Set the URL to one of the APIM SSE endpoints and click Connect:
https://<apim-name>.azure-api.net/mcp/sse # MCP Functions https://<apim-name>.azure-api.net/slack-mcp/sse # Slack MCP https://<apim-name>.azure-api.net/jira-mcp/sse # Jira MCP -
Click List Tools, then select a tool and Run Tool.
Note: MCP Inspector's web UI will auto-discover the OAuth metadata at
/.well-known/oauth-authorization-serverand initiate the OAuth flow. You can also test directly using CLI mode:npx @modelcontextprotocol/inspector --cli --transport sse \ --server-url "https://<apim-name>.azure-api.net/jira-mcp/sse" \ --method tools/list
The agent-demo/ directory contains an interactive AI Foundry agent that connects to all four MCP backends as tools. The agent can query Slack channels, search Jira issues, browse GitHub repos, and use the sample MCP Functions tools — all from a single chat interface.
See agent-demo/README.md for full setup instructions.
cd agent-demo
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
cp .env.example .env
# Edit .env with your AI Foundry project endpoint and APIM gateway URL
python agent.pyThe GitHub MCP server is hosted remotely at https://api.githubcopilot.com/mcp/ — no Container App or APIM proxy needed. Authentication uses a Custom Keys connection in AI Foundry:
- In Azure Portal or Foundry Portal, go to your AI Foundry project.
- Navigate to Connected resources → + New connection → Custom Keys.
- Set Credential name to
Authorizationand Credential value toBearer <your-github-pat>. - Name the connection
github-mcp.
Or via CLI:
az rest --method put \
--url "https://management.azure.com<your-project-resource-id>/connections/github-mcp?api-version=2025-04-01-preview" \
--body '{
"properties": {
"authType": "CustomKeys",
"category": "CustomKeys",
"target": "https://api.githubcopilot.com/mcp/",
"isSharedToAll": true,
"credentials": {
"keys": {
"Authorization": "Bearer <your-github-pat>"
}
}
}
}'Note: The GitHub MCP connection is NOT provisioned by Bicep/
azd up. It must be created manually in your AI Foundry project.
The APIM gateway protects each MCP endpoint with OAuth 2.0 (Authorization Code + PKCE). This flow is designed for interactive browser-based clients like MCP Inspector or VS Code Copilot.
The AI Foundry agent connects to MCP servers server-side and cannot perform a browser-based OAuth flow. For agent access, you have several options:
- Disable OAuth validation in APIM policies — Comment out the token validation block in the policy XML files. This is what the current deployment does (OAuth blocks are commented out in all three policy files).
- Bypass APIM — Point the agent directly at the Container App URLs instead of the APIM gateway.
- Use APIM subscription keys — Configure APIM to accept
Ocp-Apim-Subscription-Keyas an alternative auth mechanism.
To re-enable OAuth, uncomment the authorization blocks in:
infra/app/apim-mcp/mcp-api.policy.xmlinfra/app/apim-slack-mcp/slack-mcp-api.policy.xmlinfra/app/apim-jira-mcp/jira-mcp-api.policy.xml
This solution deploys a secure MCP (Model Context Protocol) server infrastructure on Azure. The architecture implements a multi-layered security model with Azure API Management serving as an intelligent gateway that handles authentication, authorization, and request routing to multiple MCP server backends.
The infrastructure provisions the following Azure resources:
- Azure API Management (APIM) - The central security gateway that exposes OAuth and multiple MCP APIs
- SKU: BasicV2 (configurable)
- Identity: System-assigned and user-assigned managed identities
- Purpose: Handles authentication flows, request validation, and secure proxying to backend services
-
Azure Function App - Hosts the original MCP server implementation
- Runtime: Python 3.11 on Flex Consumption plan
- Authentication: Function-level authentication with managed identity integration
- Purpose: Executes MCP tools and operations (snippet management in this example)
-
Azure Container Apps - Host additional MCP server backends
- Slack MCP Container App - Runs slack-mcp-server for Slack workspace integration
- Jira MCP Container App - Runs mcp-atlassian for Jira/Atlassian integration
- Resources: 0.25 vCPU, 0.5 Gi memory per container
- Scaling: Each container runs in its own Container Apps Environment with Log Analytics integration
- Purpose: Enables third-party MCP servers to be deployed as containers behind the APIM gateway
-
Azure Storage Account - Provides multiple storage functions
- Function hosting: Stores function app deployment packages
- Application data: Blob container for snippet storage
- Security: Configured with managed identity access and optional private endpoints
-
Azure Cosmos DB (Serverless) - Stores OAuth dynamic client registrations
-
User-Assigned Managed Identity - Enables secure service-to-service authentication
- Purpose: Allows Function App to access Storage and Application Insights without secrets
- Permissions: Storage Blob Data Owner, Storage Queue Data Contributor, Monitoring Metrics Publisher
-
Entra ID Application Registration - OAuth2/OpenID Connect client for authentication
- Purpose: Enables third-party authorization flow per MCP specification
- Configuration: PKCE-enabled public client with custom redirect URIs
- Scopes: Only requests the
openidscope (no admin consent required)
- Application Insights - Provides telemetry and monitoring
- Log Analytics Workspace - Centralized logging and analytics for Functions and Container Apps
- Virtual Network (VNet) - When
vnetEnabledis true- Private Endpoints: Secure connectivity to Storage Account
- Network Isolation: Functions and storage communicate over private network
Azure API Management serves as the security perimeter, implementing:
- OAuth 2.0/PKCE authentication flows per MCP specification
- Session key encryption/decryption for secure API access
- Request validation and header injection
- Rate limiting and throttling capabilities
- Centralized policy management
- Unified authentication across multiple MCP backends
Azure Functions provides:
- Serverless, pay-per-use compute model
- Native integration with Azure services
- Automatic scaling based on demand
- Built-in monitoring and diagnostics
Azure Container Apps provides:
- Flexible hosting for third-party MCP server images
- Simple deployment from container registries (e.g. GHCR)
- Per-container environment variable and secret management
- Integrated logging via Log Analytics
Managed Identities eliminate the need for:
- Service credentials management
- Secret rotation processes
- Credential exposure risks
The APIM instance is configured with four APIs that work together to implement the MCP authorization specification and route requests to multiple backends:
This API implements the complete OAuth 2.0 authorization server functionality required by the MCP specification:
Authorization Endpoint (GET /authorize)
- Purpose: Initiates the OAuth 2.0/PKCE flow
- Policy Logic:
- Extracts PKCE parameters from MCP client request
- Checks for existing user consent (via cookies)
- Redirects to consent page if consent not granted
- Generates new PKCE parameters for Entra ID communication
- Stores authentication state in APIM cache
- Redirects user to Entra ID for authentication
Consent Management (GET/POST /consent)
- Purpose: Handles user consent for MCP client access
- Features: Consent persistence via secure cookies
OAuth Metadata Endpoint (GET /.well-known/oauth-authorization-server)
- Purpose: Publishes OAuth server configuration per RFC 8414
- Returns: JSON metadata about supported endpoints, flows, and capabilities
Client Registration (POST /register)
- Purpose: Supports dynamic client registration per MCP specification
- Storage: Client registrations are persisted in Cosmos DB
Token Endpoint (POST /token)
- Purpose: Exchanges authorization codes for access tokens
- Policy Logic:
- Validates authorization code and PKCE verifier from MCP client
- Exchanges Entra ID authorization code for access tokens (using
openidscope only) - Generates encrypted session key for MCP API access
- Caches the access token with session key mapping
- Returns encrypted session key to MCP client
The OAuth API uses several APIM Named Values for configuration:
McpClientId- The registered Entra ID application client IDEntraIDFicClientId- Service identity client ID for token exchangeAPIMGatewayURL- Base URL for callback and metadata endpointsOAuthScopes- Requested OAuth scopes (openidonly -- no admin consent required)EncryptionKey/EncryptionIV- For session key encryption
This API provides the original MCP protocol endpoints backed by Azure Functions:
Server-Sent Events Endpoint (GET /mcp/sse)
- Purpose: Establishes real-time communication channel for MCP protocol
- Security: Requires valid encrypted session token
Message Endpoint (POST /mcp/message)
- Purpose: Handles MCP protocol messages and tool invocations
- Security: Requires valid encrypted session token
- Backend: Proxies to Azure Functions with
x-functions-keyheader injection
This API proxies requests to the Slack MCP Server running on Azure Container Apps:
Server-Sent Events Endpoint (GET /slack-mcp/sse)
- Purpose: Establishes SSE connection to the Slack MCP server
Message Endpoint (POST /slack-mcp/message)
- Purpose: Sends messages to the Slack MCP server
The policy rewrites SSE response URLs to include the /slack-mcp/ prefix so that clients send follow-up messages through APIM rather than directly to the container.
This API proxies requests to the Jira/Atlassian MCP Server running on Azure Container Apps:
Server-Sent Events Endpoint (GET /jira-mcp/sse)
- Purpose: Establishes SSE connection to the Jira MCP server
Message Endpoint (POST /jira-mcp/messages/)
- Purpose: Sends messages to the Jira MCP server
The policy rewrites SSE response URLs to include the /jira-mcp/ prefix for correct APIM routing.
Note: The Jira MCP server uses
/messages/?session_id=(plural, with underscore) unlike the Slack MCP server which uses/message?sessionId=(singular, camelCase). Each API policy handles the URL rewriting accordingly.
All MCP APIs (MCP Functions, Slack, Jira) share the same security policy pattern:
-
Authorization Header Validation
<check-header name="Authorization" failed-check-httpcode="401" failed-check-error-message="Not authorized" ignore-case="false" />
-
Session Key Decryption
- Extracts encrypted session key from Authorization header
- Decrypts using AES with stored key and IV
- Validates token format and structure
-
Token Cache Lookup
<cache-lookup-value key="@($"EntraToken-{context.Variables.GetValueOrDefault("decryptedSessionKey")}")" variable-name="accessToken" />
-
Access Token Validation
- Verifies cached access token exists and is valid
- Returns 401 with proper WWW-Authenticate header if invalid
-
Backend Authentication (MCP Functions only)
<set-header name="x-functions-key" exists-action="override"> <value>{{function-host-key}}</value> </set-header>
The solution implements a multi-layer security model:
Layer 1: OAuth 2.0/PKCE Authentication
- MCP clients must complete full OAuth flow with Entra ID
- PKCE prevents authorization code interception attacks
- User consent management with persistent preferences
- Only the
openidscope is requested (no admin consent required)
Layer 2: Session Key Encryption
- Access tokens are never exposed to MCP clients
- Encrypted session keys provide time-bounded access
- AES encryption with secure key management in APIM
Layer 3: Function-Level Security
- Function host keys protect direct access to Azure Functions
- Managed identity ensures secure service-to-service communication
- Network isolation available via VNet integration
Layer 4: Azure Platform Security
- All traffic encrypted in transit (TLS)
- Storage access via managed identities
- Audit logging through Application Insights
This layered approach ensures that even if one security boundary is compromised, multiple additional protections remain in place.
The architecture is designed to be extensible. To add a new MCP server backend:
-
Create the APIM API definition: Add a new directory
infra/app/apim-<name>/with:<name>-api.bicep- API definition with SSE and message operations<name>-api.policy.xml- Policy with OAuth validation and URL rewriting
-
Add the Container App module in
infra/main.bicep:- Reference the reusable
infra/core/host/container-app.bicepmodule - Configure the container image, port, command, args, secrets, and environment variables
- Reference the reusable
-
Add the APIM API module in
infra/main.bicep:- Reference the new bicep module from step 1
- Pass the APIM service name and container app FQDN
-
Add parameters in
infra/main.bicepandinfra/main.parameters.jsonfor any credentials or configuration values needed by the new backend. -
Set environment variables via
azd env setfor any secrets before runningazd up.
The mcp-atlassian Jira MCP server exposes 25 tools, but 9 of them use anyOf/allOf in their JSON Schema definitions which are not supported by the Azure AI Foundry agent SDK. The agent demo whitelists only the 16 compatible tools via the allowed_tools parameter on the MCPTool definition.
Compatible tools (16): jira_search_issues, jira_list_projects, jira_list_statuses, jira_list_priorities, jira_get_transitions, jira_get_comments, jira_get_worklogs, jira_get_project, jira_list_sprints, jira_get_sprint, jira_get_board, jira_list_boards, jira_get_issue_link_types, jira_link_issues, jira_list_fields, jira_get_field
Incompatible tools (9): jira_get_issue, jira_create_issue, jira_update_issue, jira_transition_issue, jira_add_comment, jira_add_worklog, jira_create_sprint, jira_update_sprint, jira_batch_get_changelogs
Workaround: Use jira_search_issues with JQL (e.g. key = PROJ-42) instead of jira_get_issue.
The agent demo shows a persistent Pyright warning about list[MCPTool] not being assignable to list[Tool]. This is a known SDK typing issue (list invariance in Python) and works correctly at runtime.

