Skip to content

Commit 451609d

Browse files
committed
fix: update MCP Inspector authorization flow to reflect PRM resource id
1 parent 953d315 commit 451609d

9 files changed

Lines changed: 259 additions & 109 deletions

File tree

.devcontainer/devcontainer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020
"vscode": {
2121
"extensions": [
2222
"ms-toolsai.jupyter",
23-
"ms-python.python"
23+
"ms-python.python",
24+
"ms-azuretools.vscode-bicep"
2425
]
2526
}
2627
}

labs/mcp-prm-oauth/clean-up-resources.ipynb

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,25 @@
2222
"deployment_name = os.path.basename(os.path.dirname(globals()['__vsc_ipynb_file__']))\n",
2323
"resource_group = f\"lab-{deployment_name}\"\n",
2424
"\n",
25-
"utils.cleanup_resources(deployment_name, resource_group_name=resource_group)"
25+
"utils.cleanup_resources(deployment_name, resource_group_name=resource_group)\n",
26+
"\n",
27+
"# The lab also creates an Entra ID app registration for the MCP endpoint (when you don't bring your own `mcpClientId`). It lives in Microsoft Entra ID, not in the resource group, so it must be deleted separately.\n",
28+
"\n",
29+
"app_registration_name = f\"lab-{deployment_name}-app\"\n",
30+
"\n",
31+
"print(f\"Searching for app with display name {app_registration_name}...\")\n",
32+
"output = utils.run(f\"az ad app list --filter \\\"displayName eq '{app_registration_name}'\\\"\", \"Retrieved app registration\", \"Failed to get the app registration\")\n",
33+
"if output.success and output.json_data:\n",
34+
" client_id = output.json_data[0]['appId']\n",
35+
" output = utils.run(f\"az ad app delete --id {client_id}\", \"Deleted app registration\", \"Failed to delete the app registration\")\n",
36+
"else:\n",
37+
" print(f\"No app registration found with display name {app_registration_name}. Nothing to delete.\")"
2638
]
2739
}
2840
],
2941
"metadata": {
3042
"kernelspec": {
31-
"display_name": "myenv",
43+
"display_name": "ai-gateway (3.12.13.final.0)",
3244
"language": "python",
3345
"name": "python3"
3446
},
@@ -42,7 +54,7 @@
4254
"name": "python",
4355
"nbconvert_exporter": "python",
4456
"pygments_lexer": "ipython3",
45-
"version": "3.13.5"
57+
"version": "3.12.-1"
4658
}
4759
},
4860
"nbformat": 4,

labs/mcp-prm-oauth/main.bicep

Lines changed: 9 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ param tags object = {}
3232

3333
param location string = resourceGroup().location
3434

35-
param mcpPrmPath string = 'mcp'
35+
param mcpApiPath string = 'mcp'
3636

3737
// ------------------
3838
// VARIABLES
@@ -60,7 +60,7 @@ module appInsightsModule '../../modules/monitor/v1/appinsights.bicep' = {
6060
}
6161

6262
// 3. API Management
63-
module apimModule '../../modules/apim/v2/apim.bicep' = {
63+
module apimModule '../../modules/apim/v4/apim.bicep' = {
6464
name: 'apimModule'
6565
params: {
6666
apimSku: apimSku
@@ -71,7 +71,7 @@ module apimModule '../../modules/apim/v2/apim.bicep' = {
7171
}
7272
}
7373

74-
resource apimService 'Microsoft.ApiManagement/service@2024-06-01-preview' existing = {
74+
resource apimService 'Microsoft.ApiManagement/service@2025-09-01-preview' existing = {
7575
name: 'apim-${resourceSuffix}'
7676
dependsOn: [
7777
apimModule
@@ -214,28 +214,16 @@ resource mcpServerContainerApp 'Microsoft.App/containerApps@2023-11-02-preview'
214214
]
215215
}
216216

217-
// 9. Entra ID Application Registration for MCP
218-
// NOTE: The Microsoft Graph Bicep extension is experimental and may not be available in all environments.
219-
// If the deployment fails due to Graph extension issues, you have two options:
220-
// 1. Create the Entra App manually and provide the mcpClientId parameter
221-
// 2. Use Azure CLI or PowerShell scripts to create the app registration
222-
//
223-
// To create manually:
224-
// - Create an App Registration in Entra ID
225-
// - Add API permission: user_impersonate (delegated)
226-
// - Add redirect URI: https://{containerAppFQDN}/auth/callback
227-
// - Create a federated credential trusting the managed identity
228-
// - Pass the App ID as mcpClientId parameter
217+
// 9. Entra ID Application Registration for MCP endpoint
229218
module mcpEntraAppModule 'src/bicep/identity/mcp-entra-app.bicep' = if (empty(mcpClientId)) {
230219
name: 'mcpEntraAppModule'
231220
params: {
232221
mcpAppUniqueName: mcpEntraAppName
233222
mcpAppDisplayName: mcpEntraAppName
234223
tenantId: subscription().tenantId
235-
userAssignedIdentityPrincipleId: managedIdentityModule.outputs.identityPrincipalId
236224
webAppName: mcpServerContainerApp.name
237-
// Application ID URI == PRM resource == JWT audience (all resolve to https://<apim>.azure-api.net/<mcpPrmPath>)
238-
applicationIdUri: '${apimModule.outputs.gatewayUrl}/${mcpPrmPath}'
225+
userAssignedIdentityPrincipleId: managedIdentityModule.outputs.identityPrincipalId
226+
applicationIdUri: '${apimModule.outputs.gatewayUrl}/${mcpApiPath}/mcp'
239227
}
240228
}
241229

@@ -247,7 +235,7 @@ module mcpApiModule 'src/bicep/apim-mcp/mcp-api.bicep' = {
247235
webAppName: mcpServerContainerApp.name
248236
mcpAppId: !empty(mcpClientId) ? mcpClientId : (mcpEntraAppModule.?outputs.mcpAppId ?? '')
249237
mcpAppTenantId: subscription().tenantId
250-
mcpApiPath: mcpPrmPath
238+
mcpApiPath: mcpApiPath
251239
}
252240
}
253241

@@ -299,5 +287,5 @@ output mcpServerURL string = 'https://${mcpServerContainerApp.properties.configu
299287
// MCP Configuration
300288
output mcpAppId string = !empty(mcpClientId) ? mcpClientId : (mcpEntraAppModule.?outputs.mcpAppId ?? 'Not created - provide mcpClientId parameter')
301289
output mcpAppTenantId string = subscription().tenantId
302-
output mcpApiEndpoint string = '${apimModule.outputs.gatewayUrl}/${mcpPrmPath}'
303-
output mcpPrmEndpoint string = '${apimModule.outputs.gatewayUrl}/${mcpPrmPath}/.well-known/oauth-protected-resource'
290+
output mcpApiEndpoint string = '${apimModule.outputs.gatewayUrl}/${mcpApiPath}/mcp'
291+
output mcpPrmEndpoint string = '${apimModule.outputs.gatewayUrl}/.well-known/oauth-protected-resource/${mcpApiPath}/mcp'

labs/mcp-prm-oauth/mcp-prm-oauth.ipynb

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,9 @@
5656
"import utils\n",
5757
"\n",
5858
"deployment_name = os.path.basename(os.path.dirname(globals()['__vsc_ipynb_file__']))\n",
59-
"resource_group_name = f\"lab-{deployment_name}\" # change the name to match your naming style\n",
59+
"resource_group_name = f\"lab-{deployment_name}-4\" # change the name to match your naming style\n",
6060
"resource_group_location = \"ukwest\"\n",
61+
"resource_suffix = ''.join(random.choices(string.ascii_letters + string.digits, k=8)).lower()\n",
6162
"\n",
6263
"aiservices_config = []\n",
6364
"\n",
@@ -71,8 +72,7 @@
7172
"inference_api_version = \"2025-03-01-preview\"\n",
7273
"foundry_project_name = deployment_name\n",
7374
"\n",
74-
"# Generate a unique name for the app registration\n",
75-
"app_registration_name = f\"lab-{deployment_name}-{''.join(random.choices(string.ascii_letters + string.digits, k=8)).lower()}-app\"\n",
75+
"app_registration_name = f\"lab-{deployment_name}-{resource_suffix}-app\"\n",
7676
"\n",
7777
"build = 0\n",
7878
"prm_mcp_server_image = \"mcp-prm-server\"\n",
@@ -83,8 +83,9 @@
8383
"encryption_iv = base64.b64encode(os.urandom(16)).decode('utf-8')\n",
8484
"encryption_key = base64.b64encode(os.urandom(16)).decode('utf-8')\n",
8585
"oauth_scopes = 'openid https://graph.microsoft.com/.default'\n",
86+
"mcpApiPath = \"profile\" # path to the MCP API in the APIM service\n",
8687
"\n",
87-
"utils.print_ok('Notebook initialized')"
88+
"utils.print_ok('Notebook initialized')\n"
8889
]
8990
},
9091
{
@@ -142,13 +143,13 @@
142143
" \"mcpEntraAppName\": { \"value\": app_registration_name },\n",
143144
" \"apimSku\": { \"value\": apim_sku },\n",
144145
" \"aiServicesConfig\": { \"value\": aiservices_config },\n",
145-
" #\"mcpPrmPath\": { \"value\": mcpPrmPath }, ## Add a path you would like your MCP server to exposed as ##\n",
146146
" \"modelsConfig\": { \"value\": models_config },\n",
147147
" \"apimSubscriptionsConfig\": { \"value\": apim_subscriptions_config },\n",
148148
" \"inferenceAPIPath\": { \"value\": inference_api_path },\n",
149149
" \"inferenceAPIType\": { \"value\": inference_api_type },\n",
150150
" \"foundryProjectName\": { \"value\": foundry_project_name },\n",
151151
" \"oauthScopes\": { \"value\": oauth_scopes },\n",
152+
" \"mcpApiPath\": { \"value\": mcpApiPath },\n",
152153
" \"encryptionIV\": { \"value\": encryption_iv },\n",
153154
" \"encryptionKey\": { \"value\": encryption_key },\n",
154155
" }\n",
@@ -160,7 +161,7 @@
160161
"\n",
161162
"# Run the deployment\n",
162163
"output = utils.run(f\"az deployment group create --name {deployment_name} --resource-group {resource_group_name} --template-file main.bicep --parameters params.json\",\n",
163-
" f\"Deployment '{deployment_name}' succeeded\", f\"Deployment '{deployment_name}' failed\")"
164+
" f\"Deployment '{deployment_name}' succeeded\", f\"Deployment '{deployment_name}' failed\")\n"
164165
]
165166
},
166167
{
@@ -255,7 +256,7 @@
255256
"# Unauthenticated call should fail with 401 Unauthorized\n",
256257
"import requests\n",
257258
"\n",
258-
"mcp_server_url = f\"{apim_resource_gateway_url}/mcp\"\n",
259+
"mcp_server_url = mcp_api_endpoint # e.g. https://<apim-gateway-host>/<mcpApiPath>/mcp\n",
259260
"utils.print_info(\"Calling sse endpoint WITHOUT authorization...\")\n",
260261
"utils.print_message(f\"MCP Server Url : {mcp_server_url}\")\n",
261262
"response = requests.post(mcp_server_url, headers={\"Content-Type\": \"application/json\"})\n",
@@ -299,13 +300,14 @@
299300
"1. Execute `npx @modelcontextprotocol/inspector` in a terminal\n",
300301
"2. Access the provided URL in a browser (it should open automatically).\n",
301302
"3. Set the transport type as `Streamable HTTP`\n",
302-
"4. Provide the MCP server URL\n",
303+
"4. Provide the MCP server URL (the `mcp_api_endpoint` value, e.g. `https://<apim-gateway-host>/<mcpApiPath>/mcp`)\n",
303304
"5. Click in the `Open Auth Settings` button\n",
304-
"6. Click on `Quick OAuth Flow`\n",
305-
"7. You’ll see a sign-in screen or an “Application Access Request” screen asking for your consent to use your signed-in account. After reviewing the request, click \"Allow\" to proceed.\n",
306-
"8. After being redirected back to the MCP Inspector, scroll down to the `Authentication Complete` step. Expand the `Access Tokens` section and copy the `access_token` value.\n",
307-
"9. Expand the Authentication section on the left and paste the `access_token` into the Bearer Token parameter.\n",
308-
"10. Click on \"Connect\" and verify that the Weather Tool is functioning properly."
305+
"6. In the auth settings, set the `Client ID` to your MCP app registration's `client_id` and set the `Scope` to `<mcp_api_endpoint>/user_impersonate` (e.g. `https://<apim-gateway-host>/<mcpApiPath>/mcp/user_impersonate` — the same value advertised in the `scopes_supported` field of the Protected Resource Metadata)\n",
306+
"7. Click on `Quick OAuth Flow`\n",
307+
"8. You’ll see a sign-in screen or an “Application Access Request” screen asking for your consent to use your signed-in account. After reviewing the request, click \"Allow\" to proceed.\n",
308+
"9. After being redirected back to the MCP Inspector, scroll down to the `Authentication Complete` step. Expand the `Access Tokens` section and copy the `access_token` value.\n",
309+
"10. Expand the Authentication section on the left and paste the `access_token` into the Bearer Token parameter.\n",
310+
"11. Click on \"Connect\" and verify that the Weather Tool is functioning properly.\n"
309311
]
310312
},
311313
{
@@ -322,7 +324,7 @@
322324
],
323325
"metadata": {
324326
"kernelspec": {
325-
"display_name": "pip2uv",
327+
"display_name": "ai-gateway (3.12.13.final.0)",
326328
"language": "python",
327329
"name": "python3"
328330
},

labs/mcp-prm-oauth/src/bicep/apim-mcp/mcp-api.bicep

Lines changed: 23 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@ param mcpAppTenantId string
1414
param mcpApiPath string = 'mcp'
1515

1616
// Get reference to the existing APIM service
17-
resource apimService 'Microsoft.ApiManagement/service@2024-06-01-preview' existing = {
17+
resource apimService 'Microsoft.ApiManagement/service@2025-09-01-preview' existing = {
1818
name: apimServiceName
1919
}
2020

21-
resource dynamicDiscovery 'Microsoft.ApiManagement/service/apis@2023-05-01-preview' existing = {
21+
resource dynamicDiscovery 'Microsoft.ApiManagement/service/apis@2025-09-01-preview' existing = {
2222
parent: apimService
2323
name: 'mcp-prm-dynamic-discovery'
2424
}
@@ -71,36 +71,38 @@ resource mcpApiPathNamedValue 'Microsoft.ApiManagement/service/namedValues@2021-
7171
}
7272
}
7373

74-
// Create mcp backend pointing to the Container App
75-
resource mcpBackend 'Microsoft.ApiManagement/service/backends@2024-06-01-preview' ={
74+
resource mcpBackendServerUrl 'Microsoft.ApiManagement/service/backends@2025-09-01-preview' = {
7675
parent: apimService
7776
name: '${webAppName}-mcp-backend'
7877
properties: {
78+
url: 'https://${containerApp.properties.configuration.ingress.fqdn}'
7979
protocol: 'http'
80-
url: 'https://${containerApp.properties.configuration.ingress.fqdn}/mcp'
8180
tls: {
8281
validateCertificateChain: true
8382
validateCertificateName: true
8483
}
8584
type: 'Single'
86-
}
85+
}
8786
}
8887

89-
// Create the MCP API definition in APIM
90-
resource mcpApi 'Microsoft.ApiManagement/service/apis@2024-06-01-preview' = {
88+
// Create the MCP Passthrough definition in APIM
89+
resource mcpApi 'Microsoft.ApiManagement/service/apis@2025-09-01-preview' = {
9190
parent: apimService
92-
name: '${webAppName}-mcp-tools'
91+
name: '${webAppName}-mcp-server'
9392
properties: {
94-
displayName: '${webAppName} MCP Tools'
93+
displayName: '${webAppName} MCP Server'
9594
type: 'mcp'
9695
subscriptionRequired: false
97-
backendId: mcpBackend.name
98-
path: '/${mcpApiPath}'
99-
protocols: [
100-
'https'
101-
]
102-
mcpProperties:{
103-
transportType: 'streamable'
96+
backendId: mcpBackendServerUrl.name
97+
description: 'MCP API for ${webAppName} retrieving authenticated account Graph info'
98+
path: mcpApiPath
99+
protocols: ['https']
100+
mcpProperties: {
101+
endpoints : {
102+
mcp: {
103+
uriTemplate: '/mcp'
104+
}
105+
}
104106
}
105107
authenticationSettings: {
106108
oAuth2AuthenticationSettings: []
@@ -111,7 +113,7 @@ resource mcpApi 'Microsoft.ApiManagement/service/apis@2024-06-01-preview' = {
111113
}
112114

113115
// Apply policy at the API level for all operations
114-
resource mcpApiPolicy 'Microsoft.ApiManagement/service/apis/policies@2024-06-01-preview' = {
116+
resource mcpApiPolicy 'Microsoft.ApiManagement/service/apis/policies@2025-09-01-preview' = {
115117
parent: mcpApi
116118
name: 'policy'
117119
properties: {
@@ -125,47 +127,20 @@ resource mcpApiPolicy 'Microsoft.ApiManagement/service/apis/policies@2024-06-01-
125127
]
126128
}
127129

128-
// Create the PRM (Protected Resource Metadata) endpoint within MCP server
129-
resource mcpPrmOperation 'Microsoft.ApiManagement/service/apis/operations@2023-05-01-preview' = {
130-
parent: mcpApi
131-
name: 'mcp-prm-operation'
132-
properties: {
133-
displayName: 'Protected Resource Metadata'
134-
method: 'GET'
135-
urlTemplate: '/.well-known/oauth-protected-resource'
136-
description: 'Protected Resource Metadata endpoint (RFC 9728)'
137-
}
138-
}
139-
140-
// Apply specific policy for the PRM endpoint (anonymous access)
141-
resource mcpPrmOperationPolicy 'Microsoft.ApiManagement/service/apis/operations/policies@2023-05-01-preview' = {
142-
parent: mcpPrmOperation
143-
name: 'policy'
144-
properties: {
145-
format: 'rawxml'
146-
value: loadTextContent('mcp-prm.policy.xml')
147-
}
148-
dependsOn: [
149-
APIMGatewayURLNamedValue
150-
mcpTenantIdNamedValue
151-
mcpClientIdNamedValue
152-
]
153-
}
154-
155130
// Create the PRM (Protected Resource Metadata in the global discovery) endpoint - RFC 9728
156-
resource mcpPrmDiscoveryOperation 'Microsoft.ApiManagement/service/apis/operations@2023-05-01-preview' = {
131+
resource mcpPrmDiscoveryOperation 'Microsoft.ApiManagement/service/apis/operations@2025-09-01-preview' = {
157132
parent: dynamicDiscovery
158133
name: 'mcp-prm-discovery-operation'
159134
properties: {
160135
displayName: 'Protected Resource Metadata'
161136
method: 'GET'
162-
urlTemplate: '/${mcpApiPath}'
137+
urlTemplate: '/${mcpApiPath}/mcp'
163138
description: 'Protected Resource Metadata endpoint (RFC 9728)'
164139
}
165140
}
166141

167142
// Apply specific policy for the PRM endpoint (anonymous access)
168-
resource mcpPrmGlobalPolicy 'Microsoft.ApiManagement/service/apis/operations/policies@2023-05-01-preview' = {
143+
resource mcpPrmGlobalPolicy 'Microsoft.ApiManagement/service/apis/operations/policies@2025-09-01-preview' = {
169144
parent: mcpPrmDiscoveryOperation
170145
name: 'policy'
171146
properties: {

labs/mcp-prm-oauth/src/bicep/apim-mcp/mcp-api.policy.xml

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@
66
<policies>
77
<inbound>
88
<base />
9-
<!-- Validate Azure AD JWT Token -->
10-
<validate-azure-ad-token tenant-id="{{McpTenantId}}" failed-validation-httpcode="401" failed-validation-error-message="Unauthorized">
11-
<audiences>
12-
<!-- v2 tokens carry the appId (GUID) as aud; the Application ID URI is accepted too. -->
13-
<audience>{{McpClientId}}</audience>
14-
<audience>{{APIMGatewayURL}}/{{McpApiPath}}</audience>
15-
</audiences>
16-
</validate-azure-ad-token>
9+
<!-- Validate Azure AD JWT Token -->
10+
11+
<validate-azure-ad-token tenant-id="{{McpTenantId}}" failed-validation-httpcode="401" failed-validation-error-message="Unauthorized">
12+
<audiences>
13+
<audience>{{McpClientId}}</audience>
14+
<audience>{{APIMGatewayURL}}/{{McpApiPath}}</audience>
15+
</audiences>
16+
</validate-azure-ad-token>
1717
</inbound>
1818
<backend>
1919
<base />
@@ -28,11 +28,10 @@
2828
<return-response>
2929
<set-status code="401" reason="Unauthorized" />
3030
<set-header name="WWW-Authenticate" exists-action="override">
31-
<value>Bearer error="invalid_token", resource_metadata="{{APIMGatewayURL}}/.well-known/oauth-protected-resource"</value>
31+
<value>Bearer error="invalid_token", resource_metadata="{{APIMGatewayURL}}/.well-known/oauth-protected-resource/{{McpApiPath}}/mcp/"</value>
3232
</set-header>
3333
</return-response>
3434
</when>
3535
</choose>
36-
<!-- Handle authentication/authorization errors -->
3736
</on-error>
3837
</policies>

labs/mcp-prm-oauth/src/bicep/apim-mcp/mcp-prm.policy.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,15 @@
1717
</set-header>
1818
<set-body>@{
1919
return JsonConvert.SerializeObject(new {
20-
resource = "{{APIMGatewayURL}}/{{McpApiPath}}",
20+
resource = "{{APIMGatewayURL}}/{{McpApiPath}}/mcp",
2121
authorization_servers = new[] {
2222
$"https://login.microsoftonline.com/{{McpTenantId}}/v2.0"
2323
},
2424
bearer_methods_supported = new[] {
2525
"header"
2626
},
2727
scopes_supported = new[] {
28-
"{{APIMGatewayURL}}/{{McpApiPath}}/user_impersonate"
28+
"{{APIMGatewayURL}}/{{McpApiPath}}/mcp/user_impersonate"
2929
}
3030
});
3131
}</set-body>

0 commit comments

Comments
 (0)