From 3031bd8b852664382d1998edf163e483c065f915 Mon Sep 17 00:00:00 2001 From: Ahmed Muhsin Date: Tue, 21 Apr 2026 11:22:24 -0500 Subject: [PATCH 1/6] Fix McpWeatherApp deployment: add weather infra, prepackage hook, and jar-relative file resolution - Add separate Flex Consumption plan and function app for weather service in main.bicep - Add FUNCTIONS_EXTENSIONBUNDLE_SOURCE_URI staging CDN setting for MCP extension - Add prepackage hook in azure.yaml to run npm build before Maven packaging - Fix WeatherFunction.java to resolve app/dist/index.html relative to jar location (required on Azure where CWD differs from function root) - Update mcp.json to support system key for remote MCP endpoint --- .vscode/mcp.json | 8 +++- azure.yaml | 8 ++++ infra/main.bicep | 46 ++++++++++++++++++- .../com/function/weather/WeatherFunction.java | 14 +++++- 4 files changed, 73 insertions(+), 3 deletions(-) diff --git a/.vscode/mcp.json b/.vscode/mcp.json index fb33d81..79acfd9 100644 --- a/.vscode/mcp.json +++ b/.vscode/mcp.json @@ -4,12 +4,18 @@ "type": "promptString", "id": "functionapp-name", "description": "Azure Functions App Name" + }, + { + "type": "promptString", + "id": "functionapp-key", + "description": "MCP Extension System Key", + "password": true } ], "servers": { "remote-mcp-function": { "type": "http", - "url": "https://${input:functionapp-name}.azurewebsites.net/runtime/webhooks/mcp" + "url": "https://${input:functionapp-name}.azurewebsites.net/runtime/webhooks/mcp?code=${input:functionapp-key}" }, "local-mcp-function": { "type": "http", diff --git a/azure.yaml b/azure.yaml index ee16dac..3e29a2d 100644 --- a/azure.yaml +++ b/azure.yaml @@ -15,3 +15,11 @@ services: project: ./samples/McpWeatherApp/ language: java host: function + hooks: + prepackage: + windows: + shell: pwsh + run: cd app; npm install; npm run build + posix: + shell: sh + run: cd app && npm install && npm run build diff --git a/infra/main.bicep b/infra/main.bicep index 1cb3de8..3db3a89 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -16,6 +16,7 @@ param environmentName string param location string param vnetEnabled bool param apiServiceName string = '' +param weatherServiceName string = '' param apiUserAssignedIdentityName string = '' param applicationInsightsName string = '' param appServicePlanName string = '' @@ -30,6 +31,8 @@ var resourceToken = toLower(uniqueString(subscription().id, environmentName, loc var tags = { 'azd-env-name': environmentName } var functionAppName = !empty(apiServiceName) ? apiServiceName : '${abbrs.webSitesFunctions}api-${resourceToken}' var deploymentStorageContainerName = 'app-package-${take(functionAppName, 32)}-${take(toLower(uniqueString(functionAppName, resourceToken)), 7)}' +var weatherFunctionAppName = !empty(weatherServiceName) ? weatherServiceName : '${abbrs.webSitesFunctions}weather-${resourceToken}' +var weatherDeploymentStorageContainerName = 'app-package-${take(weatherFunctionAppName, 32)}-${take(toLower(uniqueString(weatherFunctionAppName, resourceToken)), 7)}' // Organize resources in a resource group resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = { @@ -64,6 +67,21 @@ module appServicePlan './core/host/appserviceplan.bicep' = { } } +// Separate plan for weather app (Flex Consumption allows only one site per plan) +module weatherAppServicePlan './core/host/appserviceplan.bicep' = { + name: 'weatherappserviceplan' + scope: rg + params: { + name: '${abbrs.webServerFarms}weather-${resourceToken}' + location: location + tags: tags + sku: { + name: 'FC1' + tier: 'FlexConsumption' + } + } +} + module api './app/api.bicep' = { name: 'api' scope: rg @@ -80,6 +98,31 @@ module api './app/api.bicep' = { identityId: apiUserAssignedIdentity.outputs.identityId identityClientId: apiUserAssignedIdentity.outputs.identityClientId appSettings: { + FUNCTIONS_EXTENSIONBUNDLE_SOURCE_URI: 'https://cdn-staging.functions.azure.com/public' + } + virtualNetworkSubnetId: !vnetEnabled ? '' : serviceVirtualNetwork.outputs.appSubnetID + } +} + +// The weather app is a separate function app +module weather './app/api.bicep' = { + name: 'weather' + scope: rg + params: { + name: weatherFunctionAppName + location: location + tags: tags + applicationInsightsName: monitoring.outputs.applicationInsightsName + appServicePlanId: weatherAppServicePlan.outputs.id + runtimeName: 'java' + runtimeVersion: '17' + storageAccountName: storage.outputs.name + deploymentStorageContainerName: weatherDeploymentStorageContainerName + identityId: apiUserAssignedIdentity.outputs.identityId + identityClientId: apiUserAssignedIdentity.outputs.identityClientId + serviceName: 'weather' + appSettings: { + FUNCTIONS_EXTENSIONBUNDLE_SOURCE_URI: 'https://cdn-staging.functions.azure.com/public' } virtualNetworkSubnetId: !vnetEnabled ? '' : serviceVirtualNetwork.outputs.appSubnetID } @@ -93,7 +136,7 @@ module storage './core/storage/storage-account.bicep' = { name: !empty(storageAccountName) ? storageAccountName : '${abbrs.storageStorageAccounts}${resourceToken}' location: location tags: tags - containers: [{name: deploymentStorageContainerName}, {name: 'snippets'}] + containers: [{name: deploymentStorageContainerName}, {name: weatherDeploymentStorageContainerName}, {name: 'snippets'}] publicNetworkAccess: vnetEnabled ? 'Disabled' : 'Enabled' networkAcls: !vnetEnabled ? {} : { defaultAction: 'Deny' @@ -179,5 +222,6 @@ module appInsightsRoleAssignmentApi './core/monitor/appinsights-access.bicep' = output AZURE_LOCATION string = location output AZURE_TENANT_ID string = tenant().tenantId output SERVICE_API_NAME string = api.outputs.SERVICE_API_NAME +output SERVICE_WEATHER_NAME string = weather.outputs.SERVICE_API_NAME output AZURE_FUNCTION_NAME string = api.outputs.SERVICE_API_NAME output AZURE_RESOURCE_GROUP string = rg.name diff --git a/samples/McpWeatherApp/src/main/java/com/function/weather/WeatherFunction.java b/samples/McpWeatherApp/src/main/java/com/function/weather/WeatherFunction.java index 4eaf74b..824b15c 100644 --- a/samples/McpWeatherApp/src/main/java/com/function/weather/WeatherFunction.java +++ b/samples/McpWeatherApp/src/main/java/com/function/weather/WeatherFunction.java @@ -67,7 +67,19 @@ public String getWeatherWidget( executionContext.getLogger().info("GetWeatherWidget: serving weather widget UI"); - // Try loading from the filesystem (app/dist/index.html placed next to the function) + // Try loading relative to the jar location (required on Azure where CWD differs) + try { + java.io.File jarDir = new java.io.File( + WeatherFunction.class.getProtectionDomain().getCodeSource().getLocation().toURI()).getParentFile(); + java.io.File jarRelative = new java.io.File(jarDir, "app/dist/index.html"); + if (jarRelative.exists()) { + return java.nio.file.Files.readString(jarRelative.toPath(), StandardCharsets.UTF_8); + } + } catch (Exception e) { + executionContext.getLogger().log(Level.WARNING, "Failed to resolve jar-relative path", e); + } + + // Fallback: try CWD-relative path (works for local dev) java.io.File file = new java.io.File("app/dist/index.html"); if (file.exists()) { try { From f5432882b1b16744762e188a1065903aa908abee Mon Sep 17 00:00:00 2001 From: Ahmed Muhsin Date: Tue, 21 Apr 2026 12:45:36 -0500 Subject: [PATCH 2/6] Remove FUNCTIONS_EXTENSIONBUNDLE_SOURCE_URI staging CDN setting Extension bundle 4.32.0 with MCP support is already published to the production CDN (identical zip on both CDNs), so the staging override is no longer needed. --- infra/main.bicep | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/infra/main.bicep b/infra/main.bicep index 3db3a89..dfe8bc8 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -97,9 +97,7 @@ module api './app/api.bicep' = { deploymentStorageContainerName: deploymentStorageContainerName identityId: apiUserAssignedIdentity.outputs.identityId identityClientId: apiUserAssignedIdentity.outputs.identityClientId - appSettings: { - FUNCTIONS_EXTENSIONBUNDLE_SOURCE_URI: 'https://cdn-staging.functions.azure.com/public' - } + appSettings: {} virtualNetworkSubnetId: !vnetEnabled ? '' : serviceVirtualNetwork.outputs.appSubnetID } } @@ -121,9 +119,7 @@ module weather './app/api.bicep' = { identityId: apiUserAssignedIdentity.outputs.identityId identityClientId: apiUserAssignedIdentity.outputs.identityClientId serviceName: 'weather' - appSettings: { - FUNCTIONS_EXTENSIONBUNDLE_SOURCE_URI: 'https://cdn-staging.functions.azure.com/public' - } + appSettings: {} virtualNetworkSubnetId: !vnetEnabled ? '' : serviceVirtualNetwork.outputs.appSubnetID } } From 2536b630264f171bb3b4daa241cf1f5c39aae927 Mon Sep 17 00:00:00 2001 From: Ahmed Muhsin Date: Tue, 21 Apr 2026 18:05:20 -0500 Subject: [PATCH 3/6] Address review feedback: split try/catch, lowercase containers, fix pwsh hook - Split try/catch in WeatherFunction.java to separate path resolution from file read, with distinct error messages for each - Wrap function app names in toLower() for storage container names to avoid invalid mixed-case container names - Use && instead of ; in Windows pwsh prepackage hook so failures propagate correctly --- azure.yaml | 2 +- infra/main.bicep | 4 ++-- .../java/com/function/weather/WeatherFunction.java | 13 +++++++++---- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/azure.yaml b/azure.yaml index 3e29a2d..0ca3498 100644 --- a/azure.yaml +++ b/azure.yaml @@ -19,7 +19,7 @@ services: prepackage: windows: shell: pwsh - run: cd app; npm install; npm run build + run: cd app && npm install && npm run build posix: shell: sh run: cd app && npm install && npm run build diff --git a/infra/main.bicep b/infra/main.bicep index dfe8bc8..6e667d7 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -30,9 +30,9 @@ var abbrs = loadJsonContent('./abbreviations.json') var resourceToken = toLower(uniqueString(subscription().id, environmentName, location)) var tags = { 'azd-env-name': environmentName } var functionAppName = !empty(apiServiceName) ? apiServiceName : '${abbrs.webSitesFunctions}api-${resourceToken}' -var deploymentStorageContainerName = 'app-package-${take(functionAppName, 32)}-${take(toLower(uniqueString(functionAppName, resourceToken)), 7)}' +var deploymentStorageContainerName = 'app-package-${take(toLower(functionAppName), 32)}-${take(toLower(uniqueString(functionAppName, resourceToken)), 7)}' var weatherFunctionAppName = !empty(weatherServiceName) ? weatherServiceName : '${abbrs.webSitesFunctions}weather-${resourceToken}' -var weatherDeploymentStorageContainerName = 'app-package-${take(weatherFunctionAppName, 32)}-${take(toLower(uniqueString(weatherFunctionAppName, resourceToken)), 7)}' +var weatherDeploymentStorageContainerName = 'app-package-${take(toLower(weatherFunctionAppName), 32)}-${take(toLower(uniqueString(weatherFunctionAppName, resourceToken)), 7)}' // Organize resources in a resource group resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = { diff --git a/samples/McpWeatherApp/src/main/java/com/function/weather/WeatherFunction.java b/samples/McpWeatherApp/src/main/java/com/function/weather/WeatherFunction.java index 824b15c..42f3cd6 100644 --- a/samples/McpWeatherApp/src/main/java/com/function/weather/WeatherFunction.java +++ b/samples/McpWeatherApp/src/main/java/com/function/weather/WeatherFunction.java @@ -68,16 +68,21 @@ public String getWeatherWidget( executionContext.getLogger().info("GetWeatherWidget: serving weather widget UI"); // Try loading relative to the jar location (required on Azure where CWD differs) + java.io.File jarRelative = null; try { java.io.File jarDir = new java.io.File( WeatherFunction.class.getProtectionDomain().getCodeSource().getLocation().toURI()).getParentFile(); - java.io.File jarRelative = new java.io.File(jarDir, "app/dist/index.html"); - if (jarRelative.exists()) { - return java.nio.file.Files.readString(jarRelative.toPath(), StandardCharsets.UTF_8); - } + jarRelative = new java.io.File(jarDir, "app/dist/index.html"); } catch (Exception e) { executionContext.getLogger().log(Level.WARNING, "Failed to resolve jar-relative path", e); } + if (jarRelative != null && jarRelative.exists()) { + try { + return java.nio.file.Files.readString(jarRelative.toPath(), StandardCharsets.UTF_8); + } catch (IOException e) { + executionContext.getLogger().log(Level.WARNING, "Failed to read UI from jar-relative file", e); + } + } // Fallback: try CWD-relative path (works for local dev) java.io.File file = new java.io.File("app/dist/index.html"); From df3f962438fc49108be6dffa33bfceffc1747d9c Mon Sep 17 00:00:00 2001 From: Ahmed Muhsin <36454324+ahmedmuhsin@users.noreply.github.com> Date: Thu, 30 Apr 2026 17:25:18 -0500 Subject: [PATCH 4/6] Update extension bundle version format in host.json --- samples/FunctionsMcpTool/host.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/FunctionsMcpTool/host.json b/samples/FunctionsMcpTool/host.json index 4cc3c2e..b7e5ad1 100644 --- a/samples/FunctionsMcpTool/host.json +++ b/samples/FunctionsMcpTool/host.json @@ -2,6 +2,6 @@ "version": "2.0", "extensionBundle": { "id": "Microsoft.Azure.Functions.ExtensionBundle", - "version": "[4.29.0, 5.0.0)" + "version": "[4.*, 5.0.0)" } } From 57e80fdae794505bc114dc099b241da3de2069fd Mon Sep 17 00:00:00 2001 From: Ahmed Muhsin <36454324+ahmedmuhsin@users.noreply.github.com> Date: Thu, 30 Apr 2026 17:25:36 -0500 Subject: [PATCH 5/6] Update extension bundle version range --- samples/McpWeatherApp/host.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/McpWeatherApp/host.json b/samples/McpWeatherApp/host.json index 38a6669..65ec966 100644 --- a/samples/McpWeatherApp/host.json +++ b/samples/McpWeatherApp/host.json @@ -18,6 +18,6 @@ }, "extensionBundle": { "id": "Microsoft.Azure.Functions.ExtensionBundle", - "version": "[4.32.0, 4.33.0)" + "version": "[4.*, 5.0.0)" } } From cfe2cd6aea51b79ea7587ea51d3c2f0da7facfa4 Mon Sep 17 00:00:00 2001 From: Ahmed Muhsin Date: Thu, 30 Apr 2026 17:53:02 -0500 Subject: [PATCH 6/6] Add McpWeatherApp local.settings.json for local development --- samples/McpWeatherApp/local.settings.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 samples/McpWeatherApp/local.settings.json diff --git a/samples/McpWeatherApp/local.settings.json b/samples/McpWeatherApp/local.settings.json new file mode 100644 index 0000000..e97d6ec --- /dev/null +++ b/samples/McpWeatherApp/local.settings.json @@ -0,0 +1,7 @@ +{ + "IsEncrypted": false, + "Values": { + "AzureWebJobsStorage": "UseDevelopmentStorage=true", + "FUNCTIONS_WORKER_RUNTIME": "java" + } +}