Production configuration for exposing REST APIs and existing MCP servers through Azure API Management as governed MCP endpoints. Entra authentication, rate limiting, quota, credential abstraction, and streaming-safe diagnostics — without writing custom MCP server code for every backend.
Get access: sales@weldonweb.co.uk
MCP clients (AI agents, copilots, IDE extensions) need to call tools on your backend services. You want:
- Centralised auth — clients authenticate once at the gateway, not per-backend.
- Credential abstraction — clients never hold backend API keys or service credentials.
- Rate limiting and quota — per-subscription, independently tunable per MCP server.
- Observability — structured tool call metrics and logs without touching backend code.
- Streaming integrity — SSE and Streamable HTTP streams must not be buffered or broken by the gateway.
APIM Premium provides all of this. The non-obvious part is the configuration — a handful of settings in the wrong place silently breaks streaming forever. This implementation gets them right.
Pattern 1: REST-as-MCP
─────────────────────
MCP Client ──► APIM ──► (synthesises MCP) ──► SampleRestApi
│
└── imports OpenAPI spec, generates tool manifest,
marshals JSON-RPC tool calls to REST operations
Pattern 2: Govern Existing MCP Server
──────────────────────────────────────
MCP Client ──► APIM ──► SampleMcpServer (.NET 9) ──► SampleRestApi
│ (Streamable HTTP, MCP-native)
│
└── adds Entra auth, rate limiting, quota, metrics
without changing MCP server source code
┌─────────────────────────────────────────────────────────────────────┐
│ Azure API Management (Premium) │
│ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ Policy Pipeline (all requests) │ │
│ │ validate-entra-token → rate-limit → quota → emit-metric │ │
│ │ → [pattern-specific backend auth] → sse-hygiene │ │
│ │ on-error: mcp-error-handling │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
│ Pattern 1 Product Pattern 2 Product │
│ sample-rest-api sample-mcp-server │
│ │ │ │
│ ▼ ▼ │
│ API: sample-rest-api API: sample-mcp-server │
│ (OpenAPI import) (pass-through) │
│ │ │ │
│ │ KV-backed named value │ Managed Identity token │
│ ▼ ▼ │
└───────┼──────────────────────────┼───────────────────────────────────┘
│ │
▼ ▼
SampleRestApi SampleMcpServer (.NET 9)
(Container Apps) (Container Apps)
│
▼
SampleRestApi
(Container Apps)
| Boundary | Mechanism | Notes |
|---|---|---|
| MCP Client → APIM | Entra JWT (audience-pinned) + APIM subscription key | Both required. JWT validates identity and scope; subscription key gates product access. |
| APIM → SampleRestApi (Pattern 1) | Key Vault-backed named value injected as header | Client never sees the backend API key. Rotated in Key Vault without Terraform apply. |
| APIM → SampleMcpServer (Pattern 2) | APIM-acquired Managed Identity token | APIM system identity acquires a token for the backend app registration. Backend validates it. |
| SampleMcpServer → SampleRestApi | Direct HTTP (internal Container Apps network) | No auth — enforced via private/internal ingress, not credentials. |
| Backend isolation | Container Apps ingress restrictions | Both backends must accept connections from APIM only (IP allowlist or internal ingress). |
| Limitation | Detail |
|---|---|
| APIM Workspaces do not support MCP | The mcpServers resource is a service-level resource only. Products are used as the isolation boundary. See ADR 0003. |
| Consumption tier incompatible | MCP requires long-running connections. Consumption tier has a 30-second request timeout and no persistent connection support. |
azapi_resource for mcpServers disabled by default |
count = 0 in both modules. Set to 1 after verifying your APIM instance's API version supports the 2025-05-01-preview resource type. See ADR 0002. |
schema_validation_enabled = false on azapi mcpServers |
The resource type is not yet in the azapi 2.9.0 embedded schema. Remove this flag when the resource reaches GA. |
| Keep-alive required | Azure Load Balancer drops idle TCP connections after 4 minutes. SampleMcpServer's KeepAliveService pings every 2 minutes. |
The package includes production Terraform modules, working .NET 9 sample services, and an end-to-end agent client for both patterns.
- Terraform modules — shared policy fragments, diagnostics, Pattern 1 (REST-as-MCP), Pattern 2 (govern existing MCP server), and a wired-up dev environment root module
- SampleRestApi — .NET 9 minimal API (incident management domain) with OpenAPI spec and Scalar UI
- SampleMcpServer — .NET 9 MCP server wrapping SampleRestApi over Streamable HTTP
- SampleAgentClient — .NET 9 console app that proves the end-to-end flow through APIM for both patterns
- Architecture Decision Records — the reasoning behind transport choice, control plane tooling, authorisation model, and rate limiting strategy
- SSE/Streamable HTTP failure mode checklist — the gotchas we found so you don't have to
- CI workflows — Terraform validate and .NET build, ready for your PR pipeline
- Azure subscription with an existing APIM Premium instance
- Terraform >= 1.7
- .NET 9 SDK
- Azure CLI (for initial auth:
az login) - Key Vault with a secret for the SampleRestApi backend credential (Pattern 1)
- Entra app registration for the APIM gateway audience
- Entra app registration for the SampleMcpServer backend (Pattern 2)
To get access to the repository, deployment guide, and support: sales@weldonweb.co.uk
Six shared fragments are deployed by the apim-shared-policy-fragments module and included in every MCP API policy via <include-fragment />. Order within <inbound> is significant.
| Fragment | Section | Purpose |
|---|---|---|
validate-entra-token |
<inbound> |
Validates Entra JWT (v1/v2 issuers). Sets caller-object-id, caller-app-id, caller-tenant-id context variables. |
rate-limit-per-subscription |
<inbound> |
rate-limit-by-key per subscription key. Sets X-RateLimit-* response headers. |
quota-per-subscription |
<inbound> |
quota-by-key per subscription key. Long-window budget cap. |
emit-tool-call-metric |
<inbound> |
Emits mcp-tool-call custom metric with caller dimensions. |
sse-hygiene |
<backend> |
<forward-request buffer-response="false" timeout="600" />. Mandatory for streaming. |
mcp-error-handling |
<on-error> |
JSON-RPC 2.0 error envelope. WWW-Authenticate with resource_metadata on 401 for MCP OAuth discovery. |
All named values are created by the root module. Policy fragments reference them by name.
| Named Value | Type | Used By |
|---|---|---|
mcp-gateway-tenant-id |
Direct | validate-entra-token |
mcp-gateway-audience |
Direct | validate-entra-token |
mcp-required-scope |
Direct | validate-entra-token |
mcp-rate-limit-calls |
Direct | rate-limit-per-subscription |
mcp-rate-limit-period-seconds |
Direct | rate-limit-per-subscription |
mcp-quota-calls |
Direct | quota-per-subscription |
mcp-quota-period-seconds |
Direct | quota-per-subscription |
{api-name}-backend-credential |
KV-backed, secret | Pattern 1 only |
- ADR 0001 — Streamable HTTP as default transport
- ADR 0002 — azapi for MCP control plane resources
- ADR 0003 — APIM Product as MCP server authorisation unit
- ADR 0004 — No LLM token limit on MCP traffic
| Requirement | Adaptation |
|---|---|
| Multiple MCP servers | Add one module "..." block per server, pointing to apim-mcp-from-rest or apim-mcp-from-existing. Each gets its own Product and subscription key space. |
| Tighter rate limits for production | Override rate_limit_calls, rate_limit_period_seconds, quota_calls, quota_period_seconds in terraform.tfvars per environment. |
| Per-API rate limits | Move the rate-limit fragment invocation from the shared module into the per-API policy and add API-specific named values for the thresholds. |
| Multi-region APIM | rate-limit-by-key counters are per-region. Use quota-by-key (cross-region replication) for hard global budgets. See ADR 0004. |
| Private connectivity | Add a private endpoint for APIM and configure Container Apps internal ingress. The Terraform modules are not opinionated about network topology. |
| Workspace migration | When APIM Workspace MCP support reaches GA, see ADR 0003 for the migration path. |
| Content auditing | Route responses to Event Hub via <log-to-eventhub> with a separate logger. Do not increase body_bytes on the Application Insights diagnostic — this breaks streaming. |
| Custom tool credentials | Add KV-backed named values for per-tool credentials. Inject them in the API policy using the same {{named-value-name}} pattern as Pattern 1. |
This covers governing access to your own systems via the gateway you already own. It's the right pattern when you're exposing internal REST APIs and MCP servers to agents and you want to do that on infrastructure your security team has already approved.
What it doesn't give you:
- Identity-resolved audit logs of every tool call (who, what, with what arguments)
- Regex-based argument masking before requests leave your perimeter
- Per-identity, per-tool allow/deny policy without writing and maintaining APIM policy XML
- Aggregated audit across multiple MCP servers — including third-party servers you don't own
If you need those, that's a different problem. It's what the MCP Audit & Compliance Gateway is for — an Azure Managed Application that deploys into your subscription (data never leaves), available on the Azure Marketplace with a permanent free tier up to 10,000 invocations/month.
Use APIM to expose your systems to agents. Use the Audit Gateway to prove what those agents did.
Questions? sales@weldonweb.co.uk