From 9061dcb2146c991c2f763f768a673c69a0a8f587 Mon Sep 17 00:00:00 2001 From: Achyuth Maddala Sitaram <66455292+achyuth-ms@users.noreply.github.com> Date: Fri, 19 Sep 2025 17:01:10 -0700 Subject: [PATCH 1/3] changes for sql db export --- docs/azmcp-commands.md | 12 + docs/e2eTestPrompts.md | 2 + servers/Azure.Mcp.Server/README.md | 227 ++++---- .../Database/DatabaseExportCommand.cs | 189 +++++++ .../src/Commands/SqlJsonContext.cs | 1 + .../src/Models/SqlDatabaseExportResult.cs | 18 + .../Options/Database/DatabaseExportOptions.cs | 27 + .../src/Options/SqlOptionDefinitions.cs | 54 ++ .../src/Services/ISqlService.cs | 30 ++ .../src/Services/SqlService.cs | 111 ++++ tools/Azure.Mcp.Tools.Sql/src/SqlSetup.cs | 2 + .../Database/DatabaseExportCommandTests.cs | 491 ++++++++++++++++++ 12 files changed, 1024 insertions(+), 140 deletions(-) create mode 100644 tools/Azure.Mcp.Tools.Sql/src/Commands/Database/DatabaseExportCommand.cs create mode 100644 tools/Azure.Mcp.Tools.Sql/src/Models/SqlDatabaseExportResult.cs create mode 100644 tools/Azure.Mcp.Tools.Sql/src/Options/Database/DatabaseExportOptions.cs create mode 100644 tools/Azure.Mcp.Tools.Sql/tests/Azure.Mcp.Tools.Sql.UnitTests/Database/DatabaseExportCommandTests.cs diff --git a/docs/azmcp-commands.md b/docs/azmcp-commands.md index e5ffb256d..c39769670 100644 --- a/docs/azmcp-commands.md +++ b/docs/azmcp-commands.md @@ -1185,6 +1185,18 @@ azmcp sql db update --subscription \ [--elastic-pool-name ] \ [--zone-redundant ] \ [--read-scale ] + +# Export an Azure SQL Database to a BACPAC file in Azure Storage +azmcp sql db export --subscription \ + --resource-group \ + --server \ + --database \ + --storage-uri \ + --storage-key \ + --storage-key-type \ + --admin-user \ + --admin-password \ + [--auth-type ] ``` #### Elastic Pool diff --git a/docs/e2eTestPrompts.md b/docs/e2eTestPrompts.md index 9ad41f8da..457e5d4dd 100644 --- a/docs/e2eTestPrompts.md +++ b/docs/e2eTestPrompts.md @@ -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 in server | | azmcp_sql_db_update | Update the performance tier of SQL database on server | | azmcp_sql_db_update | Scale SQL database on server to use SKU | +| azmcp_sql_db_export | Export SQL database from server to Azure Storage | +| azmcp_sql_db_export | Create a BACPAC backup of database on server | ## Azure SQL Elastic Pool Operations diff --git a/servers/Azure.Mcp.Server/README.md b/servers/Azure.Mcp.Server/README.md index 14da98e80..e48e5e1b7 100644 --- a/servers/Azure.Mcp.Server/README.md +++ b/servers/Azure.Mcp.Server/README.md @@ -2,45 +2,43 @@ All Azure MCP tools in a single server. The Azure MCP Server implements the [MCP specification](https://modelcontextprotocol.io) to create a seamless connection between AI agents and Azure services. Azure MCP Server can be used alone or with the [GitHub Copilot for Azure extension](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-azure-github-copilot) in VS Code. This project is in Public Preview and implementation may significantly change prior to our General Availability. -[![Install Azure MCP in VS Code](https://img.shields.io/badge/VS_Code-Install_Azure_MCP_Server-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://vscode.dev/redirect?url=vscode:extension/ms-azuretools.vscode-azure-mcp-server) [![Install Azure MCP in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install_Azure_MCP_Server-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://vscode.dev/redirect?url=vscode-insiders:extension/ms-azuretools.vscode-azure-mcp-server) [![Install Azure MCP in Visual Studio](https://img.shields.io/badge/Visual_Studio-Install_Azure_MCP_Server-C16FDE?style=flat-square&logo=visualstudio&logoColor=white)](https://marketplace.visualstudio.com/items?itemName=github-copilot-azure.GitHubCopilotForAzure2022) [![Install Azure MCP Server](https://img.shields.io/badge/IntelliJ%20IDEA-Install%20Azure%20MCP%20Server-1495b1?style=flat-square&logo=intellijidea&logoColor=white)](https://plugins.jetbrains.com/plugin/8053) +[![Install Azure MCP in VS Code](https://img.shields.io/badge/VS_Code-Install_Azure_MCP_Server-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://vscode.dev/redirect?url=vscode:extension/ms-azuretools.vscode-azure-mcp-server) [![Install Azure MCP in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install_Azure_MCP_Server-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://vscode.dev/redirect?url=vscode-insiders:extension/ms-azuretools.vscode-azure-mcp-server) [![Install Azure MCP in Visual Studio](https://img.shields.io/badge/Visual_Studio-Install_Azure_MCP_Server-C16FDE?style=flat-square&logo=visualstudio&logoColor=white)](https://marketplace.visualstudio.com/items?itemName=github-copilot-azure.GitHubCopilotForAzure2022) ## Table of Contents - [Overview](#overview) - [Installation](#installation) - - [IDE](#ide) + - [IDE Extensions](#ide-extensions) - [VS Code (Recommended)](#vs-code-recommended) - [Visual Studio 2022](#visual-studio-2022) - [IntelliJ IDEA](#intellij-idea) - - [Manual Setup](#manual-setup) - - [Package Manager](#package-manager) + - [Package Managers](#package-managers) - [NuGet](#nuget) - [NPM](#npm) - [Docker](#docker) + - [Custom Clients](#custom-clients) - [Usage](#usage) - [Getting Started](#getting-started) - [What can you do with the Azure MCP Server?](#what-can-you-do-with-the-azure-mcp-server) - [Complete List of Supported Azure Services](#complete-list-of-supported-azure-services) -- [Support and Reference](#support-and-reference) +- [Support & Reference](#support-and-reference) - [Documentation](#documentation) - - [Feedback and Support](#feedback-and-support) + - [Feedback & Support](#feedback-and-support) - [Security](#security) - [Data Collection](#data-collection) - - [Contributing and Code of Conduct](#contributing) + - [Contributing & Code of Conduct](#contributing) -# Overview +# Overview **Azure MCP Server** supercharges your agents with Azure context across **30+ different Azure services**. -# Installation +# Installation -Install Azure MCP Server using either an IDE extension or package manager. Choose one method below. +## 🧩 IDE Extensions -## IDE +Follow these simple steps to start using Azure MCP with your favorite IDE. We recommend VS Code: -Start using Azure MCP with your favorite IDE. We recommend VS Code: - -### VS Code (Recommended) +### 🔷 VS Code (Recommended) 1. Install either the stable or Insiders release of VS Code: * [💫 Stable release](https://code.visualstudio.com/download) @@ -48,7 +46,7 @@ Start using Azure MCP with your favorite IDE. We recommend VS Code: 1. Install the [GitHub Copilot](https://marketplace.visualstudio.com/items?itemName=GitHub.copilot) and [GitHub Copilot Chat](https://marketplace.visualstudio.com/items?itemName=GitHub.copilot-chat) extensions 1. Install the [Azure MCP Server](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-azure-mcp-server) extension -### Visual Studio 2022 +### 💜 Visual Studio 2022 From within Visual Studio 2022 install [GitHub Copilot for Azure (VS 2022)](https://marketplace.visualstudio.com/items?itemName=github-copilot-azure.GitHubCopilotForAzure2022): 1. Go to `Extensions | Manage Extensions...` @@ -56,124 +54,95 @@ From within Visual Studio 2022 install [GitHub Copilot for Azure (VS 2022)](http 3. Search for `Github Copilot for Azure` 4. Click `Install` -### IntelliJ IDEA +### ☕ IntelliJ IDEA 1. Install either the [IntelliJ IDEA Ultimate](https://www.jetbrains.com/idea/download) or [IntelliJ IDEA Community](https://www.jetbrains.com/idea/download) edition. 1. Install the [GitHub Copilot](https://plugins.jetbrains.com/plugin/17718-github-copilot) plugin. 1. Install the [Azure Toolkit for Intellij](https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij) plugin. -### Manual Setup -Azure MCP Server can also be configured across other IDEs, CLIs, and MCP clients: - -
-Manual setup instructions - -#### Sample Configuration - -Copy this configuration to your client's MCP configuration file: -```json -{ - "mcpServers": { - "azure-mcp-server": { - "command": "npx", - "args": [ - "-y", - "@azure/mcp@latest", - "server", - "start" - ] - } - } -} -``` -**Note:** When manually configuring Visual Studio and Visual Studio Code, use `servers` instead of `mcpServers` as the root object. - -**Client-Specific Configuration** -| IDE | File Location | Documentation Link | -|-----|---------------|-------------------| -| **Amazon Q Developer** | `~/.aws/amazonq/mcp.json` (global)
`.amazonq/mcp.json` (workspace) | [AWS Q Developer MCP Guide](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/qdev-mcp.html) | -| **Claude Code** | `~/.claude.json` or `.mcp.json` (project) | [Claude Code MCP Configuration](https://scottspence.com/posts/configuring-mcp-tools-in-claude-code) | -| **Claude Desktop** | `~/.claude/claude_desktop_config.json` (macOS)
`%APPDATA%\Claude\claude_desktop_config.json` (Windows) | [Claude Desktop MCP Setup](https://support.claude.com/en/articles/10949351-getting-started-with-local-mcp-servers-on-claude-desktop) | -| **Cursor** | `~/.cursor/mcp.json` or `.cursor/mcp.json` | [Cursor MCP Documentation](https://docs.cursor.com/context/model-context-protocol) | -| **IntelliJ IDEA** | Built-in MCP server (2025.2+)
Settings > Tools > MCP Server | [IntelliJ MCP Documentation](https://www.jetbrains.com/help/ai-assistant/mcp.html) | -| **Visual Studio** | `.mcp.json` (solution/workspace) | [Visual Studio MCP Setup](https://learn.microsoft.com/visualstudio/ide/mcp-servers?view=vs-2022) | -| **VS Code** | `.vscode/mcp.json` (workspace)
`settings.json` (user) | [VS Code MCP Documentation](https://code.visualstudio.com/docs/copilot/chat/mcp-servers) | -| **Windsurf** | `~/.codeium/windsurf/mcp_config.json` | [Windsurf Cascade MCP Integration](https://docs.windsurf.com/windsurf/cascade/mcp) | -
+## Package Managers +### 🤖 NuGet -## Package Manager -Package manager installation offers several advantages over IDE-specific setup, including centralized dependency management, CI/CD integration, support for headless/server environments, version control, and project portability. +Microsoft publishes an official Azure MCP Server .NET Tool on NuGet: [Azure.Mcp](https://www.nuget.org/packages/Azure.Mcp). -Install Azure MCP Server via a package manager: +### 📦 NPM -### NuGet +Microsoft publishes an official Azure MCP Server npm package for Node.js: [@azure/mcp](https://www.npmjs.com/package/@azure/mcp). -Install the .NET Tool: [Azure.Mcp](https://www.nuget.org/packages/Azure.Mcp). +### 🐋 Docker -```bash -dotnet tool install --global Azure.Mcp -``` +Microsoft publishes an official Azure MCP Server Docker container on the [Microsoft Artifact Registry](https://mcr.microsoft.com/artifact/mar/azure-sdk/azure-mcp). -### NPM +
+For a step-by-step Docker installation, follow these instructions: + +1. Create an `.env` file with environment variables that [match one of the `EnvironmentCredential`](https://learn.microsoft.com/dotnet/api/azure.identity.environmentcredential) sets. For example, a `.env` file using a service principal could look like: + + ```bash + AZURE_TENANT_ID={YOUR_AZURE_TENANT_ID} + AZURE_CLIENT_ID={YOUR_AZURE_CLIENT_ID} + AZURE_CLIENT_SECRET={YOUR_AZURE_CLIENT_SECRET} + ``` + +2. Add `.vscode/mcp.json` or update existing MCP configuration. Replace `/full/path/to/.env` with a path to your `.env` file. + + ```json + { + "servers": { + "Azure MCP Server": { + "command": "docker", + "args": [ + "run", + "-i", + "--rm", + "--env-file", + "/full/path/to/.env", + "mcr.microsoft.com/azure-sdk/azure-mcp:latest", + ] + } + } + } + ``` -Install the Node.js package: [@azure/mcp](https://www.npmjs.com/package/@azure/mcp). +Optionally, use `--env` or `--volume` to pass authentication values. +
-```bash -npm install -g @azure/mcp -``` +## 🤖 Custom Clients -### Docker +You can easily configure your MCP client to use the Azure MCP Server. -Pull the Docker image: [mcr.microsoft.com/azure-sdk/azure-mcp](https://mcr.microsoft.com/artifact/mar/azure-sdk/azure-mcp). +
+Have your client run the following command and access it via standard IO: ```bash -docker pull mcr.microsoft.com/azure-sdk/azure-mcp +npx -y @azure/mcp@latest server start ``` -
-Docker instructions - -#### Create an env file with Azure credentials +For example, add the following `mcp.json` to VS Code. Other clients will look similar, but may be structured slightly different. Consult the documentation of the custom client for details. -1. Create a `.env` file with Azure credentials ([see EnvironmentCredential options](https://learn.microsoft.com/dotnet/api/azure.identity.environmentcredential)): - -```bash -AZURE_TENANT_ID={YOUR_AZURE_TENANT_ID} -AZURE_CLIENT_ID={YOUR_AZURE_CLIENT_ID} -AZURE_CLIENT_SECRET={YOUR_AZURE_CLIENT_SECRET} -``` +1. Example `mcp.json`: -#### Configure your MCP client to use Docker: - -2. Add or update existing `mcp.json`. - - Replace `/full/path/to/your.env` with the actual `.env` file path. - - Optionally, use `--env` or `--volume` to pass authentication values. - - **Note:** When manually configuring Visual Studio and Visual Studio Code, use `servers` instead of `mcpServers` as the root object. - -```json - { - "mcpServers": { - "Azure MCP Server": { - "command": "docker", - "args": [ - "run", - "-i", - "--rm", - "--env-file", - "/full/path/to/your.env", - "mcr.microsoft.com/azure-sdk/azure-mcp:latest" - ] - } + ```json + { + "servers": { + "Azure MCP Server": { + "command": "npx", + "args": [ + "-y", + "@azure/mcp@latest", + "server", + "start" + ] + } } - } -``` - + } + ```
-# Usage +# Usage -## Getting Started +## 🚀 Getting Started 1. Open GitHub Copilot in [VS Code](https://code.visualstudio.com/docs/copilot/chat/chat-agent-mode) or [IntelliJ](https://github.blog/changelog/2025-05-19-agent-mode-and-mcp-support-for-copilot-in-jetbrains-eclipse-and-xcode-now-in-public-preview/#agent-mode) and switch to Agent mode. 1. Click `refresh` on the tools list @@ -184,9 +153,9 @@ AZURE_CLIENT_SECRET={YOUR_AZURE_CLIENT_SECRET} 1. We're building this in the open. Your feedback is much appreciated, and will help us shape the future of the Azure MCP server - 👉 [Open an issue in the public repository](https://github.com/microsoft/mcp/issues/new/choose) -## What can you do with the Azure MCP Server? +## ✨ What can you do with the Azure MCP Server? -✨ The Azure MCP Server supercharges your agents with Azure context. Here are some cool prompts you can try: +The Azure MCP Server supercharges your agents with Azure context. Here are some cool prompts you can try: ### 🧮 Azure AI Foundry @@ -195,19 +164,12 @@ AZURE_CLIENT_SECRET={YOUR_AZURE_CLIENT_SECRET} * List foundry model deployments * List knowledge indexes * Get knowledge index schema configuration - + ### 🔎 Azure AI Search * "What indexes do I have in my Azure AI Search service 'mysvc'?" * "Let's search this index for 'my search query'" -### 🎤 Azure AI Services Speech - -* "Convert this audio file to text using Azure Speech Services" -* "Recognize speech from my audio file with language detection" -* "Transcribe speech from audio with profanity filtering" -* "Transcribe audio with phrase hints for better accuracy" - ### ⚙️ Azure App Configuration * "List my App Configuration stores" @@ -217,18 +179,6 @@ AZURE_CLIENT_SECRET={YOUR_AZURE_CLIENT_SECRET} * "Help me diagnose issues with my app" -### 🕸️ Azure App Service - -* "List the websites in my subscription" -* "Show me the websites in my 'my-resource-group' resource group" -* "Get the details for website 'my-website'" -* "Get the details for app service plan 'my-app-service-plan'" - -### 📦 Azure Container Apps - -* "List the container apps in my subscription" -* "Show me the container apps in my 'my-resource-group' resource group" - ### 📦 Azure Container Registry (ACR) * "List all my Azure Container Registries" @@ -315,17 +265,14 @@ AZURE_CLIENT_SECRET={YOUR_AZURE_CLIENT_SECRET} * "Upload my file to the blob container" -## Complete List of Supported Azure Services +## 🛠️ Complete List of Supported Azure Services The Azure MCP Server provides tools for interacting with **30+ Azure service areas**: - 🧮 **Azure AI Foundry** - AI model management, AI model deployment, and knowledge index management - 🔎 **Azure AI Search** - Search engine/vector database operations -- 🎤 **Azure AI Services Speech** - Speech-to-text recognition - ⚙️ **Azure App Configuration** - Configuration management -- 🕸️ **Azure App Service** - Web app hosting - 🛡️ **Azure Best Practices** - Secure, production-grade guidance -- 📦 **Azure Container Apps** - Container hosting - 📦 **Azure Container Registry (ACR)** - Container registry management - 📊 **Azure Cosmos DB** - NoSQL database operations - 🧮 **Azure Data Explorer** - Analytics queries and KQL @@ -359,26 +306,26 @@ The Azure MCP Server provides tools for interacting with **30+ Azure service are - 🏗️ **Bicep** - Azure resource templates - 🏗️ **Cloud Architect** - Guided architecture design -# Support and Reference +# Support & Reference -## Documentation +## Documentation - See our [official documentation on learn.microsoft.com](https://learn.microsoft.com/azure/developer/azure-mcp-server/) to learn how to use the Azure MCP Server to interact with Azure resources through natural language commands from AI agents and other types of clients. - For additional command documentation and examples, see [Azure MCP Commands](https://github.com/microsoft/mcp/blob/main/docs/azmcp-commands.md). -## Feedback and Support +## Feedback & Support - Check the [Troubleshooting guide](https://aka.ms/azmcp/troubleshooting) to diagnose and resolve common issues with the Azure MCP Server. - We're building this in the open. Your feedback is much appreciated, and will help us shape the future of the Azure MCP server. - 👉 [Open an issue](https://github.com/microsoft/mcp/issues) in the public GitHub repository — we’d love to hear from you! -## Security +## 🛡️ Security Your credentials are always handled securely through the official [Azure Identity SDK](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/identity/Azure.Identity/README.md) - **we never store or manage tokens directly**. MCP as a phenomenon is very novel and cutting-edge. As with all new technology standards, consider doing a security review to ensure any systems that integrate with MCP servers follow all regulations and standards your system is expected to adhere to. This includes not only the Azure MCP Server, but any MCP client/agent that you choose to implement down to the model provider. -## Data Collection +## Data Collection The software may collect information about you and your use of the software and send it to Microsoft. Microsoft may use this information to provide services and improve our products and services. You may turn off the telemetry as described in the repository. There are also some features in the software that may enable you and Microsoft to collect data from users of your applications. If you use these features, you must comply with applicable law, including providing appropriate notices to users of your applications together with a copy of Microsoft's [privacy statement](https://www.microsoft.com/privacy/privacystatement). You can learn more about data collection and use in the help documentation and our privacy statement. Your use of the software operates as your consent to these practices. @@ -390,7 +337,7 @@ To opt out, set the environment variable `AZURE_MCP_COLLECT_TELEMETRY` to `false -## Contributing +## 👥 Contributing We welcome contributions to the Azure MCP Server! Whether you're fixing bugs, adding new features, or improving documentation, your contributions are welcome. @@ -402,7 +349,7 @@ Please read our [Contributing Guide](https://github.com/microsoft/mcp/blob/main/ * 🔄 Making pull requests -## Code of Conduct +## 🤝 Code of Conduct This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). diff --git a/tools/Azure.Mcp.Tools.Sql/src/Commands/Database/DatabaseExportCommand.cs b/tools/Azure.Mcp.Tools.Sql/src/Commands/Database/DatabaseExportCommand.cs new file mode 100644 index 000000000..33330573a --- /dev/null +++ b/tools/Azure.Mcp.Tools.Sql/src/Commands/Database/DatabaseExportCommand.cs @@ -0,0 +1,189 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.CommandLine; +using System.CommandLine.Parsing; +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 logger) + : BaseDatabaseCommand(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 = true, + 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); + return options; + } + + public override async Task 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)) + { + context.Response.Status = 400; + context.Response.Message = "Storage URI is required for database export."; + return context.Response; + } + + if (string.IsNullOrEmpty(options.StorageKey)) + { + context.Response.Status = 400; + context.Response.Message = "Storage key is required for database export."; + return context.Response; + } + + if (string.IsNullOrEmpty(options.StorageKeyType)) + { + context.Response.Status = 400; + context.Response.Message = "Storage key type is required for database export."; + return context.Response; + } + + if (string.IsNullOrEmpty(options.AdminUser)) + { + context.Response.Status = 400; + context.Response.Message = "Administrator user is required for database export."; + return context.Response; + } + + if (string.IsNullOrEmpty(options.AdminPassword)) + { + context.Response.Status = 400; + context.Response.Message = "Administrator password is required for database export."; + return context.Response; + } + + // Validate storage key type + var validStorageKeyTypes = new[] { "StorageAccessKey", "SharedAccessKey", "ManagedIdentity" }; + if (!validStorageKeyTypes.Contains(options.StorageKeyType, StringComparer.OrdinalIgnoreCase)) + { + context.Response.Status = 400; + context.Response.Message = $"Invalid storage key type '{options.StorageKeyType}'. Valid values are: {string.Join(", ", validStorageKeyTypes)}"; + return context.Response; + } + + // Validate storage URI format + if (!Uri.TryCreate(options.StorageUri, UriKind.Absolute, out _)) + { + context.Response.Status = 400; + context.Response.Message = "Storage URI must be a valid absolute URI."; + return context.Response; + } + + // Validate authentication type if provided + if (!string.IsNullOrEmpty(options.AuthType)) + { + var validAuthTypes = new[] { "SQL", "ADPassword", "ManagedIdentity" }; + if (!validAuthTypes.Contains(options.AuthType, StringComparer.OrdinalIgnoreCase)) + { + context.Response.Status = 400; + context.Response.Message = $"Invalid authentication type '{options.AuthType}'. Valid values are: {string.Join(", ", validAuthTypes)}"; + return context.Response; + } + } + + try + { + var sqlService = context.GetService(); + + 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 == 404 => + "SQL database or server not found. Verify the database name, server name, resource group, and that you have access.", + RequestFailedException reqEx when reqEx.Status == 403 => + $"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 == 400 => + $"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); +} diff --git a/tools/Azure.Mcp.Tools.Sql/src/Commands/SqlJsonContext.cs b/tools/Azure.Mcp.Tools.Sql/src/Commands/SqlJsonContext.cs index db75719e0..d40089630 100644 --- a/tools/Azure.Mcp.Tools.Sql/src/Commands/SqlJsonContext.cs +++ b/tools/Azure.Mcp.Tools.Sql/src/Commands/SqlJsonContext.cs @@ -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))] diff --git a/tools/Azure.Mcp.Tools.Sql/src/Models/SqlDatabaseExportResult.cs b/tools/Azure.Mcp.Tools.Sql/src/Models/SqlDatabaseExportResult.cs new file mode 100644 index 000000000..4536c492c --- /dev/null +++ b/tools/Azure.Mcp.Tools.Sql/src/Models/SqlDatabaseExportResult.cs @@ -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 +); \ No newline at end of file diff --git a/tools/Azure.Mcp.Tools.Sql/src/Options/Database/DatabaseExportOptions.cs b/tools/Azure.Mcp.Tools.Sql/src/Options/Database/DatabaseExportOptions.cs new file mode 100644 index 000000000..2b9feb241 --- /dev/null +++ b/tools/Azure.Mcp.Tools.Sql/src/Options/Database/DatabaseExportOptions.cs @@ -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; } +} \ No newline at end of file diff --git a/tools/Azure.Mcp.Tools.Sql/src/Options/SqlOptionDefinitions.cs b/tools/Azure.Mcp.Tools.Sql/src/Options/SqlOptionDefinitions.cs index ea9e50ee5..d681a2c88 100644 --- a/tools/Azure.Mcp.Tools.Sql/src/Options/SqlOptionDefinitions.cs +++ b/tools/Azure.Mcp.Tools.Sql/src/Options/SqlOptionDefinitions.cs @@ -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 Server = new( $"--{ServerName}" @@ -185,4 +191,52 @@ public static class SqlOptionDefinitions Description = "Read scale option for the database (Enabled or Disabled).", Required = false }; + + public static readonly Option 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 StorageKeyOption = new( + $"--{StorageKey}" + ) + { + Description = "The storage access key or shared access signature for the storage account.", + Required = true + }; + + public static readonly Option StorageKeyTypeOption = new( + $"--{StorageKeyType}" + ) + { + Description = "The storage key type (StorageAccessKey, SharedAccessKey, or ManagedIdentity).", + Required = true + }; + + public static readonly Option AdminUserOption = new( + $"--{AdminUser}" + ) + { + Description = "The SQL Server administrator login name for database access.", + Required = true + }; + + public static readonly Option AdminPasswordOption = new( + $"--{AdminPassword}" + ) + { + Description = "The SQL Server administrator password for database access.", + Required = true + }; + + public static readonly Option AuthTypeOption = new( + $"--{AuthType}" + ) + { + Description = "The authentication type (SQL, ADPassword, or ManagedIdentity).", + Required = false + }; } diff --git a/tools/Azure.Mcp.Tools.Sql/src/Services/ISqlService.cs b/tools/Azure.Mcp.Tools.Sql/src/Services/ISqlService.cs index ac8cd3cec..d5fc18ff7 100644 --- a/tools/Azure.Mcp.Tools.Sql/src/Services/ISqlService.cs +++ b/tools/Azure.Mcp.Tools.Sql/src/Services/ISqlService.cs @@ -115,6 +115,36 @@ Task RenameDatabaseAsync( RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + /// + /// Exports an Azure SQL Database to a BACPAC file in Azure Storage. + /// + /// The name of the SQL server + /// The name of the database to export + /// The resource group name + /// The subscription ID or name + /// The storage URI for the BACPAC file + /// The storage access key or shared access signature + /// The storage key type (StorageAccessKey, SharedAccessKey, or ManagedIdentity) + /// The SQL Server administrator login name + /// The SQL Server administrator password + /// Optional authentication type (SQL, ADPassword, or ManagedIdentity) + /// Optional retry policy options + /// Cancellation token + /// The export operation information + Task ExportDatabaseAsync( + string serverName, + string databaseName, + string resourceGroup, + string subscription, + string storageUri, + string storageKey, + string storageKeyType, + string adminUser, + string adminPassword, + string? authType = null, + RetryPolicyOptions? retryPolicy = null, + CancellationToken cancellationToken = default); + /// /// Gets a list of databases for a SQL server. /// diff --git a/tools/Azure.Mcp.Tools.Sql/src/Services/SqlService.cs b/tools/Azure.Mcp.Tools.Sql/src/Services/SqlService.cs index 60f16f34c..bfce522d7 100644 --- a/tools/Azure.Mcp.Tools.Sql/src/Services/SqlService.cs +++ b/tools/Azure.Mcp.Tools.Sql/src/Services/SqlService.cs @@ -351,6 +351,117 @@ public async Task RenameDatabaseAsync( throw; } } + /// Exports an Azure SQL Database to a BACPAC file in Azure Storage. + /// + /// The name of the SQL server hosting the database + /// The name of the database to export + /// The name of the resource group containing the server + /// The subscription ID or name + /// The storage URI for the BACPAC file + /// The storage access key or shared access signature + /// The storage key type (StorageAccessKey, SharedAccessKey, or ManagedIdentity) + /// The SQL Server administrator login name + /// The SQL Server administrator password + /// Optional authentication type (SQL, ADPassword, or ManagedIdentity) + /// Optional retry policy configuration for resilient operations + /// Token to observe for cancellation requests + /// The export operation information + /// Thrown when required parameters are null or empty + public async Task ExportDatabaseAsync( + string serverName, + string databaseName, + string resourceGroup, + string subscription, + string storageUri, + string storageKey, + string storageKeyType, + string adminUser, + string adminPassword, + string? authType = null, + RetryPolicyOptions? retryPolicy = null, + CancellationToken cancellationToken = default) + { + ValidateRequiredParameters(serverName, resourceGroup, subscription, databaseName); + + if (string.IsNullOrEmpty(storageUri)) + throw new ArgumentException("Storage URI cannot be null or empty", nameof(storageUri)); + if (string.IsNullOrEmpty(storageKey)) + throw new ArgumentException("Storage key cannot be null or empty", nameof(storageKey)); + if (string.IsNullOrEmpty(storageKeyType)) + throw new ArgumentException("Storage key type cannot be null or empty", nameof(storageKeyType)); + if (string.IsNullOrEmpty(adminUser)) + throw new ArgumentException("Admin user cannot be null or empty", nameof(adminUser)); + if (string.IsNullOrEmpty(adminPassword)) + throw new ArgumentException("Admin password cannot be null or empty", nameof(adminPassword)); + + try + { + var armClient = await CreateArmClientAsync(null, retryPolicy); + var subscriptionResource = armClient.GetSubscriptionResource(Azure.ResourceManager.Resources.SubscriptionResource.CreateResourceIdentifier(subscription)); + var resourceGroupResource = await subscriptionResource.GetResourceGroupAsync(resourceGroup); + var sqlServerResource = await resourceGroupResource.Value.GetSqlServers().GetAsync(serverName); + var databaseResource = await sqlServerResource.Value.GetSqlDatabases().GetAsync(databaseName); + + // Parse storage key type + if (!Enum.TryParse(storageKeyType, true, out var storageKeyTypeEnum)) + { + throw new ArgumentException($"Invalid storage key type: {storageKeyType}. Valid values are: StorageAccessKey, SharedAccessKey, ManagedIdentity"); + } + + var exportDefinition = new ResourceManager.Sql.Models.DatabaseExportDefinition( + storageKeyTypeEnum, + storageKey, + new Uri(storageUri), + adminUser) + { + AdministratorLoginPassword = adminPassword + }; + + var operation = await databaseResource.Value.ExportAsync( + Azure.WaitUntil.Started, + exportDefinition, + cancellationToken); + + var result = operation.Value; + + _logger.LogInformation( + "Successfully started SQL database export. Server: {Server}, Database: {Database}, ResourceGroup: {ResourceGroup}, OperationId: {OperationId}", + serverName, databaseName, resourceGroup, result?.Id?.ToString()); + + // Convert string times to DateTimeOffset if possible + DateTimeOffset? queuedTime = null; + DateTimeOffset? lastModifiedTime = null; + + if (!string.IsNullOrEmpty(result?.QueuedTime) && DateTimeOffset.TryParse(result.QueuedTime, out var queuedDateTime)) + { + queuedTime = queuedDateTime; + } + + if (!string.IsNullOrEmpty(result?.LastModifiedTime) && DateTimeOffset.TryParse(result.LastModifiedTime, out var lastModifiedDateTime)) + { + lastModifiedTime = lastModifiedDateTime; + } + + return new SqlDatabaseExportResult( + result?.Id?.ToString(), + result?.RequestId?.ToString(), + result?.Status, + queuedTime, + lastModifiedTime, + serverName, + databaseName, + storageUri, + result?.ErrorMessage + ); + } + catch (Exception ex) + { + _logger.LogError(ex, + "Error exporting SQL database. Server: {Server}, Database: {Database}, ResourceGroup: {ResourceGroup}, StorageUri: {StorageUri}", + serverName, databaseName, resourceGroup, storageUri); + throw; + } + } /// /// Retrieves a list of all SQL databases from an Azure SQL Server. diff --git a/tools/Azure.Mcp.Tools.Sql/src/SqlSetup.cs b/tools/Azure.Mcp.Tools.Sql/src/SqlSetup.cs index c3e0656ba..09e4fa8e1 100644 --- a/tools/Azure.Mcp.Tools.Sql/src/SqlSetup.cs +++ b/tools/Azure.Mcp.Tools.Sql/src/SqlSetup.cs @@ -57,6 +57,8 @@ public CommandGroup RegisterCommands(IServiceProvider serviceProvider) database.AddCommand(databaseCreate.Name, databaseCreate); var databaseRename = serviceProvider.GetRequiredService(); database.AddCommand(databaseRename.Name, databaseRename); + var databaseExport = serviceProvider.GetRequiredService(); + database.AddCommand(databaseExport.Name, databaseExport); var databaseUpdate = serviceProvider.GetRequiredService(); database.AddCommand(databaseUpdate.Name, databaseUpdate); var databaseDelete = serviceProvider.GetRequiredService(); diff --git a/tools/Azure.Mcp.Tools.Sql/tests/Azure.Mcp.Tools.Sql.UnitTests/Database/DatabaseExportCommandTests.cs b/tools/Azure.Mcp.Tools.Sql/tests/Azure.Mcp.Tools.Sql.UnitTests/Database/DatabaseExportCommandTests.cs new file mode 100644 index 000000000..0f8412abf --- /dev/null +++ b/tools/Azure.Mcp.Tools.Sql/tests/Azure.Mcp.Tools.Sql.UnitTests/Database/DatabaseExportCommandTests.cs @@ -0,0 +1,491 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.CommandLine; +using Azure.Mcp.Core.Models.Command; +using Azure.Mcp.Core.Options; +using Azure.Mcp.Tools.Sql.Commands.Database; +using Azure.Mcp.Tools.Sql.Models; +using Azure.Mcp.Tools.Sql.Services; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Xunit; + +namespace Azure.Mcp.Tools.Sql.UnitTests.Database; + +public class DatabaseExportCommandTests +{ + private readonly IServiceProvider _serviceProvider; + private readonly ISqlService _sqlService; + private readonly ILogger _logger; + private readonly DatabaseExportCommand _command; + private readonly CommandContext _context; + private readonly Command _commandDefinition; + + public DatabaseExportCommandTests() + { + _sqlService = Substitute.For(); + _logger = Substitute.For>(); + + var collection = new ServiceCollection(); + collection.AddSingleton(_sqlService); + _serviceProvider = collection.BuildServiceProvider(); + + _command = new(_logger); + _context = new(_serviceProvider); + _commandDefinition = _command.GetCommand(); + } + + [Fact] + public void Constructor_InitializesCommandCorrectly() + { + var command = _command.GetCommand(); + Assert.Equal("export", command.Name); + Assert.NotNull(command.Description); + Assert.NotEmpty(command.Description); + Assert.Contains("Export an Azure SQL Database to a BACPAC file", command.Description); + } + + [Fact] + public async Task ExecuteAsync_WithValidParameters_ReturnsSuccessResult() + { + // Arrange + var mockResult = new SqlDatabaseExportResult( + OperationId: "operation-123", + RequestId: "request-456", + Status: "InProgress", + QueuedTime: DateTimeOffset.UtcNow, + LastModifiedTime: DateTimeOffset.UtcNow, + ServerName: "test-server", + DatabaseName: "test-db", + StorageUri: "https://storage.blob.core.windows.net/container/export.bacpac", + Message: null + ); + + _sqlService.ExportDatabaseAsync( + Arg.Is("test-server"), + Arg.Is("test-db"), + Arg.Is("test-rg"), + Arg.Is("test-subscription"), + Arg.Is("https://storage.blob.core.windows.net/container/export.bacpac"), + Arg.Is("storagekey123"), + Arg.Is("StorageAccessKey"), + Arg.Is("admin"), + Arg.Is("password123"), + Arg.Is("SQL"), + Arg.Any(), + Arg.Any()) + .Returns(mockResult); + + var args = _commandDefinition.Parse([ + "--subscription", "test-subscription", + "--resource-group", "test-rg", + "--server", "test-server", + "--database", "test-db", + "--storage-uri", "https://storage.blob.core.windows.net/container/export.bacpac", + "--storage-key", "storagekey123", + "--storage-key-type", "StorageAccessKey", + "--admin-user", "admin", + "--admin-password", "password123", + "--auth-type", "SQL" + ]); + + // Act + var response = await _command.ExecuteAsync(_context, args); + + // Assert + Assert.NotNull(response); + // Debug: Add detailed error info + if (response.Status != 200) + { + throw new Exception($"Expected 200 but got {response.Status}. Message: {response.Message}"); + } + Assert.Equal(200, response.Status); + Assert.NotNull(response.Results); + Assert.Equal("Success", response.Message); + } + + [Fact] + public async Task ExecuteAsync_WithServiceException_ReturnsErrorResult() + { + // Arrange + _sqlService.ExportDatabaseAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .ThrowsAsync(new Exception("Database not found")); + + var args = _commandDefinition.Parse([ + "--subscription", "test-subscription", + "--resource-group", "test-rg", + "--server", "test-server", + "--database", "test-db", + "--storage-uri", "https://storage.blob.core.windows.net/container/export.bacpac", + "--storage-key", "storagekey123", + "--storage-key-type", "StorageAccessKey", + "--admin-user", "admin", + "--admin-password", "password123", + "--auth-type", "SQL" + ]); + + // Act + var response = await _command.ExecuteAsync(_context, args); + + // Assert + Assert.Equal(500, response.Status); + Assert.Contains("Database not found", response.Message); + } + + [Fact] + public async Task ExecuteAsync_WithMissingSubscription_ReturnsValidationError() + { + // Arrange + var args = _commandDefinition.Parse([ + "--resource-group", "test-rg", + "--server", "test-server", + "--database", "test-db", + "--storage-uri", "https://storage.blob.core.windows.net/container/export.bacpac", + "--storage-key", "storagekey123", + "--storage-key-type", "StorageAccessKey", + "--admin-user", "admin", + "--admin-password", "password123", + "--auth-type", "SQL" + ]); + + // Act + var response = await _command.ExecuteAsync(_context, args); + + // Assert + Assert.Equal(400, response.Status); + Assert.Contains("subscription", response.Message, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public async Task ExecuteAsync_WithMissingStorageUri_ReturnsValidationError() + { + // Arrange + var args = _commandDefinition.Parse([ + "--subscription", "test-subscription", + "--resource-group", "test-rg", + "--server", "test-server", + "--database", "test-db", + "--storage-key", "storagekey123", + "--storage-key-type", "StorageAccessKey", + "--admin-user", "admin", + "--admin-password", "password123", + "--auth-type", "SQL" + ]); + + // Act + var response = await _command.ExecuteAsync(_context, args); + + // Assert + Assert.Equal(400, response.Status); + Assert.Contains("Missing Required options: --storage-uri", response.Message); + } + + [Fact] + public async Task ExecuteAsync_WithInvalidStorageUri_ReturnsValidationError() + { + // Arrange + var args = _commandDefinition.Parse([ + "--subscription", "test-subscription", + "--resource-group", "test-rg", + "--server", "test-server", + "--database", "test-db", + "--storage-uri", "invalid-uri", + "--storage-key", "storagekey123", + "--storage-key-type", "StorageAccessKey", + "--admin-user", "admin", + "--admin-password", "password123", + "--auth-type", "SQL" + ]); + + // Act + var response = await _command.ExecuteAsync(_context, args); + + // Assert + Assert.Equal(400, response.Status); + Assert.Contains("Storage URI must be a valid absolute URI", response.Message); + } + + [Fact] + public async Task ExecuteAsync_WithInvalidStorageKeyType_ReturnsValidationError() + { + // Arrange + var args = _commandDefinition.Parse([ + "--subscription", "test-subscription", + "--resource-group", "test-rg", + "--server", "test-server", + "--database", "test-db", + "--storage-uri", "https://storage.blob.core.windows.net/container/export.bacpac", + "--storage-key", "storagekey123", + "--storage-key-type", "InvalidType", + "--admin-user", "admin", + "--admin-password", "password123", + "--auth-type", "SQL" + ]); + + // Act + var response = await _command.ExecuteAsync(_context, args); + + // Assert + Assert.Equal(400, response.Status); + Assert.Contains("Invalid storage key type", response.Message); + } + + [Fact] + public async Task ExecuteAsync_WithInvalidAuthenticationType_ReturnsValidationError() + { + // Arrange + var args = _commandDefinition.Parse([ + "--subscription", "test-subscription", + "--resource-group", "test-rg", + "--server", "test-server", + "--database", "test-db", + "--storage-uri", "https://storage.blob.core.windows.net/container/export.bacpac", + "--storage-key", "storagekey123", + "--storage-key-type", "StorageAccessKey", + "--admin-user", "admin", + "--admin-password", "password123", + "--auth-type", "InvalidAuth" + ]); + + // Act + var response = await _command.ExecuteAsync(_context, args); + + // Assert + Assert.Equal(400, response.Status); + Assert.Contains("Invalid authentication type", response.Message); + } + + [Fact] + public async Task ExecuteAsync_CallsServiceWithCorrectParameters() + { + // Arrange + var mockResult = new SqlDatabaseExportResult( + OperationId: "operation-123", + RequestId: "request-456", + Status: "InProgress", + QueuedTime: DateTimeOffset.UtcNow, + LastModifiedTime: DateTimeOffset.UtcNow, + ServerName: "test-server", + DatabaseName: "test-db", + StorageUri: "https://storage.blob.core.windows.net/container/export.bacpac", + Message: null + ); + + _sqlService.ExportDatabaseAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(mockResult); + + var args = _commandDefinition.Parse([ + "--subscription", "test-subscription", + "--resource-group", "test-rg", + "--server", "test-server", + "--database", "test-db", + "--storage-uri", "https://storage.blob.core.windows.net/container/export.bacpac", + "--storage-key", "storagekey123", + "--storage-key-type", "StorageAccessKey", + "--admin-user", "admin", + "--admin-password", "password123", + "--auth-type", "SQL" + ]); + + // Act + await _command.ExecuteAsync(_context, args); + + // Assert + await _sqlService.Received(1).ExportDatabaseAsync( + "test-server", + "test-db", + "test-rg", + "test-subscription", + "https://storage.blob.core.windows.net/container/export.bacpac", + "storagekey123", + "StorageAccessKey", + "admin", + "password123", + "SQL", + Arg.Any(), + Arg.Any()); + } + + [Theory] + [InlineData("StorageAccessKey")] + [InlineData("SharedAccessKey")] + [InlineData("ManagedIdentity")] + public async Task ExecuteAsync_WithValidStorageKeyTypes_ExecutesSuccessfully(string storageKeyType) + { + // Arrange + var mockResult = new SqlDatabaseExportResult( + OperationId: "operation-123", + RequestId: "request-456", + Status: "InProgress", + QueuedTime: DateTimeOffset.UtcNow, + LastModifiedTime: DateTimeOffset.UtcNow, + ServerName: "test-server", + DatabaseName: "test-db", + StorageUri: "https://storage.blob.core.windows.net/container/export.bacpac", + Message: null + ); + + _sqlService.ExportDatabaseAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(mockResult); + + var args = _commandDefinition.Parse([ + "--subscription", "test-subscription", + "--resource-group", "test-rg", + "--server", "test-server", + "--database", "test-db", + "--storage-uri", "https://storage.blob.core.windows.net/container/export.bacpac", + "--storage-key", "storagekey123", + "--storage-key-type", storageKeyType, + "--admin-user", "admin", + "--admin-password", "password123", + "--auth-type", "SQL" + ]); + + // Act + var response = await _command.ExecuteAsync(_context, args); + + // Assert + Assert.Equal(200, response.Status); + } + + [Theory] + [InlineData("SQL")] + [InlineData("ADPassword")] + [InlineData("ManagedIdentity")] + public async Task ExecuteAsync_WithValidAuthTypes_ExecutesSuccessfully(string authType) + { + // Arrange + var mockResult = new SqlDatabaseExportResult( + OperationId: "operation-123", + RequestId: "request-456", + Status: "InProgress", + QueuedTime: DateTimeOffset.UtcNow, + LastModifiedTime: DateTimeOffset.UtcNow, + ServerName: "test-server", + DatabaseName: "test-db", + StorageUri: "https://storage.blob.core.windows.net/container/export.bacpac", + Message: null + ); + + _sqlService.ExportDatabaseAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(mockResult); + + var args = _commandDefinition.Parse([ + "--subscription", "test-subscription", + "--resource-group", "test-rg", + "--server", "test-server", + "--database", "test-db", + "--storage-uri", "https://storage.blob.core.windows.net/container/export.bacpac", + "--storage-key", "storagekey123", + "--storage-key-type", "StorageAccessKey", + "--admin-user", "admin", + "--admin-password", "password123", + "--auth-type", authType + ]); + + // Act + var response = await _command.ExecuteAsync(_context, args); + + // Assert + Assert.Equal(200, response.Status); + } + + [Fact] + public async Task ExecuteAsync_WithMissingAdminUser_ReturnsValidationError() + { + // Arrange + var args = _commandDefinition.Parse([ + "--subscription", "test-subscription", + "--resource-group", "test-rg", + "--server", "test-server", + "--database", "test-db", + "--storage-uri", "https://storage.blob.core.windows.net/container/export.bacpac", + "--storage-key", "storagekey123", + "--storage-key-type", "StorageAccessKey", + "--admin-password", "password123", + "--auth-type", "SQL" + ]); + + // Act + var response = await _command.ExecuteAsync(_context, args); + + // Assert + Assert.Equal(400, response.Status); + Assert.Contains("Missing Required options: --admin-user", response.Message); + } + + [Fact] + public async Task ExecuteAsync_WithMissingAdminPassword_ReturnsValidationError() + { + // Arrange + var args = _commandDefinition.Parse([ + "--subscription", "test-subscription", + "--resource-group", "test-rg", + "--server", "test-server", + "--database", "test-db", + "--storage-uri", "https://storage.blob.core.windows.net/container/export.bacpac", + "--storage-key", "storagekey123", + "--storage-key-type", "StorageAccessKey", + "--admin-user", "admin", + "--auth-type", "SQL" + ]); + + // Act + var response = await _command.ExecuteAsync(_context, args); + + // Assert + Assert.Equal(400, response.Status); + Assert.Contains("Missing Required options: --admin-password", response.Message); + } +} \ No newline at end of file From 41ea76ccfd6fee62d7535566ec58e9506ef0c818 Mon Sep 17 00:00:00 2001 From: Achyuth Maddala Sitaram <66455292+achyuth-ms@users.noreply.github.com> Date: Thu, 25 Sep 2025 14:10:22 -0700 Subject: [PATCH 2/3] rebase and error code fix --- docs/azmcp-commands.md | 8 ++--- servers/Azure.Mcp.Server/CHANGELOG.md | 1 + .../Database/DatabaseExportCommand.cs | 30 ++++++++-------- .../src/Commands/SqlJsonContext.cs | 1 + .../src/Options/SqlOptionDefinitions.cs | 2 +- .../src/Services/SqlService.cs | 9 +++-- tools/Azure.Mcp.Tools.Sql/src/SqlSetup.cs | 1 + .../Database/DatabaseExportCommandTests.cs | 36 +++++++++---------- 8 files changed, 44 insertions(+), 44 deletions(-) diff --git a/docs/azmcp-commands.md b/docs/azmcp-commands.md index c39769670..62b2e49ea 100644 --- a/docs/azmcp-commands.md +++ b/docs/azmcp-commands.md @@ -272,7 +272,7 @@ azmcp appconfig kv list --subscription \ [--key ] \ [--label