Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 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
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
<PackageVersion Include="Azure.ResourceManager.Resources" Version="1.11.0" />
<PackageVersion Include="Azure.ResourceManager.RedisEnterprise" Version="1.2.1" />
<PackageVersion Include="Azure.ResourceManager.ResourceHealth" Version="1.1.0-beta.5" />
<PackageVersion Include="Azure.ResourceManager.Sql" Version="1.4.0-beta.3" />
<PackageVersion Include="Azure.ResourceManager.LoadTesting" Version="1.1.2" />
<PackageVersion Include="Azure.ResourceManager.Network" Version="1.11.2" />
<PackageVersion Include="Azure.ResourceManager.MachineLearning" Version="1.2.3" />
Expand Down
16 changes: 15 additions & 1 deletion docs/azmcp-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -898,8 +898,22 @@ azmcp sql db show --subscription <subscription> \
--server <server-name> \
--database <database>

# Create a firewall rule for a SQL server
azmcp sql server firewall-rule create --subscription <subscription> \
--resource-group <resource-group> \
--server <server-name> \
--firewall-rule-name <rule-name> \
--start-ip-address <start-ip> \
--end-ip-address <end-ip>

# Delete a firewall rule from a SQL server
azmcp sql server firewall-rule delete --subscription <subscription> \
--resource-group <resource-group> \
--server <server-name> \
--firewall-rule-name <rule-name>

# Gets a list of firewall rules for a SQL server
azmcp sql firewall-rule list --subscription <subscription> \
azmcp sql server firewall-rule list --subscription <subscription> \
--resource-group <resource-group> \
--server <server-name>
```
Expand Down
6 changes: 6 additions & 0 deletions docs/e2eTestPrompts.md
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,12 @@ This file contains prompts used for end-to-end testing to ensure each tool is in
| azmcp_sql_server_firewall-rule_list | List all firewall rules for SQL server <server_name> |
| azmcp_sql_server_firewall-rule_list | Show me the firewall rules for SQL server <server_name> |
| azmcp_sql_server_firewall-rule_list | What firewall rules are configured for my SQL server <server_name>? |
| azmcp_sql_server_firewall-rule_create | Create a firewall rule for my Azure SQL server <server_name> |
| azmcp_sql_server_firewall-rule_create | Add a firewall rule to allow access from IP range <start_ip> to <end_ip> for SQL server <server_name> |
| azmcp_sql_server_firewall-rule_create | Create a new firewall rule named <rule_name> for SQL server <server_name> |
| azmcp_sql_server_firewall-rule_delete | Delete a firewall rule from my Azure SQL server <server_name> |
| azmcp_sql_server_firewall-rule_delete | Remove the firewall rule <rule_name> from SQL server <server_name> |
| azmcp_sql_server_firewall-rule_delete | Delete firewall rule <rule_name> for SQL server <server_name> |

## Azure Storage

Expand Down
1 change: 1 addition & 0 deletions servers/Azure.Mcp.Server/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ The Azure MCP Server updates automatically by default whenever a new release com
## 0.5.12 (Unreleased)

### Features Added
- add `azmcp sql server firewall-rule create` and `azmcp sql server firewall-rule delete` commands. [#121](https://github.com/microsoft/mcp/pull/121)

### Breaking Changes

Expand Down
4 changes: 4 additions & 0 deletions servers/Azure.Mcp.Server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ The Azure MCP Server supercharges your agents with Azure context. Here are some
* "Show me details about my Azure SQL database 'mydb'"
* "List all databases in my Azure SQL server 'myserver'"
* "List all firewall rules for my Azure SQL server 'myserver'"
* "Create a firewall rule for my Azure SQL server 'myserver'"
* "Delete a firewall rule from my Azure SQL server 'myserver'"
* "List all elastic pools in my Azure SQL server 'myserver'"
* "List Active Directory administrators for my Azure SQL server 'myserver'"

Expand Down Expand Up @@ -276,6 +278,8 @@ The Azure MCP Server supercharges your agents with Azure context. Here are some
* Show database details and properties
* List the details and properties of all databases
* List SQL server firewall rules
* Create SQL server firewall rules
* Delete SQL server firewall rules

### 🗄️ Azure SQL Elastic Pool

Expand Down
1 change: 1 addition & 0 deletions tools/Azure.Mcp.Tools.Sql/src/Azure.Mcp.Tools.Sql.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
<ItemGroup />
<ItemGroup>
<PackageReference Include="Azure.ResourceManager" />
<PackageReference Include="Azure.ResourceManager.Sql" />
<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,112 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Azure.Mcp.Core.Commands;
using Azure.Mcp.Core.Services.Telemetry;
using Azure.Mcp.Tools.Sql.Models;
using Azure.Mcp.Tools.Sql.Options;
using Azure.Mcp.Tools.Sql.Options.FirewallRule;
using Azure.Mcp.Tools.Sql.Services;
using Microsoft.Extensions.Logging;

namespace Azure.Mcp.Tools.Sql.Commands.FirewallRule;

public sealed class FirewallRuleCreateCommand(ILogger<FirewallRuleCreateCommand> logger)
: BaseSqlCommand<FirewallRuleCreateOptions>(logger)
{
private const string CommandTitle = "Create SQL Server Firewall Rule";

private readonly Option<string> _firewallRuleNameOption = SqlOptionDefinitions.FirewallRuleNameOption;
private readonly Option<string> _startIpAddressOption = SqlOptionDefinitions.StartIpAddressOption;
private readonly Option<string> _endIpAddressOption = SqlOptionDefinitions.EndIpAddressOption;

public override string Name => "create";

public override string Description =>
"""
Creates a firewall rule for a SQL server. Firewall rules control which IP addresses
are allowed to connect to the SQL server. You can specify either a single IP address
(by setting start and end IP to the same value) or a range of IP addresses. Returns
the created firewall rule with its properties.
""";

public override string Title => CommandTitle;

public override ToolMetadata Metadata => new() { Destructive = false, ReadOnly = false };

protected override void RegisterOptions(Command command)
{
base.RegisterOptions(command);
command.AddOption(_firewallRuleNameOption);
command.AddOption(_startIpAddressOption);
command.AddOption(_endIpAddressOption);
}

protected override FirewallRuleCreateOptions BindOptions(ParseResult parseResult)
{
var options = base.BindOptions(parseResult);
options.FirewallRuleName = parseResult.GetValueForOption(_firewallRuleNameOption);
options.StartIpAddress = parseResult.GetValueForOption(_startIpAddressOption);
options.EndIpAddress = parseResult.GetValueForOption(_endIpAddressOption);
return options;
}

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

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

var sqlService = context.GetService<ISqlService>();

var firewallRule = await sqlService.CreateFirewallRuleAsync(
options.Server!,
options.ResourceGroup!,
options.Subscription!,
options.FirewallRuleName!,
options.StartIpAddress!,
options.EndIpAddress!,
options.RetryPolicy);

context.Response.Results = ResponseResult.Create(
new FirewallRuleCreateResult(firewallRule),
SqlJsonContext.Default.FirewallRuleCreateResult);
}
catch (Exception ex)
{
_logger.LogError(ex,
"Error creating SQL server firewall rule. Server: {Server}, ResourceGroup: {ResourceGroup}, Rule: {Rule}, Options: {@Options}",
options.Server, options.ResourceGroup, options.FirewallRuleName, options);
HandleException(context, ex);
}

return context.Response;
}

protected override string GetErrorMessage(Exception ex) => ex switch
{
Azure.RequestFailedException reqEx when reqEx.Status == 404 =>
"SQL server not found. Verify the server name, resource group, and that you have access.",
Azure.RequestFailedException reqEx when reqEx.Status == 403 =>
$"Authorization failed creating the firewall rule. Verify you have appropriate permissions. Details: {reqEx.Message}",
Azure.RequestFailedException reqEx when reqEx.Status == 409 =>
"A firewall rule with this name already exists. Choose a different name or update the existing rule.",
Azure.RequestFailedException reqEx => reqEx.Message,
ArgumentException argEx => $"Invalid IP address format: {argEx.Message}",
_ => base.GetErrorMessage(ex)
};

protected override int GetStatusCode(Exception ex) => ex switch
{
Azure.RequestFailedException reqEx => reqEx.Status,
ArgumentException => 400,
_ => base.GetStatusCode(ex)
};

internal record FirewallRuleCreateResult(SqlServerFirewallRule FirewallRule);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Azure.Mcp.Core.Commands;
using Azure.Mcp.Core.Services.Telemetry;
using Azure.Mcp.Tools.Sql.Options;
using Azure.Mcp.Tools.Sql.Options.FirewallRule;
using Azure.Mcp.Tools.Sql.Services;
using Microsoft.Extensions.Logging;

namespace Azure.Mcp.Tools.Sql.Commands.FirewallRule;

public sealed class FirewallRuleDeleteCommand(ILogger<FirewallRuleDeleteCommand> logger)
: BaseSqlCommand<FirewallRuleDeleteOptions>(logger)
{
private const string CommandTitle = "Delete SQL Server Firewall Rule";

private readonly Option<string> _firewallRuleNameOption = SqlOptionDefinitions.FirewallRuleNameOption;

public override string Name => "delete";

public override string Description =>
"""
Deletes a firewall rule from a SQL server. This operation removes the specified
firewall rule, potentially restricting access for the IP addresses that were
previously allowed by this rule. The operation is idempotent - if the rule
doesn't exist, no error is returned.
""";

public override string Title => CommandTitle;

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

protected override void RegisterOptions(Command command)
{
base.RegisterOptions(command);
command.AddOption(_firewallRuleNameOption);
}

protected override FirewallRuleDeleteOptions BindOptions(ParseResult parseResult)
{
var options = base.BindOptions(parseResult);
options.FirewallRuleName = parseResult.GetValueForOption(_firewallRuleNameOption);
return options;
}

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

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

var sqlService = context.GetService<ISqlService>();

var deleted = await sqlService.DeleteFirewallRuleAsync(
options.Server!,
options.ResourceGroup!,
options.Subscription!,
options.FirewallRuleName!,
options.RetryPolicy);

context.Response.Results = ResponseResult.Create(
new FirewallRuleDeleteResult(deleted, options.FirewallRuleName!),
SqlJsonContext.Default.FirewallRuleDeleteResult);
}
catch (Exception ex)
{
_logger.LogError(ex,
"Error deleting SQL server firewall rule. Server: {Server}, ResourceGroup: {ResourceGroup}, Rule: {Rule}, Options: {@Options}",
options.Server, options.ResourceGroup, options.FirewallRuleName, options);
HandleException(context, ex);
}

return context.Response;
}

protected override string GetErrorMessage(Exception ex) => ex switch
{
Azure.RequestFailedException reqEx when reqEx.Status == 404 =>
"SQL server or firewall rule not found. Verify the server name, rule name, resource group, and that you have access.",
Azure.RequestFailedException reqEx when reqEx.Status == 403 =>
$"Authorization failed deleting the firewall rule. Verify you have appropriate permissions. Details: {reqEx.Message}",
Azure.RequestFailedException reqEx => reqEx.Message,
ArgumentException argEx => argEx.Message,
_ => base.GetErrorMessage(ex)
};

protected override int GetStatusCode(Exception ex) => ex switch
{
Azure.RequestFailedException reqEx => reqEx.Status,
ArgumentException => 400,
_ => base.GetStatusCode(ex)
};

internal record FirewallRuleDeleteResult(bool Deleted, string RuleName);
}
2 changes: 2 additions & 0 deletions tools/Azure.Mcp.Tools.Sql/src/Commands/SqlJsonContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ namespace Azure.Mcp.Tools.Sql.Commands;
[JsonSerializable(typeof(DatabaseListCommand.DatabaseListResult))]
[JsonSerializable(typeof(EntraAdminListCommand.EntraAdminListResult))]
[JsonSerializable(typeof(FirewallRuleListCommand.FirewallRuleListResult))]
[JsonSerializable(typeof(FirewallRuleCreateCommand.FirewallRuleCreateResult))]
[JsonSerializable(typeof(FirewallRuleDeleteCommand.FirewallRuleDeleteResult))]
[JsonSerializable(typeof(ElasticPoolListCommand.ElasticPoolListResult))]
[JsonSerializable(typeof(SqlDatabase))]
[JsonSerializable(typeof(SqlServerEntraAdministrator))]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Text.Json.Serialization;
using Azure.Mcp.Tools.Sql.Options;

namespace Azure.Mcp.Tools.Sql.Options.FirewallRule;

public class FirewallRuleCreateOptions : BaseSqlOptions
{
[JsonPropertyName(SqlOptionDefinitions.FirewallRuleName)]
public string? FirewallRuleName { get; set; }

[JsonPropertyName(SqlOptionDefinitions.StartIpAddress)]
public string? StartIpAddress { get; set; }

[JsonPropertyName(SqlOptionDefinitions.EndIpAddress)]
public string? EndIpAddress { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Text.Json.Serialization;
using Azure.Mcp.Tools.Sql.Options;

namespace Azure.Mcp.Tools.Sql.Options.FirewallRule;

public class FirewallRuleDeleteOptions : BaseSqlOptions
{
[JsonPropertyName(SqlOptionDefinitions.FirewallRuleName)]
public string? FirewallRuleName { get; set; }
}
27 changes: 27 additions & 0 deletions tools/Azure.Mcp.Tools.Sql/src/Options/SqlOptionDefinitions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ public static class SqlOptionDefinitions
{
public const string ServerName = "server";
public const string DatabaseName = "database";
public const string FirewallRuleName = "firewall-rule-name";
public const string StartIpAddress = "start-ip-address";
public const string EndIpAddress = "end-ip-address";

public static readonly Option<string> Server = new(
$"--{ServerName}",
Expand All @@ -23,4 +26,28 @@ public static class SqlOptionDefinitions
{
IsRequired = true
};

public static readonly Option<string> FirewallRuleNameOption = new(
$"--{FirewallRuleName}",
"The name of the firewall rule."
)
{
IsRequired = true
};

public static readonly Option<string> StartIpAddressOption = new(
$"--{StartIpAddress}",
"The start IP address of the firewall rule range."
)
{
IsRequired = true
};

public static readonly Option<string> EndIpAddressOption = new(
$"--{EndIpAddress}",
"The end IP address of the firewall rule range."
)
{
IsRequired = true
};
}
Loading
Loading