Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .vscode/cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -416,11 +416,13 @@
"filefilters",
"filesystem",
"filesystems",
"flexconsumption",
"fnames",
"francecentral",
"frontendservice",
"functionapp",
"functionapps",
"functionspremium",
"germanynorth",
"grpcio",
"gsaascend",
Expand Down Expand Up @@ -606,6 +608,7 @@
"vsmarketplace",
"vsts",
"vuepress",
"webjobs",
"westcentralus",
"westeurope",
"westus",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2196,6 +2196,39 @@
"mappedToolList": [
"speech_tts_synthesize"
]
},
{
"name": "create_azure_app_resource",
"description": "Create Azure application platform services, such as Azure Functions.",
"toolMetadata": {
"destructive": {
"value": true,
"description": "This tool performs only additive updates without deleting or modifying existing resources."
},
"idempotent": {
"value": false,
"description": "Running this operation multiple times with the same arguments produces the same result without additional effects."
},
"openWorld": {
"value": false,
"description": "This tool's domain of interaction is closed and well-defined, limited to a specific set of entities."
},
"readOnly": {
"value": false,
"description": "This tool only performs read operations without modifying any state or data."
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This description should describe what the tool/tools will do to support this metadata value. Given that readOnly is false, it shouldn't say this tool only performs read operations. You can look at other tools with readOnly=false.

Same comment for other metadata descriptions.

},
"secret": {
"value": false,
"description": "This tool does not handle sensitive or secret information."
},
"localRequired": {
"value": false,
"description": "This tool is available in both local and remote server modes."
}
},
"mappedToolList": [
"functionapp_create"
]
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ public interface IResourceGroupService
Task<List<ResourceGroupInfo>> GetResourceGroups(string subscriptionId, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default);
Task<ResourceGroupInfo?> GetResourceGroup(string subscriptionId, string resourceGroupName, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default);
Task<ResourceGroupResource?> GetResourceGroupResource(string subscriptionId, string resourceGroupName, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default);
Task<ResourceGroupResource> CreateOrUpdateResourceGroup(string subscriptionId, string resourceGroupName, string location, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
Expand Up @@ -111,4 +111,22 @@ public async Task<List<ResourceGroupInfo>> GetResourceGroups(string subscription
throw new Exception($"Error retrieving resource group {resourceGroupName}: {ex.Message}", ex);
}
}

public async Task<ResourceGroupResource> CreateOrUpdateResourceGroup(string subscription, string resourceGroupName, string location, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default)
{
ValidateRequiredParameters((nameof(subscription), subscription), (nameof(resourceGroupName), resourceGroupName), (nameof(location), location));

try
{
var subscriptionResource = await _subscriptionService.GetSubscription(subscription, tenant, retryPolicy, cancellationToken);
var op = await subscriptionResource.GetResourceGroups()
.CreateOrUpdateAsync(WaitUntil.Completed, resourceGroupName, new ResourceGroupData(location), cancellationToken)
.ConfigureAwait(false);
return op.Value;
}
catch (Exception ex)
{
throw new Exception($"Error creating or updating resource group {resourceGroupName}: {ex.Message}", ex);
}
}
}
10 changes: 10 additions & 0 deletions servers/Azure.Mcp.Server/docs/azmcp-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -1041,6 +1041,16 @@ azmcp eventhubs namespace update --subscription <subscription> \
### Azure Function App Operations

```bash
# Create a function app with automatic provisioning of dependencies
# ✅ Destructive | ❌ Idempotent | ❌ OpenWorld | ❌ ReadOnly | ❌ Secret | ❌ LocalRequired
azmcp functionapp create --subscription <subscription> \
--resource-group <resource-group> \
--function-app <function-app-name> \
--location <location> \
[--plan-type <consumption|flex|premium|appservice|containerapp>] \
[--runtime <dotnet|dotnet-isolated|node|python|java|powershell>] \
[--os <windows|linux>]

# Get detailed properties of function apps
# ❌ Destructive | ✅ Idempotent | ❌ OpenWorld | ✅ ReadOnly | ❌ Secret | ❌ LocalRequired
azmcp functionapp get --subscription <subscription> \
Expand Down
5 changes: 5 additions & 0 deletions servers/Azure.Mcp.Server/docs/e2eTestPrompts.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,11 @@ This file contains prompts used for end-to-end testing to ensure each tool is in

| Tool Name | Test Prompt |
|:----------|:----------|
| functionapp_create | Create a new Azure Function App named <function_app_name> in <resource_group_name> |
| functionapp_create | Create a function app with Python runtime in <resource_group_name> |
| functionapp_create | Deploy a new function app to <location> region |
| functionapp_create | Set up a function app with premium hosting plan |
| functionapp_create | Create function app with container app hosting |
| functionapp_get | Describe the function app <function_app_name> in resource group <resource_group_name> |
| functionapp_get | Get configuration for function app <function_app_name> |
| functionapp_get | Get function app status for <function_app_name> |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
<ItemGroup>
<PackageReference Include="Azure.ResourceManager" />
<PackageReference Include="Azure.ResourceManager.AppService" />
<PackageReference Include="Azure.ResourceManager.Storage" />
<PackageReference Include="Azure.ResourceManager.AppContainers" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="ModelContextProtocol" />
<PackageReference Include="System.CommandLine" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Net;
using Azure.Mcp.Core.Commands;
using Azure.Mcp.Core.Extensions;
using Azure.Mcp.Core.Models.Option;
using Azure.Mcp.Tools.FunctionApp.Models;
using Azure.Mcp.Tools.FunctionApp.Options;
using Azure.Mcp.Tools.FunctionApp.Options.FunctionApp;
using Azure.Mcp.Tools.FunctionApp.Services;
using Microsoft.Extensions.Logging;
using Microsoft.Mcp.Core.Models.Option;

namespace Azure.Mcp.Tools.FunctionApp.Commands.FunctionApp;

public sealed class FunctionAppCreateCommand(ILogger<FunctionAppCreateCommand> logger)
: BaseFunctionAppCommand<FunctionAppCreateOptions>
{
private const string CommandTitle = "Create Azure Function App";
private readonly ILogger<FunctionAppCreateCommand> _logger = logger;

private readonly Option<string> _functionAppNameOption = FunctionAppOptionDefinitions.FunctionApp;
private readonly Option<string> _locationOption = FunctionAppOptionDefinitions.Location;
private readonly Option<string> _appServicePlanOption = FunctionAppOptionDefinitions.AppServicePlan;
private readonly Option<string> _planTypeOption = FunctionAppOptionDefinitions.PlanType;
private readonly Option<string> _planSkuOption = FunctionAppOptionDefinitions.PlanSku;
private readonly Option<string> _runtimeOption = FunctionAppOptionDefinitions.Runtime;
private readonly Option<string> _runtimeVersionOption = FunctionAppOptionDefinitions.RuntimeVersion;
private readonly Option<string> _osOption = FunctionAppOptionDefinitions.OperatingSystem;
private readonly Option<string> _storageAccountOption = FunctionAppOptionDefinitions.StorageAccount;
private readonly Option<string> _containerAppsEnvironmentOption = FunctionAppOptionDefinitions.ContainerAppsEnvironment;

public override string Id => "a19eaab4-4822-41cb-a6ec-ffdc56405400";

public override string Name => "create";

public override string Description =>
"""
Create a new Azure Function App in the specified resource group and region.
Automatically provisions dependencies when omitted (App Service plan OR Container App managed environment + Container App, and a Storage account) and applies sensible runtime & SKU defaults.
Required options:
- subscription: Target Azure subscription (ID or name)
- resource-group: Resource group (created if missing)
- function-app: Globally unique Function App name
- location: Azure region (e.g. eastus)
Optional options:
- app-service-plan: Use an existing App Service plan; if omitted one is created when hosting on App Service (non-container).
- plan-type: Hosting kind to create when a plan is needed (consumption|flex|premium|appservice|containerapp). Default: consumption.
* consumption -> Y1 (Dynamic)
* flex / flexconsumption -> FC1 (FlexConsumption, Linux only)
* premium / functionspremium -> EP1 (Elastic Premium)
* appservice -> B1 (Basic) unless overridden by --plan-sku
* containerapp -> Creates a Container App instead of an App Service plan/site (no plan created). Container App will reuse the function-app name.
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documentation formatting: Line 56 starts with an asterisk (*) instead of proper indentation/formatting like the lines above it. This should be indented consistently with the other bullets (lines 52-55) to maintain proper documentation structure.

Suggested change
* containerapp -> Creates a Container App instead of an App Service plan/site (no plan created). Container App will reuse the function-app name.
* containerapp -> Creates a Container App instead of an App Service plan/site (no plan created). Container App will reuse the function-app name.

Copilot uses AI. Check for mistakes.
- plan-sku: Explicit App Service plan SKU (e.g. B1, S1, P1v3). Overrides --plan-type SKU selection (ignored for containerapp).
- runtime: FUNCTIONS_WORKER_RUNTIME (dotnet|dotnet-isolated|node|python|java|powershell). Default: dotnet.
- runtime-version: Specific runtime version; if omitted a default per runtime is applied (see defaults below).
- os: windows|linux. Default: windows unless runtime/plan requires linux (python, flex consumption, containerapp). Overridden to linux automatically when required. Python & flex consumption do not support Windows.
Automatic resources & defaults:
- Storage account: Always created (Standard_LRS, HTTPS only, blob public access disabled). Name pattern: <sanitized-functionapp>[random6]. Connection string injected as AzureWebJobsStorage.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Azure Function supports using managed identity to connect to storage account. If it is not too hard we should recommend users to use that instead of the connection string since connection strings use storage account keys.

https://learn.microsoft.com/en-us/azure/azure-functions/storage-considerations?tabs=azure-cli#storage-account-connection-setting

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. I was following what Azure Portal does by default. That's been my pattern. I don't disagree that using managed identities are best practices.

- App Service plan: Auto-created when not provided (name: <function-app>-plan) unless containerapp hosting.
- Container App: If containerapp hosting selected, a managed environment and container app are created using the function-app name and an official Azure Functions image for the runtime.
- Linux vs Windows: Linux automatically enforced for python and flex consumption. Other runtimes default to Windows unless plan-type dictates Linux (flex) or runtime is python.
- Explicit --os overrides default when compatible; incompatible combinations cause validation errors (e.g. --os windows with python or flex consumption).
- Runtime version defaults (LinuxFxVersion when Linux):
* python -> 3.12
* node -> 22
* dotnet -> 8.0
* java -> 21.0
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documentation inconsistency: The command description states the default Java version is "21.0" (line 72), but the code implementation in GetDefaultRuntimeVersion returns "17" for Java runtime (line 55 in FunctionAppService.cs). These should match.

Suggested change
* java -> 21.0
* java -> 17.0

Copilot uses AI. Check for mistakes.
* powershell -> 7.4
- WEBSITE_NODE_DEFAULT_VERSION: Set to ~<major> for Windows Node apps when a version is supplied.
- FUNCTIONS_EXTENSION_VERSION: Always ~4.
Behavior notes:
- Providing --plan-sku with --plan-type is allowed; SKU wins.
- --container-app path skips App Service plan & site creation and provisions a Container App instead.
- Invalid combination examples: specifying app-service-plan with plan-type containerapp.
Returns: functionApp object (name, resourceGroup, location, plan, state, defaultHostName, tags)
""";

public override string Title => CommandTitle;

public override ToolMetadata Metadata => new()
{
Destructive = true,
Idempotent = false,
OpenWorld = false,
ReadOnly = false,
LocalRequired = false,
Secret = false
};

protected override void RegisterOptions(Command command)
{
base.RegisterOptions(command);
command.Options.Add(OptionDefinitions.Common.ResourceGroup.AsRequired());
command.Options.Add(_functionAppNameOption);
command.Options.Add(_locationOption);
command.Options.Add(_appServicePlanOption);
command.Options.Add(_planTypeOption);
command.Options.Add(_planSkuOption);
command.Options.Add(_runtimeOption);
command.Options.Add(_runtimeVersionOption);
command.Options.Add(_osOption);
command.Options.Add(_storageAccountOption);
command.Options.Add(_containerAppsEnvironmentOption);
}

protected override FunctionAppCreateOptions BindOptions(ParseResult parseResult)
{
var options = base.BindOptions(parseResult);
options.ResourceGroup ??= parseResult.GetValueOrDefault<string>(OptionDefinitions.Common.ResourceGroup.Name);
options.FunctionAppName = parseResult.GetValueOrDefault<string>(_functionAppNameOption.Name);
options.Location = parseResult.GetValueOrDefault<string>(_locationOption.Name);
options.AppServicePlan = parseResult.GetValueOrDefault<string>(_appServicePlanOption.Name);
options.PlanType = parseResult.GetValueOrDefault<string>(_planTypeOption.Name);
options.PlanSku = parseResult.GetValueOrDefault<string>(_planSkuOption.Name);
options.Runtime = parseResult.GetValueOrDefault<string>(_runtimeOption.Name) ?? "dotnet";
options.RuntimeVersion = parseResult.GetValueOrDefault<string>(_runtimeVersionOption.Name);
options.OperatingSystem = parseResult.GetValueOrDefault<string>(_osOption.Name);
options.StorageAccount = parseResult.GetValueOrDefault<string>(_storageAccountOption.Name);
options.ContainerAppsEnvironment = parseResult.GetValueOrDefault<string>(_containerAppsEnvironmentOption.Name);
return options;
}

public override async Task<CommandResponse> ExecuteAsync(CommandContext context, ParseResult parseResult, CancellationToken cancellationToken)
{
var options = BindOptions(parseResult);

try
{
if (!Validate(parseResult.CommandResult, context.Response).IsValid)
return context.Response;

if (!string.IsNullOrWhiteSpace(options.FunctionAppName))
{
var len = options.FunctionAppName.Length;
if (len < 2 || len > 43)
{
context.Response.Status = HttpStatusCode.BadRequest;
context.Response.Message = "function-app name must be between 2 and 43 characters.";
return context.Response;
}
}

if (!string.IsNullOrWhiteSpace(options.AppServicePlan) && string.Equals(options.PlanType, "containerapp", StringComparison.OrdinalIgnoreCase))
{
context.Response.Status = HttpStatusCode.BadRequest;
context.Response.Message = "--app-service-plan cannot be combined with --plan-type containerapp.";
return context.Response;
}

var service = context.GetService<IFunctionAppService>();
var result = await service.CreateFunctionApp(
options.Subscription!,
options.ResourceGroup!,
options.FunctionAppName!,
options.Location!,
options.AppServicePlan,
options.PlanType,
options.PlanSku,
options.Runtime ?? "dotnet",
options.RuntimeVersion,
options.OperatingSystem,
options.StorageAccount,
options.ContainerAppsEnvironment,
options.Tenant,
options.RetryPolicy,
cancellationToken);

context.Response.Results = ResponseResult.Create(
new FunctionAppCreateCommandResult(result),
FunctionAppJsonContext.Default.FunctionAppCreateCommandResult);
}
catch (Exception ex)
{
_logger.LogError(ex,
"Error creating function app. Subscription: {Subscription}, ResourceGroup: {ResourceGroup}, FunctionApp: {FunctionApp}, Options: {@Options}",
options.Subscription, options.ResourceGroup, options.FunctionAppName, options);
HandleException(context, ex);
}

return context.Response;
}

protected override string GetErrorMessage(Exception ex) => ex switch
{
RequestFailedException reqEx when reqEx.Status == (int)HttpStatusCode.Conflict =>
"Function App name already exists or conflict in resource group. Choose a different name or check plan settings.",
RequestFailedException reqEx when reqEx.Status == (int)HttpStatusCode.NotFound =>
"Resource group or plan not found. Verify the resource group and plan exist and you have access.",
RequestFailedException reqEx when reqEx.Status == (int)HttpStatusCode.Forbidden =>
$"Authorization failed accessing the Function App. Details: {reqEx.Message}",
RequestFailedException reqEx => reqEx.Message,
_ => base.GetErrorMessage(ex)
};

internal record FunctionAppCreateCommandResult(FunctionAppInfo FunctionApp);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
namespace Azure.Mcp.Tools.FunctionApp.Commands;

[JsonSerializable(typeof(FunctionAppGetCommand.FunctionAppGetCommandResult))]
[JsonSerializable(typeof(FunctionAppCreateCommand.FunctionAppCreateCommandResult))]
[JsonSerializable(typeof(FunctionAppInfo))]
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
internal partial class FunctionAppJsonContext : JsonSerializerContext;
5 changes: 5 additions & 0 deletions tools/Azure.Mcp.Tools.FunctionApp/src/FunctionAppSetup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ public void ConfigureServices(IServiceCollection services)
services.AddSingleton<IFunctionAppService, FunctionAppService>();

services.AddSingleton<FunctionAppGetCommand>();

services.AddSingleton<FunctionAppCreateCommand>();
}

public CommandGroup RegisterCommands(IServiceProvider serviceProvider)
Expand All @@ -29,6 +31,9 @@ public CommandGroup RegisterCommands(IServiceProvider serviceProvider)
var getCommand = serviceProvider.GetRequiredService<FunctionAppGetCommand>();
functionApp.AddCommand(getCommand.Name, getCommand);

var createCommand = serviceProvider.GetRequiredService<FunctionAppCreateCommand>();
functionApp.AddCommand(createCommand.Name, createCommand);

return functionApp;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@ public record FunctionAppInfo(
[property: JsonPropertyName("appServicePlanName")] string? AppServicePlanName,
[property: JsonPropertyName("status")] string? Status,
[property: JsonPropertyName("defaultHostName")] string? DefaultHostName,
[property: JsonPropertyName("operatingSystem")] string? OperatingSystem,
[property: JsonPropertyName("tags")] IDictionary<string, string>? Tags
);
Loading