Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
21 changes: 21 additions & 0 deletions docs/azmcp-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -930,6 +930,27 @@ azmcp sql elastic-pool list --subscription <subscription> \
### Azure SQL Server Operations

```bash
# Create a new SQL server
azmcp sql server create --subscription <subscription> \
--resource-group <resource-group> \
--server <server-name> \
--location <location> \
--admin-user <admin-username> \
--admin-password <admin-password> \
[--version <server-version>] \
[--minimal-tls-version <tls-version>] \
Comment thread
ericshape marked this conversation as resolved.
Outdated
[--public-network-access <enabled|disabled>]

# Delete a SQL server
azmcp sql server delete --subscription <subscription> \
Comment thread
ericshape marked this conversation as resolved.
--resource-group <resource-group> \
--server <server-name>

# Show details of a specific SQL server
azmcp sql server show --subscription <subscription> \
--resource-group <resource-group> \
--server <server-name>

# List Microsoft Entra ID administrators for a SQL server
azmcp sql server entra-admin list --subscription <subscription> \
--resource-group <resource-group> \
Expand Down
15 changes: 12 additions & 3 deletions docs/e2eTestPrompts.md
Original file line number Diff line number Diff line change
Expand Up @@ -354,18 +354,27 @@ This file contains prompts used for end-to-end testing to ensure each tool is in

| Tool Name | Test Prompt |
|:----------|:----------|
| azmcp_sql_server_create | Create a new Azure SQL server named <server_name> in resource group <resource_group_name> |
| azmcp_sql_server_create | Create an Azure SQL server with name <server_name> in location <location> with admin user <admin_user> |
| azmcp_sql_server_create | Set up a new SQL server called <server_name> in my resource group <resource_group_name> |
| azmcp_sql_server_delete | Delete the Azure SQL server <server_name> from resource group <resource_group_name> |
| azmcp_sql_server_delete | Remove the SQL server <server_name> from my subscription |
| azmcp_sql_server_delete | Delete SQL server <server_name> permanently |
| azmcp_sql_server_entra-admin_list | List Microsoft Entra ID administrators for SQL server <server_name> |
| azmcp_sql_server_entra-admin_list | Show me the Entra ID administrators configured for SQL server <server_name> |
| azmcp_sql_server_entra-admin_list | What Microsoft Entra ID administrators are set up for my SQL server <server_name>? |
| 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> |
| 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_show | Show me the details of Azure SQL server <server_name> in resource group <resource_group_name> |
| azmcp_sql_server_show | Get the configuration details for SQL server <server_name> |
| azmcp_sql_server_show | Display the properties of SQL server <server_name> |

## Azure Storage

Expand Down
6 changes: 6 additions & 0 deletions servers/Azure.Mcp.Server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ The Azure MCP Server supercharges your agents with Azure context. Here are some
* "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'"
* "Create a new Azure SQL server in my resource group"
* "Show me details about my Azure SQL server 'myserver'"
* "Delete my Azure SQL server 'myserver'"

### 💾 Azure Storage

Expand Down Expand Up @@ -288,6 +291,9 @@ The Azure MCP Server supercharges your agents with Azure context. Here are some
### 🗄️ Azure SQL Server

* List Microsoft Entra ID administrators for SQL servers
* Create new SQL servers
* Show details and properties of SQL servers
* Delete SQL servers

### 💾 Azure Storage

Expand Down
120 changes: 120 additions & 0 deletions tools/Azure.Mcp.Tools.Sql/src/Commands/Server/ServerCreateCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// 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.Server;
using Azure.Mcp.Tools.Sql.Services;
using Microsoft.Extensions.Logging;

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

public sealed class ServerCreateCommand(ILogger<ServerCreateCommand> logger)
: BaseSqlCommand<ServerCreateOptions>(logger)
{
private const string CommandTitle = "Create SQL Server";

private readonly Option<string> _administratorLoginOption = SqlOptionDefinitions.AdministratorLoginOption;
private readonly Option<string> _administratorPasswordOption = SqlOptionDefinitions.AdministratorPasswordOption;
private readonly Option<string> _locationOption = SqlOptionDefinitions.LocationOption;
private readonly Option<string> _versionOption = SqlOptionDefinitions.VersionOption;
private readonly Option<string> _publicNetworkAccessOption = SqlOptionDefinitions.PublicNetworkAccessOption;

public override string Name => "create";

public override string Description =>
"""
Creates a new Azure SQL server in the specified resource group and location.
The server will be created with the specified administrator credentials and
optional configuration settings. Returns the created server with its properties
including the fully qualified domain name.
""";

public override string Title => CommandTitle;

public override ToolMetadata Metadata => new() { Destructive = false, ReadOnly = false };
Comment thread
ericshape marked this conversation as resolved.
Outdated

protected override void RegisterOptions(Command command)
{
base.RegisterOptions(command);
command.AddOption(_administratorLoginOption);
command.AddOption(_administratorPasswordOption);
command.AddOption(_locationOption);
command.AddOption(_versionOption);
command.AddOption(_publicNetworkAccessOption);
Comment thread
ericshape marked this conversation as resolved.
Outdated
}

protected override ServerCreateOptions BindOptions(ParseResult parseResult)
{
var options = base.BindOptions(parseResult);
options.AdministratorLogin = parseResult.GetValueForOption(_administratorLoginOption);
Comment thread
ericshape marked this conversation as resolved.
Outdated
options.AdministratorPassword = parseResult.GetValueForOption(_administratorPasswordOption);
options.Location = parseResult.GetValueForOption(_locationOption);
options.Version = parseResult.GetValueForOption(_versionOption);
options.PublicNetworkAccess = parseResult.GetValueForOption(_publicNetworkAccessOption);
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;
}
Comment thread
ericshape marked this conversation as resolved.
Outdated

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

var server = await sqlService.CreateServerAsync(
options.Server!,
options.ResourceGroup!,
options.Subscription!,
options.Location!,
options.AdministratorLogin!,
options.AdministratorPassword!,
options.Version,
options.PublicNetworkAccess,
options.RetryPolicy);

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

return context.Response;
}

protected override string GetErrorMessage(Exception ex) => ex switch
{
Azure.RequestFailedException reqEx when reqEx.Status == 409 =>
"A SQL server with this name already exists. Choose a different server name.",
Azure.RequestFailedException reqEx when reqEx.Status == 403 =>
$"Authorization failed creating the SQL server. Verify you have appropriate permissions. Details: {reqEx.Message}",
Azure.RequestFailedException reqEx when reqEx.Status == 400 =>
$"Invalid request parameters for SQL server creation: {reqEx.Message}",
Azure.RequestFailedException reqEx => reqEx.Message,
ArgumentException argEx => $"Invalid parameter: {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 ServerCreateResult(SqlServer Server);
}
135 changes: 135 additions & 0 deletions tools/Azure.Mcp.Tools.Sql/src/Commands/Server/ServerDeleteCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
// 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.Server;
using Azure.Mcp.Tools.Sql.Services;
using Microsoft.Extensions.Logging;

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

public sealed class ServerDeleteCommand(ILogger<ServerDeleteCommand> logger)
: BaseSqlCommand<ServerDeleteOptions>(logger)
{
private const string CommandTitle = "Delete SQL Server";

private readonly Option<bool> _forceOption = SqlOptionDefinitions.ForceOption;

public override string Name => "delete";

public override string Description =>
"""
Deletes an Azure SQL server and all of its databases from the specified resource group.
This operation is irreversible and will permanently remove the server and all its data.
Use the --force flag to skip confirmation prompts.
""";

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(_forceOption);
}

protected override ServerDeleteOptions BindOptions(ParseResult parseResult)
{
var options = base.BindOptions(parseResult);
options.Force = parseResult.GetValueForOption(_forceOption);
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;
}

// Show warning about destructive operation unless force is specified
if (!options.Force)
{
context.Response.Status = 200;
context.Response.Message =
$"WARNING: This operation will permanently delete the SQL server '{options.Server}' " +
$"and ALL its databases in resource group '{options.ResourceGroup}'. " +
$"This action cannot be undone. Use --force to confirm deletion.";
return context.Response;
}

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

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

if (deleted)
{
context.Response.Results = ResponseResult.Create(
new ServerDeleteResult($"SQL server '{options.Server}' was successfully deleted.", true),
SqlJsonContext.Default.ServerDeleteResult);
}
else
{
context.Response.Status = 404;
context.Response.Message = $"SQL server '{options.Server}' not found in resource group '{options.ResourceGroup}'.";
}
}
catch (Exception ex)
{
_logger.LogError(ex,
"Error deleting SQL server. Server: {Server}, ResourceGroup: {ResourceGroup}, Options: {@Options}",
options.Server, options.ResourceGroup, options);
HandleException(context, ex);
}

return context.Response;
}

protected override string GetErrorMessage(Exception ex) => ex switch
{
Azure.RequestFailedException reqEx when reqEx.Status == 404 =>
$"SQL server '{GetResourceName(ex)}' not found. It may have already been deleted.",
Azure.RequestFailedException reqEx when reqEx.Status == 403 =>
$"Authorization failed deleting the SQL server. Verify you have appropriate permissions. Details: {reqEx.Message}",
Azure.RequestFailedException reqEx when reqEx.Status == 409 =>
$"Cannot delete SQL server due to a conflict. It may be in use or have dependent resources. Details: {reqEx.Message}",
Azure.RequestFailedException reqEx => reqEx.Message,
ArgumentException argEx => $"Invalid parameter: {argEx.Message}",
_ => base.GetErrorMessage(ex)
};

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

private static string GetResourceName(Exception ex)
{
// Try to extract resource name from exception message if available
if (ex is Azure.RequestFailedException reqEx && reqEx.Message.Contains("'") && reqEx.Message.Contains("'"))
{
var start = reqEx.Message.IndexOf("'") + 1;
var end = reqEx.Message.IndexOf("'", start);
if (end > start)
{
return reqEx.Message.Substring(start, end - start);
}
}
Comment thread
ericshape marked this conversation as resolved.
Outdated
return "unknown";
}

internal record ServerDeleteResult(string Message, bool Success);
}
Loading
Loading