|
| 1 | +--- |
| 2 | +title: "Azure API Management as MCP gateway" |
| 3 | +sidebarTitle: "Azure APIM" |
| 4 | +description: "Route Context7 MCP traffic through Azure API Management with Microsoft Entra ID authentication." |
| 5 | +--- |
| 6 | + |
| 7 | +Azure API Management (APIM, sometimes marketed as "Azure AI Gateway") can sit in front of `mcp.context7.com` and let your organization control who reaches it from your tenant. APIM validates a Microsoft Entra ID token at the gateway, and Context7 supports two ways of resolving that identity at the backend. |
| 8 | + |
| 9 | +## Two integration patterns |
| 10 | + |
| 11 | +| | Shared identity | Per-user identity | |
| 12 | +|---|---|---| |
| 13 | +| **Status** | Available today | In development | |
| 14 | +| **Token on device** | Entra-issued | Entra-issued | |
| 15 | +| **APIM validates** | Yes (`validate-azure-ad-token`) | Yes (`validate-azure-ad-token`) | |
| 16 | +| **Forwarded to Context7** | Single teamspace API key | Same Entra JWT | |
| 17 | +| **Context7 sees** | One shared teamspace identity | Each user individually | |
| 18 | +| **Per-user usage / audit** | In APIM logs only | In APIM and in Context7 | |
| 19 | +| **MFA / conditional access** | Enforced by Entra at sign-in | Enforced by Entra at sign-in | |
| 20 | + |
| 21 | +For enterprise deployments, **per-user identity** is the recommended target. Start the POC on shared identity to unblock security review and initial trials, then migrate once per-user identity ships. Both patterns use the same APIM topology, so migration is essentially a policy change in APIM (drop the API key injection, keep `validate-azure-ad-token`) plus a one-time tenant configuration on the Context7 side. |
| 22 | + |
| 23 | +## Pattern 1: Shared identity (available today) |
| 24 | + |
| 25 | +### Architecture |
| 26 | + |
| 27 | +``` |
| 28 | +MCP client ──(Entra JWT)──► APIM ──(Context7 API key)──► mcp.context7.com |
| 29 | + │ |
| 30 | + ├─ validate-azure-ad-token |
| 31 | + ├─ set-header Authorization |
| 32 | + └─ rewrite-uri /mcp → /mcp/oauth |
| 33 | +``` |
| 34 | + |
| 35 | +1. The MCP client (Claude Code, Cursor, VS Code, ChatGPT) obtains an access token from Entra ID. |
| 36 | +2. The client calls APIM with `Authorization: Bearer <entra-jwt>`. |
| 37 | +3. APIM validates the JWT against your tenant, audience, and required scope. |
| 38 | +4. APIM strips the Entra token and injects your Context7 teamspace API key. |
| 39 | +5. The request lands at `mcp.context7.com/mcp/oauth` as an authenticated Context7 request. |
| 40 | + |
| 41 | +### Before you start |
| 42 | + |
| 43 | +You will need: |
| 44 | + |
| 45 | +- An **Azure subscription** in the same tenant as your Entra users. |
| 46 | +- An **Entra admin** who can register applications. |
| 47 | +- A **Context7 API key** from a teamspace. Generate one at [context7.com/dashboard](https://context7.com/dashboard) under **API Keys**. It starts with `ctx7sk_`. |
| 48 | +- The Azure CLI installed locally (`brew install azure-cli` on macOS) and authenticated to your subscription (`az login`). |
| 49 | + |
| 50 | +### Provision APIM |
| 51 | + |
| 52 | +APIM Basic v2 provisions in about 5 minutes and supports MCP routing. Consumption tier does **not** support MCP backends. |
| 53 | + |
| 54 | +Create a Bicep file: |
| 55 | + |
| 56 | +```bicep apim.bicep |
| 57 | +param name string |
| 58 | +param location string |
| 59 | +param publisherEmail string |
| 60 | +param publisherName string |
| 61 | +
|
| 62 | +resource apim 'Microsoft.ApiManagement/service@2023-09-01-preview' = { |
| 63 | + name: name |
| 64 | + location: location |
| 65 | + sku: { |
| 66 | + name: 'BasicV2' |
| 67 | + capacity: 1 |
| 68 | + } |
| 69 | + properties: { |
| 70 | + publisherEmail: publisherEmail |
| 71 | + publisherName: publisherName |
| 72 | + } |
| 73 | +} |
| 74 | +
|
| 75 | +output gatewayUrl string = apim.properties.gatewayUrl |
| 76 | +``` |
| 77 | + |
| 78 | +Deploy it: |
| 79 | + |
| 80 | +```bash |
| 81 | +RG=rg-context7-mcp |
| 82 | +LOC=westeurope |
| 83 | +APIM=apim-context7-$(openssl rand -hex 3) |
| 84 | + |
| 85 | +az group create -n $RG -l $LOC |
| 86 | +az deployment group create -g $RG --template-file apim.bicep \ |
| 87 | + --parameters name=$APIM location=$LOC \ |
| 88 | + publisherEmail=you@example.com publisherName="Your Org" |
| 89 | +``` |
| 90 | + |
| 91 | +Capture the gateway URL once it returns: |
| 92 | + |
| 93 | +```bash |
| 94 | +APIM_HOST=$(az apim show -g $RG -n $APIM --query gatewayUrl -o tsv) |
| 95 | +echo $APIM_HOST # https://<apim-name>.azure-api.net |
| 96 | +``` |
| 97 | + |
| 98 | +### Register the MCP API in Entra |
| 99 | + |
| 100 | +This Entra app represents the protected MCP resource. Its scope is what Entra users must request when they ask for an access token. |
| 101 | + |
| 102 | +1. **Microsoft Entra admin center** → **App registrations** → **+ New registration**. |
| 103 | + - **Name:** `Context7 MCP` |
| 104 | + - **Supported account types:** Accounts in this organizational directory only |
| 105 | + - **Redirect URI:** leave blank |
| 106 | + - **Register** |
| 107 | + |
| 108 | +2. Note the **Application (client) ID** and **Directory (tenant) ID** from the Overview page. |
| 109 | + |
| 110 | +3. **Expose an API** → **Add** next to "Application ID URI" → accept the default `api://<client-id>` → **Save**. |
| 111 | + |
| 112 | +4. **+ Add a scope**: |
| 113 | + - **Scope name:** `mcp.access` |
| 114 | + - **Who can consent:** Admins and users |
| 115 | + - **Admin consent display name:** `Access Context7 MCP server` |
| 116 | + - **Admin consent description:** anything descriptive |
| 117 | + - **State:** Enabled |
| 118 | + - **Add scope** |
| 119 | + |
| 120 | +5. **Manifest** → find `"requestedAccessTokenVersion"` under `api` → set to `2`. Save. |
| 121 | + |
| 122 | + <Warning> |
| 123 | + This forces v2 tokens (`iss: https://login.microsoftonline.com/<tid>/v2.0`). v1 tokens use a different issuer and will fail JWT validation in APIM if you do not set this. |
| 124 | + </Warning> |
| 125 | + |
| 126 | +6. **API permissions** → for each MCP client tool you want to allow (Claude Code, Cursor, VS Code, ChatGPT), register its client app separately and grant it delegated permission on `mcp.access`. Pre-authorize these clients in **Expose an API** → **Authorized client applications** to skip end-user consent. |
| 127 | + |
| 128 | +### Configure APIM |
| 129 | + |
| 130 | +#### Store the Context7 API key |
| 131 | + |
| 132 | +```bash |
| 133 | +az apim nv create -g $RG --service-name $APIM \ |
| 134 | + --named-value-id context7-api-key \ |
| 135 | + --display-name context7-api-key \ |
| 136 | + --value "ctx7sk_PASTE_YOUR_KEY" \ |
| 137 | + --secret true |
| 138 | +``` |
| 139 | + |
| 140 | +<Tip> |
| 141 | +For production, store the key in Azure Key Vault and reference it from APIM with a Key Vault-backed named value. The example above uses inline storage for brevity. |
| 142 | +</Tip> |
| 143 | + |
| 144 | +#### Create the API and operation |
| 145 | + |
| 146 | +```bash |
| 147 | +az apim api create -g $RG --service-name $APIM \ |
| 148 | + --api-id context7-mcp \ |
| 149 | + --display-name "Context7 MCP" \ |
| 150 | + --path context7 \ |
| 151 | + --service-url https://mcp.context7.com \ |
| 152 | + --protocols https \ |
| 153 | + --subscription-required false |
| 154 | + |
| 155 | +az apim api operation create -g $RG --service-name $APIM \ |
| 156 | + --api-id context7-mcp \ |
| 157 | + --operation-id post-mcp \ |
| 158 | + --display-name "MCP" \ |
| 159 | + --method POST \ |
| 160 | + --url-template "/mcp" |
| 161 | +``` |
| 162 | + |
| 163 | +#### Attach the policy |
| 164 | + |
| 165 | +Save this as `policy.xml`, replacing `<your-tenant-id>` and `<your-mcp-api-app-id>` with the values from the Entra registration step: |
| 166 | + |
| 167 | +```xml policy.xml |
| 168 | +<policies> |
| 169 | + <inbound> |
| 170 | + <base /> |
| 171 | + <validate-azure-ad-token tenant-id="<your-tenant-id>" |
| 172 | + header-name="Authorization" |
| 173 | + failed-validation-httpcode="401" |
| 174 | + failed-validation-error-message="Unauthorized."> |
| 175 | + <audiences> |
| 176 | + <audience><your-mcp-api-app-id></audience> |
| 177 | + </audiences> |
| 178 | + <required-claims> |
| 179 | + <claim name="scp" match="any"> |
| 180 | + <value>mcp.access</value> |
| 181 | + </claim> |
| 182 | + </required-claims> |
| 183 | + </validate-azure-ad-token> |
| 184 | + <set-header name="Authorization" exists-action="override"> |
| 185 | + <value>@("Bearer " + "{{context7-api-key}}")</value> |
| 186 | + </set-header> |
| 187 | + <rewrite-uri template="/mcp/oauth" /> |
| 188 | + </inbound> |
| 189 | + <backend><base /></backend> |
| 190 | + <outbound><base /></outbound> |
| 191 | + <on-error><base /></on-error> |
| 192 | +</policies> |
| 193 | +``` |
| 194 | + |
| 195 | +Apply via the management REST API (some CLI versions do not expose `az apim api policy create`): |
| 196 | + |
| 197 | +```bash |
| 198 | +SUB=$(az account show --query id -o tsv) |
| 199 | + |
| 200 | +python3 -c "import json; print(json.dumps({'properties': {'value': open('policy.xml').read(), 'format': 'xml'}}))" > policy-body.json |
| 201 | + |
| 202 | +az rest --method put \ |
| 203 | + --uri "https://management.azure.com/subscriptions/$SUB/resourceGroups/$RG/providers/Microsoft.ApiManagement/service/$APIM/apis/context7-mcp/policies/policy?api-version=2023-09-01-preview" \ |
| 204 | + --body @policy-body.json |
| 205 | +``` |
| 206 | + |
| 207 | +### Test end-to-end |
| 208 | + |
| 209 | +Obtain an Entra access token. The fastest way is device code flow: |
| 210 | + |
| 211 | +```bash |
| 212 | +TENANT_ID=<your-tenant-id> |
| 213 | +CLIENT_ID=<your-mcp-api-app-id> |
| 214 | + |
| 215 | +curl -s -X POST "https://login.microsoftonline.com/$TENANT_ID/oauth2/v2.0/devicecode" \ |
| 216 | + -d "client_id=$CLIENT_ID" \ |
| 217 | + -d "scope=api://$CLIENT_ID/mcp.access offline_access" |
| 218 | +``` |
| 219 | + |
| 220 | +Open the `verification_uri`, enter the `user_code`, sign in. Then exchange the `device_code`: |
| 221 | + |
| 222 | +```bash |
| 223 | +curl -s -X POST "https://login.microsoftonline.com/$TENANT_ID/oauth2/v2.0/token" \ |
| 224 | + -d "grant_type=urn:ietf:params:oauth:grant-type:device_code" \ |
| 225 | + -d "client_id=$CLIENT_ID" \ |
| 226 | + -d "device_code=<paste-device-code>" |
| 227 | +``` |
| 228 | + |
| 229 | +Copy `access_token` and call APIM: |
| 230 | + |
| 231 | +```bash |
| 232 | +ENTRA_TOKEN="<paste-access-token>" |
| 233 | + |
| 234 | +curl -i -X POST "$APIM_HOST/context7/mcp" \ |
| 235 | + -H "Authorization: Bearer $ENTRA_TOKEN" \ |
| 236 | + -H "Content-Type: application/json" \ |
| 237 | + -H "Accept: application/json, text/event-stream" \ |
| 238 | + -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"resolve-library-id","arguments":{"query":"routing","libraryName":"Next.js"}}}' |
| 239 | +``` |
| 240 | + |
| 241 | +You should see a `200` with library results streamed back as an SSE event. |
| 242 | + |
| 243 | +Verify the gateway rejects unauthorized requests: |
| 244 | + |
| 245 | +```bash |
| 246 | +# No token → 401 at APIM, request never reaches Context7 |
| 247 | +curl -i -X POST "$APIM_HOST/context7/mcp" \ |
| 248 | + -H "Content-Type: application/json" \ |
| 249 | + -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}' |
| 250 | + |
| 251 | +# Tampered token → 401 |
| 252 | +curl -i -X POST "$APIM_HOST/context7/mcp" \ |
| 253 | + -H "Authorization: Bearer ${ENTRA_TOKEN}XYZ" \ |
| 254 | + -H "Content-Type: application/json" \ |
| 255 | + -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}' |
| 256 | +``` |
| 257 | + |
| 258 | +### Connecting an MCP client |
| 259 | + |
| 260 | +For a quick test from Claude Code with a static token: |
| 261 | + |
| 262 | +```bash |
| 263 | +claude mcp add context7-apim https://<your-apim-host>/context7/mcp \ |
| 264 | + --transport http \ |
| 265 | + --header "Authorization: Bearer $ENTRA_TOKEN" |
| 266 | +``` |
| 267 | + |
| 268 | +For human users without manual token handling, MCP clients can do OAuth 2.1 discovery against your APIM. This requires APIM to advertise a Protected Resource Metadata (RFC 9728) endpoint and either pre-register each MCP client tool in Entra or deploy a Dynamic Client Registration shim. Reach out for the OAuth discovery walkthrough if you need it. |
| 269 | + |
| 270 | +## Pattern 2: Per-user identity via native Entra validation (in development) |
| 271 | + |
| 272 | +### Architecture |
| 273 | + |
| 274 | +``` |
| 275 | +MCP client ──(Entra JWT)──► APIM ──(same Entra JWT)──► mcp.context7.com |
| 276 | + │ │ |
| 277 | + validate-azure-ad-token validates iss/aud against |
| 278 | + (defence-in-depth) your tenant config, |
| 279 | + resolves oid → C7 user |
| 280 | +``` |
| 281 | + |
| 282 | +### One-time tenant onboarding |
| 283 | + |
| 284 | +You provide Context7 with: |
| 285 | + |
| 286 | +- Your Entra **tenant ID** |
| 287 | +- Your MCP API **app ID** (audience) |
| 288 | +- The required **scope** (typically `mcp.access`) |
| 289 | + |
| 290 | +We configure these against your Context7 teamspace. Once set up, our MCP server validates inbound Entra tokens for your tenant directly, and resolves the token's `oid` claim to a per-user Context7 record (auto-provisioned on first sign-in). |
| 291 | + |
| 292 | +### Flow |
| 293 | + |
| 294 | +1. A developer's MCP client (e.g. VS Code with GitHub Copilot) calls APIM. |
| 295 | +2. The client performs OAuth against your Entra tenant for an access token scoped to your MCP API. |
| 296 | +3. The client retries the request against APIM with the Entra token in the `Authorization` header. |
| 297 | +4. APIM validates the token locally (`validate-azure-ad-token`) as defence-in-depth. |
| 298 | +5. APIM forwards the request to `mcp.context7.com` with the Entra token unchanged. |
| 299 | +6. The Context7 MCP server validates the token against your configured tenant ID and audience. |
| 300 | +7. The `oid` claim is resolved to a Context7 user record in your teamspace, auto-provisioned on first sign-in. |
| 301 | +8. The request is served attributed to that user. |
| 302 | + |
| 303 | +End state: every developer has their own Context7 identity tied to their Entra account. The token on the device remains Entra-issued throughout. APIM stays as the network and audit boundary. MFA and conditional access continue to be enforced by Entra at sign-in. |
| 304 | + |
| 305 | +### Migration from Pattern 1 |
| 306 | + |
| 307 | +If you start on Pattern 1 and migrate to Pattern 2 later, the move is: |
| 308 | + |
| 309 | +1. Share your tenant ID, audience, and scope with us. We onboard your tenant configuration. |
| 310 | +2. Update the APIM policy: keep `validate-azure-ad-token`, drop the `set-header` swap and the `rewrite-uri`. |
| 311 | +3. APIM now forwards the Entra JWT unchanged to Context7. |
| 312 | + |
| 313 | +The Entra app registration, the Bicep, and all client-side wiring stay the same. The Context7 named value (`context7-api-key`) can be deleted after the cutover. |
| 314 | + |
| 315 | +### Status |
| 316 | + |
| 317 | +Pattern 2 is currently in development. Contact [context7@upstash.com](mailto:context7@upstash.com) or your Context7 account contact for the latest timeline and to schedule tenant onboarding. |
| 318 | + |
| 319 | +## Troubleshooting |
| 320 | + |
| 321 | +### `401 Unauthorized` with valid-looking token |
| 322 | + |
| 323 | +Decode the token at [jwt.io](https://jwt.io) and verify: |
| 324 | + |
| 325 | +- `iss` is `https://login.microsoftonline.com/<your-tenant-id>/v2.0` (v2 form). If it is `https://sts.windows.net/<tid>/`, your manifest's `requestedAccessTokenVersion` is missing or set to `null`. |
| 326 | +- `aud` matches the value in your `<audiences>` policy block. v2 tokens use the GUID, not `api://...`. |
| 327 | +- `scp` contains `mcp.access`. |
| 328 | +- `exp` is in the future. |
| 329 | + |
| 330 | +### `Invalid API key. Please check your API key. API keys should start with 'ctx7sk' prefix.` |
| 331 | + |
| 332 | +This applies to Pattern 1 only. APIM forwarded successfully but the Context7 backend rejected the key. The `context7-api-key` named value is missing, set to a placeholder, or the key has been revoked. Generate a fresh key at [context7.com/dashboard](https://context7.com/dashboard) and update the named value: |
| 333 | + |
| 334 | +```bash |
| 335 | +az apim nv update -g $RG --service-name $APIM \ |
| 336 | + --named-value-id context7-api-key \ |
| 337 | + --set value="ctx7sk_NEW_KEY" |
| 338 | +``` |
| 339 | + |
| 340 | +### `AADSTS650057: Invalid resource` |
| 341 | + |
| 342 | +The MCP client's app registration does not list your MCP API as an allowed resource. In your Entra MCP API app → **Expose an API** → **Authorized client applications**, add the client's app ID and check the `mcp.access` scope. |
| 343 | + |
| 344 | +### Streaming responses get cut off |
| 345 | + |
| 346 | +APIM diagnostics with "Number of payload bytes to log" > 0 break MCP's SSE streams. Set it to `0` for Frontend Response at the service level. |
| 347 | + |
| 348 | +## What's not covered |
| 349 | + |
| 350 | +- **Self-hosted MCP server.** This guide proxies the hosted `mcp.context7.com`. For air-gapped or compliance scenarios where MCP traffic cannot leave your network, contact [context7@upstash.com](mailto:context7@upstash.com) about the self-hosted MCP package. |
| 351 | +- **Dynamic Client Registration.** Entra does not implement RFC 7591. MCP clients that depend on it (some versions of Claude.ai, ChatGPT) need to be pre-registered in Entra and pre-authorized on the MCP API app. A DCR shim deployed inside APIM is possible but not officially supported by Microsoft; reach out if you need this. |
0 commit comments