Skip to content

Commit 5acdc55

Browse files
feat(mcp): multi-tenant Entra ID validation
jwt.ts now fetches per-teamspace Entra config from context7app's /api/v2/entra/config/[audience] endpoint, enabling per-user identity for customers using Azure API Management in front of mcp.context7.com. Per-tenant JWKS cache and 5-minute in-memory config cache. Also adds the Azure APIM enterprise integration docs page covering both the shared-identity (today) and per-user-identity (in development) patterns.
1 parent 61de754 commit 5acdc55

3 files changed

Lines changed: 437 additions & 9 deletions

File tree

docs/docs.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
"group": "Enterprise",
5555
"pages": [
5656
"enterprise",
57+
"enterprise/azure-apim",
5758
{
5859
"group": "On-Premise",
5960
"pages": [

docs/enterprise/azure-apim.mdx

Lines changed: 351 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,351 @@
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

Comments
 (0)