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..0ca3498 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..6e667d7 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 = '' @@ -29,7 +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(toLower(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 @@ -79,8 +97,29 @@ module api './app/api.bicep' = { deploymentStorageContainerName: deploymentStorageContainerName identityId: apiUserAssignedIdentity.outputs.identityId identityClientId: apiUserAssignedIdentity.outputs.identityClientId - appSettings: { - } + appSettings: {} + 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: {} virtualNetworkSubnetId: !vnetEnabled ? '' : serviceVirtualNetwork.outputs.appSubnetID } } @@ -93,7 +132,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 +218,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/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)" } } 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)" } } 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" + } +} 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..42f3cd6 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,24 @@ 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) + java.io.File jarRelative = null; + try { + java.io.File jarDir = new java.io.File( + WeatherFunction.class.getProtectionDomain().getCodeSource().getLocation().toURI()).getParentFile(); + 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"); if (file.exists()) { try {