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
18 changes: 15 additions & 3 deletions docs/azmcp-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ azmcp appconfig kv list --subscription <subscription> \
[--key <key>] \
[--label <label>]

# Lock (make it read-only) or unlock (remove read-only) a key-value setting
# Lock (make it read-only) or unlock (remove read-only) a key-value setting
azmcp appconfig kv lock set --subscription <subscription> \
--account <account> \
--key <key> \
Expand Down Expand Up @@ -738,7 +738,7 @@ azmcp keyvault key list --subscription <subscription> \
Tools that handle sensitive data such as secrets require user consent before execution through a security mechanism called **elicitation**. When you run commands that access sensitive information, the MCP client will prompt you to confirm the operation before proceeding.

> **🛡️ Elicitation (user confirmation) Security Feature:**
>
>
> Elicitation prompts appear when tools may expose sensitive information like:
> - Key Vault secrets
> - Connection strings and passwords
Expand Down Expand Up @@ -1158,7 +1158,7 @@ azmcp sql db delete --subscription <subscription> \
azmcp sql db list --subscription <subscription> \
--resource-group <resource-group> \
--server <server-name>

# Rename an existing SQL database to a new name within the same server
azmcp sql db rename --subscription <subscription> \
--resource-group <resource-group> \
Expand All @@ -1185,6 +1185,18 @@ azmcp sql db update --subscription <subscription> \
[--elastic-pool-name <elastic-pool-name>] \
[--zone-redundant <true/false>] \
[--read-scale <Enabled|Disabled>]

# Export an Azure SQL Database to a BACPAC file in Azure Storage
azmcp sql db export --subscription <subscription> \
--resource-group <resource-group> \
--server <server-name> \
--database <database-name> \
--storage-uri <storage-uri> \
--storage-key <storage-key> \
--storage-key-type <StorageAccessKey|SharedAccessKey> \
--admin-user <admin-user> \
--admin-password <admin-password> \
[--auth-type <SQL|ADPassword|ManagedIdentity>]
```

#### Elastic Pool
Expand Down
2 changes: 2 additions & 0 deletions docs/e2eTestPrompts.md
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,8 @@ This file contains prompts used for end-to-end testing to ensure each tool is in
| azmcp_sql_db_show | Show me the details of SQL database <database_name> in server <server_name> |
| azmcp_sql_db_update | Update the performance tier of SQL database <database_name> on server <server_name> |
| azmcp_sql_db_update | Scale SQL database <database_name> on server <server_name> to use <sku_name> SKU |
| azmcp_sql_db_export | Export SQL database <database_name> from server <server_name> to Azure Storage |
| azmcp_sql_db_export | Create a BACPAC backup of database <database_name> on server <server_name> |

## Azure SQL Elastic Pool Operations

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 @@ -32,6 +32,7 @@ The Azure MCP Server updates automatically by default whenever a new release com

- Added support for listing SQL servers in a subscription and resource group via the command `azmcp_sql_server_list`. [[#503](https://github.com/microsoft/mcp/issues/503)]
- Added support for renaming Azure SQL databases within a server while retaining configuration via the `azmcp sql db rename` command. [[#542](https://github.com/microsoft/mcp/pull/542)]
- Added support for exporting Azure SQL databases to BACPAC files in Azure Storage via the `azmcp sql db export` command. [[#526](https://github.com/microsoft/mcp/pull/526)]
- Added support for Azure App Service database management via the command `azmcp_appservice_database_add`. [[#59](https://github.com/microsoft/mcp/pull/59)]
- Added the following Azure Foundry agents commands: [[#55](https://github.com/microsoft/mcp/pull/55)]
- `azmcp_foundry_agents_connect`: Connect to an agent in an AI Foundry project and query it
Expand Down
227 changes: 87 additions & 140 deletions servers/Azure.Mcp.Server/README.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.CommandLine;
using System.CommandLine.Parsing;
using System.Net;
using Azure.Mcp.Core.Commands;
using Azure.Mcp.Core.Extensions;
using Azure.Mcp.Core.Models.Command;
using Azure.Mcp.Tools.Sql.Commands;
using Azure.Mcp.Tools.Sql.Models;
using Azure.Mcp.Tools.Sql.Options;
using Azure.Mcp.Tools.Sql.Options.Database;
using Azure.Mcp.Tools.Sql.Services;
using Azure.ResourceManager.Sql.Models;
using Microsoft.Extensions.Logging;

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

public sealed class DatabaseExportCommand(ILogger<DatabaseExportCommand> logger)
: BaseDatabaseCommand<DatabaseExportOptions>(logger)
{
private const string CommandTitle = "Export SQL Database";

public override string Name => "export";

public override string Description =>
"""
Export an Azure SQL Database to a BACPAC file in Azure Storage. This command creates a logical backup
of the database schema and data that can be used for archiving or migration purposes. The export
operation is equivalent to 'az sql db export'. Returns export operation information including status.
""";

public override string Title => CommandTitle;

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

protected override void RegisterOptions(Command command)
{
base.RegisterOptions(command);
command.Options.Add(SqlOptionDefinitions.StorageUriOption);
command.Options.Add(SqlOptionDefinitions.StorageKeyOption);
command.Options.Add(SqlOptionDefinitions.StorageKeyTypeOption);
command.Options.Add(SqlOptionDefinitions.AdminUserOption);
command.Options.Add(SqlOptionDefinitions.AdminPasswordOption);
command.Options.Add(SqlOptionDefinitions.AuthTypeOption);
}

protected override DatabaseExportOptions BindOptions(ParseResult parseResult)
{
var options = base.BindOptions(parseResult);
options.StorageUri = parseResult.GetValueOrDefault(SqlOptionDefinitions.StorageUriOption);
options.StorageKey = parseResult.GetValueOrDefault(SqlOptionDefinitions.StorageKeyOption);
options.StorageKeyType = parseResult.GetValueOrDefault(SqlOptionDefinitions.StorageKeyTypeOption);
options.AdminUser = parseResult.GetValueOrDefault(SqlOptionDefinitions.AdminUserOption);
options.AdminPassword = parseResult.GetValueOrDefault(SqlOptionDefinitions.AdminPasswordOption);
options.AuthType = parseResult.GetValueOrDefault(SqlOptionDefinitions.AuthTypeOption);
Copy link
Contributor

Choose a reason for hiding this comment

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

@xiangyan99 pls review

return options;
}

public override async Task<CommandResponse> ExecuteAsync(CommandContext context, ParseResult parseResult)
{
if (!Validate(parseResult.CommandResult, context.Response).IsValid)
{
return context.Response;
}

var options = BindOptions(parseResult);

// Additional validation for export-specific parameters
if (string.IsNullOrEmpty(options.StorageUri))
Copy link
Contributor

Choose a reason for hiding this comment

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

Add to Validators @alzimmermsft

{
context.Response.Status = HttpStatusCode.BadRequest;
context.Response.Message = "Storage URI is required for database export.";
return context.Response;
}

if (string.IsNullOrEmpty(options.StorageKey))
{
context.Response.Status = HttpStatusCode.BadRequest;
context.Response.Message = "Storage key is required for database export.";
return context.Response;
}

if (string.IsNullOrEmpty(options.StorageKeyType))
{
context.Response.Status = HttpStatusCode.BadRequest;
context.Response.Message = "Storage key type is required for database export.";
return context.Response;
}

if (string.IsNullOrEmpty(options.AdminUser))
{
context.Response.Status = HttpStatusCode.BadRequest;
context.Response.Message = "Administrator user is required for database export.";
return context.Response;
}

if (string.IsNullOrEmpty(options.AdminPassword))
{
context.Response.Status = HttpStatusCode.BadRequest;
context.Response.Message = "Administrator password is required for database export.";
return context.Response;
}

var validStorageKeyTypes = new[] { "StorageAccessKey", "SharedAccessKey" };
if (!validStorageKeyTypes.Contains(options.StorageKeyType, StringComparer.OrdinalIgnoreCase))
{
context.Response.Status = HttpStatusCode.BadRequest;
context.Response.Message = $"Invalid storage key type '{options.StorageKeyType}'. Valid values are: {string.Join(", ", validStorageKeyTypes)}";
return context.Response;
}

if (!Uri.TryCreate(options.StorageUri, UriKind.Absolute, out _))
{
context.Response.Status = HttpStatusCode.BadRequest;
context.Response.Message = "Storage URI must be a valid absolute URI.";
return context.Response;
}

if (!string.IsNullOrEmpty(options.AuthType))
{
var validAuthTypes = new[] { "SQL", "ADPassword", "ManagedIdentity" };
if (!validAuthTypes.Contains(options.AuthType, StringComparer.OrdinalIgnoreCase))
{
context.Response.Status = HttpStatusCode.BadRequest;
context.Response.Message = $"Invalid authentication type '{options.AuthType}'. Valid values are: {string.Join(", ", validAuthTypes)}";
return context.Response;
}
}

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

var exportResult = await sqlService.ExportDatabaseAsync(
options.Server!,
options.Database!,
options.ResourceGroup!,
options.Subscription!,
options.StorageUri!,
options.StorageKey!,
options.StorageKeyType!,
options.AdminUser!,
options.AdminPassword!,
options.AuthType,
options.RetryPolicy);

context.Response.Results = ResponseResult.Create(
new DatabaseExportResult(exportResult),
SqlJsonContext.Default.DatabaseExportResult);
}
catch (Exception ex)
{
_logger.LogError(ex,
"Error exporting SQL database. Server: {Server}, Database: {Database}, ResourceGroup: {ResourceGroup}, StorageUri: {StorageUri}",
options.Server, options.Database, options.ResourceGroup, options.StorageUri);
HandleException(context, ex);
}

return context.Response;
}

protected override string GetErrorMessage(Exception ex) => ex switch
{
RequestFailedException reqEx when reqEx.Status == (int)HttpStatusCode.NotFound =>
"SQL database or server not found. Verify the database name, server name, resource group, and that you have access.",
RequestFailedException reqEx when reqEx.Status == (int)HttpStatusCode.Forbidden =>
$"Authorization failed exporting the SQL database. Verify you have appropriate permissions and the storage account is accessible. Details: {reqEx.Message}",
RequestFailedException reqEx when reqEx.Status == (int)HttpStatusCode.BadRequest =>
$"Invalid export parameters. Check your storage URI, credentials, and database configuration. Details: {reqEx.Message}",
ArgumentException argEx =>
$"Invalid argument: {argEx.Message}",
RequestFailedException reqEx => reqEx.Message,
_ => base.GetErrorMessage(ex)
};

internal record DatabaseExportResult(SqlDatabaseExportResult ExportResult);
}
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 @@ -17,6 +17,7 @@ namespace Azure.Mcp.Tools.Sql.Commands;
[JsonSerializable(typeof(DatabaseCreateCommand.DatabaseCreateResult))]
[JsonSerializable(typeof(DatabaseUpdateCommand.DatabaseUpdateResult))]
[JsonSerializable(typeof(DatabaseRenameCommand.DatabaseRenameResult))]
[JsonSerializable(typeof(DatabaseExportCommand.DatabaseExportResult))]
[JsonSerializable(typeof(DatabaseDeleteCommand.DatabaseDeleteResult))]
[JsonSerializable(typeof(EntraAdminListCommand.EntraAdminListResult))]
[JsonSerializable(typeof(FirewallRuleListCommand.FirewallRuleListResult))]
Expand All @@ -42,6 +43,7 @@ namespace Azure.Mcp.Tools.Sql.Commands;
[JsonSerializable(typeof(SqlElasticPoolProperties))]
[JsonSerializable(typeof(SqlElasticPoolPerDatabaseSettings))]
[JsonSerializable(typeof(SqlFirewallRuleData))]
[JsonSerializable(typeof(SqlDatabaseExportResult))]
[JsonSourceGenerationOptions(
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
WriteIndented = true,
Expand Down
18 changes: 18 additions & 0 deletions tools/Azure.Mcp.Tools.Sql/src/Models/SqlDatabaseExportResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Text.Json.Serialization;

namespace Azure.Mcp.Tools.Sql.Models;

public record SqlDatabaseExportResult(
[property: JsonPropertyName("operationId")] string? OperationId,
[property: JsonPropertyName("requestId")] string? RequestId,
[property: JsonPropertyName("status")] string? Status,
[property: JsonPropertyName("queuedTime")] DateTimeOffset? QueuedTime,
[property: JsonPropertyName("lastModifiedTime")] DateTimeOffset? LastModifiedTime,
[property: JsonPropertyName("serverName")] string? ServerName,
[property: JsonPropertyName("databaseName")] string? DatabaseName,
[property: JsonPropertyName("storageUri")] string? StorageUri,
[property: JsonPropertyName("message")] string? Message
Comment on lines +9 to +17
Copy link
Contributor

Choose a reason for hiding this comment

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

seems like we might not want to duplicate the names, can you use SqlOptionDefinitions.

);
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Text.Json.Serialization;

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

public class DatabaseExportOptions : BaseDatabaseOptions
{
[JsonPropertyName(SqlOptionDefinitions.StorageUri)]
public string? StorageUri { get; set; }

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

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

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

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

[JsonPropertyName(SqlOptionDefinitions.AuthType)]
public string? AuthType { get; set; }
}
54 changes: 54 additions & 0 deletions tools/Azure.Mcp.Tools.Sql/src/Options/SqlOptionDefinitions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ public static class SqlOptionDefinitions
public const string ElasticPoolName = "elastic-pool-name";
public const string ZoneRedundant = "zone-redundant";
public const string ReadScale = "read-scale";
public const string StorageUri = "storage-uri";
public const string StorageKey = "storage-key";
public const string StorageKeyType = "storage-key-type";
public const string AdminUser = "admin-user";
public const string AdminPassword = "admin-password";
public const string AuthType = "auth-type";

public static readonly Option<string> Server = new(
$"--{ServerName}"
Expand Down Expand Up @@ -185,4 +191,52 @@ public static class SqlOptionDefinitions
Description = "Read scale option for the database (Enabled or Disabled).",
Required = false
};

public static readonly Option<string> StorageUriOption = new(
$"--{StorageUri}"
)
{
Description = "The storage URI for the BACPAC file (e.g., https://mystorageaccount.blob.core.windows.net/mycontainer/myfile.bacpac).",
Required = true
};

public static readonly Option<string> StorageKeyOption = new(
$"--{StorageKey}"
)
{
Description = "The storage access key or shared access signature for the storage account.",
Required = true
};

public static readonly Option<string> StorageKeyTypeOption = new(
$"--{StorageKeyType}"
)
{
Description = "The storage key type (StorageAccessKey or SharedAccessKey).",
Required = true
};

public static readonly Option<string> AdminUserOption = new(
$"--{AdminUser}"
)
{
Description = "The SQL Server administrator login name for database access.",
Required = true
};

public static readonly Option<string> AdminPasswordOption = new(
$"--{AdminPassword}"
)
{
Description = "The SQL Server administrator password for database access.",
Required = true
};

public static readonly Option<string> AuthTypeOption = new(
$"--{AuthType}"
)
{
Description = "The authentication type (SQL, ADPassword, or ManagedIdentity).",
Required = false
};
}
Loading