diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 76069d3411..7d42c1e6ed 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -166,6 +166,12 @@
# ServiceLabel: %tools-Docker
# ServiceOwners: @conniey @microsoft/azure-mcp
+# PRLabel: %tools-DocumentDb
+/tools/Azure.Mcp.Tools.DocumentDb/ @xingfan-git @microsoft/azure-mcp
+
+# ServiceLabel: %tools-DocumentDb
+# ServiceOwners: @xingfan-git
+
# ServiceLabel: %tools-Eclipse
# ServiceOwners: @srnagar @microsoft/azure-mcp
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 9ac3e74524..89e339ba97 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -83,6 +83,7 @@
+
diff --git a/eng/scripts/New-BuildInfo.ps1 b/eng/scripts/New-BuildInfo.ps1
index 1b1bce7546..187acdf52f 100644
--- a/eng/scripts/New-BuildInfo.ps1
+++ b/eng/scripts/New-BuildInfo.ps1
@@ -170,6 +170,53 @@ function CheckVariable($name) {
return $value
}
+function Test-ProjectUsesMongoDbDriver {
+ param(
+ [string] $ProjectPath
+ )
+
+ if (!(Test-Path $ProjectPath)) {
+ return $false
+ }
+
+ $projectContent = Get-Content $ProjectPath -Raw
+ return $projectContent -match '
+
+
+
+
+
+
+
+
diff --git a/servers/Azure.Mcp.Server/README.md b/servers/Azure.Mcp.Server/README.md
index 3377dcafee..5117423d4d 100644
--- a/servers/Azure.Mcp.Server/README.md
+++ b/servers/Azure.Mcp.Server/README.md
@@ -963,6 +963,18 @@ Example prompts that generate Azure CLI commands:
* "Get Azure Data Explorer databases in cluster 'mycluster'"
* "Sample 10 rows from table 'StormEvents' in Azure Data Explorer database 'db1'"
+### 🗄️ Azure DocumentDB (with MongoDB compatibility)
+
+* "List indexes for collection 'items' in DocumentDB database 'test'"
+* "Create an index on field 'category' for collection 'items' in DocumentDB database 'test'"
+* "Drop index 'category_1' from collection 'items' in DocumentDB database 'test'"
+* "Show index statistics for collection 'items' in DocumentDB database 'test'"
+* "Show current DocumentDB operations"
+* "List all databases in DocumentDB"
+* "Get statistics for database 'mydb'"
+* "Get details for database 'analytics' in DocumentDB"
+* "Drop database 'testdb'"
+
### 📣 Azure Event Grid
* "List all Event Grid topics in subscription 'my-subscription'"
diff --git a/servers/Azure.Mcp.Server/changelog-entries/1773124572664.yaml b/servers/Azure.Mcp.Server/changelog-entries/1773124572664.yaml
new file mode 100644
index 0000000000..7fc9dfd344
--- /dev/null
+++ b/servers/Azure.Mcp.Server/changelog-entries/1773124572664.yaml
@@ -0,0 +1,4 @@
+pr: 1968
+changes:
+ - section: "Features Added"
+ description: "Added mcp tools for managing Azure DocumentDB (with MongoDB compatibility) index"
\ No newline at end of file
diff --git a/servers/Azure.Mcp.Server/changelog-entries/1773579786664.yaml b/servers/Azure.Mcp.Server/changelog-entries/1773579786664.yaml
new file mode 100644
index 0000000000..f7cd615a31
--- /dev/null
+++ b/servers/Azure.Mcp.Server/changelog-entries/1773579786664.yaml
@@ -0,0 +1,3 @@
+changes:
+ - section: "Features Added"
+ description: "Added mcp tools for managing Azure DocumentDB (with MongoDB compatibility) database"
\ No newline at end of file
diff --git a/servers/Azure.Mcp.Server/docs/azmcp-commands.md b/servers/Azure.Mcp.Server/docs/azmcp-commands.md
index dbb2165bba..be8d9a6179 100644
--- a/servers/Azure.Mcp.Server/docs/azmcp-commands.md
+++ b/servers/Azure.Mcp.Server/docs/azmcp-commands.md
@@ -1694,6 +1694,57 @@ azmcp deviceregistry namespace list --subscription \
[--resource-group ]
```
+### Azure DocumentDB (with MongoDB compatibility) Operations
+
+```bash
+# List all indexes on a collection
+# ❌ Destructive | ✅ Idempotent | ❌ OpenWorld | ✅ ReadOnly | ❌ Secret | ❌ LocalRequired
+azmcp documentdb index list indexes --connection-string \
+ --db-name \
+ --collection-name
+
+# Create an index on a collection
+# ✅ Destructive | ❌ Idempotent | ❌ OpenWorld | ❌ ReadOnly | ❌ Secret | ❌ LocalRequired
+azmcp documentdb index create index --connection-string \
+ --db-name \
+ --collection-name \
+ --keys \
+ [--options ]
+
+# Drop an index from a collection
+# ✅ Destructive | ❌ Idempotent | ❌ OpenWorld | ❌ ReadOnly | ❌ Secret | ❌ LocalRequired
+azmcp documentdb index drop index --connection-string \
+ --db-name \
+ --collection-name \
+ --index-name
+
+# Get index statistics for a collection
+# ❌ Destructive | ✅ Idempotent | ❌ OpenWorld | ✅ ReadOnly | ❌ Secret | ❌ LocalRequired
+azmcp documentdb index index stats --connection-string \
+ --db-name \
+ --collection-name
+
+# Get current DocumentDB operations
+# ❌ Destructive | ✅ Idempotent | ❌ OpenWorld | ✅ ReadOnly | ❌ Secret | ❌ LocalRequired
+azmcp documentdb index current ops --connection-string \
+ [--ops ]
+
+# List all databases or inspect a single database
+# ❌ Destructive | ✅ Idempotent | ❌ OpenWorld | ✅ ReadOnly | ❌ Secret | ❌ LocalRequired
+azmcp documentdb database list databases --connection-string \
+ [--db-name ]
+
+# Get statistics for a database
+# ❌ Destructive | ✅ Idempotent | ❌ OpenWorld | ✅ ReadOnly | ❌ Secret | ❌ LocalRequired
+azmcp documentdb database db stats --connection-string \
+ --db-name
+
+# Drop a database
+# ✅ Destructive | ❌ Idempotent | ❌ OpenWorld | ❌ ReadOnly | ❌ Secret | ❌ LocalRequired
+azmcp documentdb database drop database --connection-string \
+ --db-name
+```
+
### Azure Event Grid Operations
```bash
diff --git a/servers/Azure.Mcp.Server/docs/e2eTestPrompts.md b/servers/Azure.Mcp.Server/docs/e2eTestPrompts.md
index a75f04dac0..2f4d9590d9 100644
--- a/servers/Azure.Mcp.Server/docs/e2eTestPrompts.md
+++ b/servers/Azure.Mcp.Server/docs/e2eTestPrompts.md
@@ -356,6 +356,28 @@ This file contains prompts used for end-to-end testing to ensure each tool is in
| deviceregistry_namespace_list | List Device Registry namespaces in resource group |
| deviceregistry_namespace_list | What Device Registry namespaces do I have in my Azure subscription? |
+## Azure DocumentDB (with MongoDB compatibility)
+
+| Tool Name | Test Prompt |
+|:----------|:----------|
+| documentdb_index_list_indexes | List indexes for collection in DocumentDB database |
+| documentdb_index_list_indexes | Show me all indexes on collection in database |
+| documentdb_index_create_index | Create an index on collection in DocumentDB database using keys |
+| documentdb_index_create_index | Add a DocumentDB index for collection in database with keys and options |
+| documentdb_index_drop_index | Drop index from collection in DocumentDB database |
+| documentdb_index_drop_index | Remove the index from DocumentDB collection in database |
+| documentdb_index_index_stats | Show index statistics for collection in DocumentDB database |
+| documentdb_index_index_stats | Get DocumentDB index stats for collection in database |
+| documentdb_index_current_ops | Show current DocumentDB operations |
+| documentdb_index_current_ops | Get current DocumentDB operations filtered by |
+| documentdb_database_db_stats | Get statistics for database |
+| documentdb_database_db_stats | Show me stats for DocumentDB database |
+| documentdb_database_drop_database | Drop database |
+| documentdb_database_drop_database | Delete the database from DocumentDB |
+| documentdb_database_list_databases | List all databases in DocumentDB |
+| documentdb_database_list_databases | Show me all DocumentDB databases |
+| documentdb_database_list_databases | Get details for database in DocumentDB |
+
## Azure Event Grid
| Tool Name | Test Prompt |
diff --git a/servers/Azure.Mcp.Server/src/Program.cs b/servers/Azure.Mcp.Server/src/Program.cs
index 53c3b752ce..7fd21e71f7 100644
--- a/servers/Azure.Mcp.Server/src/Program.cs
+++ b/servers/Azure.Mcp.Server/src/Program.cs
@@ -104,6 +104,7 @@ private static IAreaSetup[] RegisterAreas()
new Azure.Mcp.Tools.AzureTerraformBestPractices.AzureTerraformBestPracticesSetup(),
new Azure.Mcp.Tools.Deploy.DeploySetup(),
new Azure.Mcp.Tools.DeviceRegistry.DeviceRegistrySetup(),
+ new Azure.Mcp.Tools.DocumentDb.DocumentDbSetup(),
new Azure.Mcp.Tools.EventGrid.EventGridSetup(),
new Azure.Mcp.Tools.Acr.AcrSetup(),
new Azure.Mcp.Tools.Advisor.AdvisorSetup(),
diff --git a/servers/Azure.Mcp.Server/src/Resources/consolidated-tools.json b/servers/Azure.Mcp.Server/src/Resources/consolidated-tools.json
index c3deaf18a9..4c6d28d62a 100644
--- a/servers/Azure.Mcp.Server/src/Resources/consolidated-tools.json
+++ b/servers/Azure.Mcp.Server/src/Resources/consolidated-tools.json
@@ -181,6 +181,142 @@
"cosmos_database_container_item_query"
]
},
+ {
+ "name": "inspect_azure_documentdb_indexes_and_diagnostics",
+ "description": "Inspect Azure DocumentDB collection indexes, index statistics, and current operations by supplying a connection string for each request.",
+ "toolMetadata": {
+ "destructive": {
+ "value": false,
+ "description": "This tool performs only additive updates without deleting or modifying existing resources."
+ },
+ "idempotent": {
+ "value": true,
+ "description": "Running this operation multiple times with the same arguments produces the same result without additional effects."
+ },
+ "openWorld": {
+ "value": false,
+ "description": "This tool's domain of interaction is closed and well-defined, limited to a specific set of entities (like memory access)."
+ },
+ "readOnly": {
+ "value": true,
+ "description": "This tool only performs read operations without modifying any state or data."
+ },
+ "secret": {
+ "value": false,
+ "description": "This tool does not handle sensitive or secret information."
+ },
+ "localRequired": {
+ "value": false,
+ "description": "This tool is available in both local and remote server modes."
+ }
+ },
+ "mappedToolList": [
+ "documentdb_index_list_indexes",
+ "documentdb_index_index_stats",
+ "documentdb_index_current_ops"
+ ]
+ },
+ {
+ "name": "manage_azure_documentdb_indexes",
+ "description": "Create or drop indexes in Azure DocumentDB collections by supplying a connection string for each request.",
+ "toolMetadata": {
+ "destructive": {
+ "value": true,
+ "description": "This tool may delete or modify existing resources in its environment."
+ },
+ "idempotent": {
+ "value": false,
+ "description": "Running this operation multiple times with the same arguments may have additional effects or produce different results."
+ },
+ "openWorld": {
+ "value": false,
+ "description": "This tool's domain of interaction is closed and well-defined, limited to a specific set of entities (like memory access)."
+ },
+ "readOnly": {
+ "value": false,
+ "description": "This tool may modify its environment and perform write operations (create, update, delete)."
+ },
+ "secret": {
+ "value": false,
+ "description": "This tool does not handle sensitive or secret information."
+ },
+ "localRequired": {
+ "value": false,
+ "description": "This tool is available in both local and remote server modes."
+ }
+ },
+ "mappedToolList": [
+ "documentdb_index_create_index",
+ "documentdb_index_drop_index"
+ ]
+ },
+ {
+ "name": "get_azure_documentdb_database_details",
+ "description": "List DocumentDB databases, inspect a specific database, and retrieve database statistics including collection counts and storage usage.",
+ "toolMetadata": {
+ "destructive": {
+ "value": false,
+ "description": "This tool performs only additive updates without deleting or modifying existing resources."
+ },
+ "idempotent": {
+ "value": true,
+ "description": "Running this operation multiple times with the same arguments produces the same result without additional effects."
+ },
+ "openWorld": {
+ "value": false,
+ "description": "This tool's domain of interaction is closed and well-defined, limited to a specific set of entities (like memory access)."
+ },
+ "readOnly": {
+ "value": true,
+ "description": "This tool only performs read operations without modifying any state or data."
+ },
+ "secret": {
+ "value": false,
+ "description": "This tool does not handle sensitive or secret information."
+ },
+ "localRequired": {
+ "value": false,
+ "description": "This tool is available in both local and remote server modes."
+ }
+ },
+ "mappedToolList": [
+ "documentdb_database_list_databases",
+ "documentdb_database_db_stats"
+ ]
+ },
+ {
+ "name": "delete_azure_documentdb_databases",
+ "description": "Delete DocumentDB databases by dropping a database and all of its collections and data.",
+ "toolMetadata": {
+ "destructive": {
+ "value": true,
+ "description": "This tool may delete or modify existing resources in its environment."
+ },
+ "idempotent": {
+ "value": false,
+ "description": "Running this operation multiple times with the same arguments may have additional effects or produce different results."
+ },
+ "openWorld": {
+ "value": false,
+ "description": "This tool's domain of interaction is closed and well-defined, limited to a specific set of entities (like memory access)."
+ },
+ "readOnly": {
+ "value": false,
+ "description": "This tool may modify its environment and perform write operations (create, update, delete)."
+ },
+ "secret": {
+ "value": false,
+ "description": "This tool does not handle sensitive or secret information."
+ },
+ "localRequired": {
+ "value": false,
+ "description": "This tool is available in both local and remote server modes."
+ }
+ },
+ "mappedToolList": [
+ "documentdb_database_drop_database"
+ ]
+ },
{
"name": "create_azure_sql_databases_and_servers",
"description": "Create new Azure SQL databases and SQL servers with configurable performance tiers and settings.",
diff --git a/tools/Azure.Mcp.Tools.DocumentDb/src/AssemblyInfo.cs b/tools/Azure.Mcp.Tools.DocumentDb/src/AssemblyInfo.cs
new file mode 100644
index 0000000000..9f6cdfab4b
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.DocumentDb/src/AssemblyInfo.cs
@@ -0,0 +1,6 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("Azure.Mcp.Tools.DocumentDb.UnitTests")]
diff --git a/tools/Azure.Mcp.Tools.DocumentDb/src/Azure.Mcp.Tools.DocumentDb.csproj b/tools/Azure.Mcp.Tools.DocumentDb/src/Azure.Mcp.Tools.DocumentDb.csproj
new file mode 100644
index 0000000000..0131c8a708
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.DocumentDb/src/Azure.Mcp.Tools.DocumentDb.csproj
@@ -0,0 +1,22 @@
+
+
+ true
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tools/Azure.Mcp.Tools.DocumentDb/src/Commands/BaseDocumentDbCommand.cs b/tools/Azure.Mcp.Tools.DocumentDb/src/Commands/BaseDocumentDbCommand.cs
new file mode 100644
index 0000000000..2540569aec
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.DocumentDb/src/Commands/BaseDocumentDbCommand.cs
@@ -0,0 +1,29 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.CommandLine;
+using System.Diagnostics.CodeAnalysis;
+using Azure.Mcp.Core.Commands;
+using Azure.Mcp.Core.Extensions;
+using Azure.Mcp.Tools.DocumentDb.Options;
+
+namespace Azure.Mcp.Tools.DocumentDb.Commands;
+
+public abstract class BaseDocumentDbCommand<
+ [DynamicallyAccessedMembers(TrimAnnotations.CommandAnnotations)] TOptions>
+ : GlobalCommand where TOptions : BaseDocumentDbOptions, new()
+{
+ protected override void RegisterOptions(Command command)
+ {
+ base.RegisterOptions(command);
+ command.Options.Add(DocumentDbOptionDefinitions.ConnectionString);
+ }
+
+ protected override TOptions BindOptions(ParseResult parseResult)
+ {
+ return new TOptions
+ {
+ ConnectionString = parseResult.GetValueOrDefault(DocumentDbOptionDefinitions.ConnectionString.Name)
+ };
+ }
+}
diff --git a/tools/Azure.Mcp.Tools.DocumentDb/src/Commands/Database/DbStatsCommand.cs b/tools/Azure.Mcp.Tools.DocumentDb/src/Commands/Database/DbStatsCommand.cs
new file mode 100644
index 0000000000..3a763bb358
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.DocumentDb/src/Commands/Database/DbStatsCommand.cs
@@ -0,0 +1,83 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.CommandLine;
+using Azure.Mcp.Core.Commands;
+using Azure.Mcp.Core.Extensions;
+using Azure.Mcp.Tools.DocumentDb.Options;
+using Azure.Mcp.Tools.DocumentDb.Services;
+using Microsoft.Extensions.Logging;
+using Microsoft.Mcp.Core.Commands;
+using Microsoft.Mcp.Core.Models.Command;
+
+namespace Azure.Mcp.Tools.DocumentDb.Commands.Database;
+
+public sealed class DbStatsCommand(ILogger logger)
+ : BaseDocumentDbCommand()
+{
+ private readonly ILogger _logger = logger;
+
+ public override string Id => "e5f6a7b8-c9d0-4e5f-2a3b-4c5d6e7f8a9b";
+
+ public override string Name => "db_stats";
+
+ public override string Description => "Show statistics for a DocumentDB database, including collection counts, size, and storage usage details.";
+
+ public override string Title => "Database Statistics";
+
+ public override ToolMetadata Metadata => new()
+ {
+ Destructive = false,
+ Idempotent = true,
+ OpenWorld = false,
+ ReadOnly = true,
+ Secret = false,
+ LocalRequired = false
+ };
+
+ protected override void RegisterOptions(Command command)
+ {
+ base.RegisterOptions(command);
+ command.Options.Add(DocumentDbOptionDefinitions.DbName);
+ }
+
+ protected override DbStatsOptions BindOptions(ParseResult parseResult)
+ {
+ var options = base.BindOptions(parseResult);
+ options.DbName = parseResult.GetValueOrDefault(DocumentDbOptionDefinitions.DbName.Name);
+ return options;
+ }
+
+ public override async Task ExecuteAsync(
+ CommandContext context,
+ ParseResult parseResult,
+ CancellationToken cancellationToken)
+ {
+ DbStatsOptions? options = null;
+
+ try
+ {
+ if (!Validate(parseResult.CommandResult, context.Response).IsValid)
+ {
+ return context.Response;
+ }
+
+ options = BindOptions(parseResult);
+ var dbName = options.DbName!;
+
+ var service = context.GetService();
+
+ var result = await service.GetDatabaseStatsAsync(options.ConnectionString!, dbName, cancellationToken);
+
+ DocumentDbResponseHelper.ProcessResponse(context, result);
+
+ return context.Response;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to get database statistics for database: {DbName}", options?.DbName);
+ HandleException(context, ex);
+ return context.Response;
+ }
+ }
+}
diff --git a/tools/Azure.Mcp.Tools.DocumentDb/src/Commands/Database/DropDatabaseCommand.cs b/tools/Azure.Mcp.Tools.DocumentDb/src/Commands/Database/DropDatabaseCommand.cs
new file mode 100644
index 0000000000..631b7f842c
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.DocumentDb/src/Commands/Database/DropDatabaseCommand.cs
@@ -0,0 +1,83 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.CommandLine;
+using Azure.Mcp.Core.Commands;
+using Azure.Mcp.Core.Extensions;
+using Azure.Mcp.Tools.DocumentDb.Options;
+using Azure.Mcp.Tools.DocumentDb.Services;
+using Microsoft.Extensions.Logging;
+using Microsoft.Mcp.Core.Commands;
+using Microsoft.Mcp.Core.Models.Command;
+
+namespace Azure.Mcp.Tools.DocumentDb.Commands.Database;
+
+public sealed class DropDatabaseCommand(ILogger logger)
+ : BaseDocumentDbCommand()
+{
+ private readonly ILogger _logger = logger;
+
+ public override string Id => "a7b8c9d0-e1f2-4a7b-4c5d-6e7f8a9b0c1d";
+
+ public override string Name => "drop_database";
+
+ public override string Description => "Drop a DocumentDB database, removing all of its collections and data.";
+
+ public override string Title => "Drop Database";
+
+ public override ToolMetadata Metadata => new()
+ {
+ Destructive = true,
+ Idempotent = false,
+ OpenWorld = false,
+ ReadOnly = false,
+ Secret = false,
+ LocalRequired = false
+ };
+
+ protected override void RegisterOptions(Command command)
+ {
+ base.RegisterOptions(command);
+ command.Options.Add(DocumentDbOptionDefinitions.DbName);
+ }
+
+ protected override DropDatabaseOptions BindOptions(ParseResult parseResult)
+ {
+ var options = base.BindOptions(parseResult);
+ options.DbName = parseResult.GetValueOrDefault(DocumentDbOptionDefinitions.DbName.Name);
+ return options;
+ }
+
+ public override async Task ExecuteAsync(
+ CommandContext context,
+ ParseResult parseResult,
+ CancellationToken cancellationToken)
+ {
+ DropDatabaseOptions? options = null;
+
+ try
+ {
+ if (!Validate(parseResult.CommandResult, context.Response).IsValid)
+ {
+ return context.Response;
+ }
+
+ options = BindOptions(parseResult);
+ var dbName = options.DbName!;
+
+ var service = context.GetService();
+
+ var result = await service.DropDatabaseAsync(options.ConnectionString!, dbName, cancellationToken);
+
+ DocumentDbResponseHelper.ProcessResponse(context, result);
+
+ return context.Response;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to drop database: {DbName}", options?.DbName);
+ HandleException(context, ex);
+ return context.Response;
+ }
+ }
+}
diff --git a/tools/Azure.Mcp.Tools.DocumentDb/src/Commands/Database/ListDatabasesCommand.cs b/tools/Azure.Mcp.Tools.DocumentDb/src/Commands/Database/ListDatabasesCommand.cs
new file mode 100644
index 0000000000..875cc94dc1
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.DocumentDb/src/Commands/Database/ListDatabasesCommand.cs
@@ -0,0 +1,84 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.CommandLine;
+using Azure.Mcp.Core.Commands;
+using Azure.Mcp.Core.Extensions;
+using Azure.Mcp.Tools.DocumentDb.Options;
+using Azure.Mcp.Tools.DocumentDb.Services;
+using Microsoft.Extensions.Logging;
+using Microsoft.Mcp.Core.Commands;
+using Microsoft.Mcp.Core.Models.Command;
+using Microsoft.Mcp.Core.Models.Option;
+
+namespace Azure.Mcp.Tools.DocumentDb.Commands.Database;
+
+public sealed class ListDatabasesCommand(ILogger logger)
+ : BaseDocumentDbCommand()
+{
+ private readonly ILogger _logger = logger;
+
+ public override string Id => "d4e5f6a7-b8c9-4d4e-1f2a-3b4c5d6e7f8a";
+
+ public override string Name => "list_databases";
+
+ public override string Description => "List DocumentDB databases. If --db-name is omitted, returns all database names. If --db-name is provided, returns detailed information for that database.";
+
+ public override string Title => "List Databases";
+
+ public override ToolMetadata Metadata => new()
+ {
+ Destructive = false,
+ Idempotent = true,
+ OpenWorld = false,
+ ReadOnly = true,
+ Secret = false,
+ LocalRequired = false
+ };
+
+ protected override void RegisterOptions(Command command)
+ {
+ base.RegisterOptions(command);
+ command.Options.Add(DocumentDbOptionDefinitions.DbName.AsOptional());
+ }
+
+ protected override ListDatabasesOptions BindOptions(ParseResult parseResult)
+ {
+ var options = base.BindOptions(parseResult);
+ options.DbName = parseResult.GetValueOrDefault(DocumentDbOptionDefinitions.DbName.Name);
+ return options;
+ }
+
+ public override async Task ExecuteAsync(
+ CommandContext context,
+ ParseResult parseResult,
+ CancellationToken cancellationToken)
+ {
+ ListDatabasesOptions? options = null;
+
+ try
+ {
+ if (!Validate(parseResult.CommandResult, context.Response).IsValid)
+ {
+ return context.Response;
+ }
+
+ options = BindOptions(parseResult);
+ var dbName = options.DbName;
+
+ var service = context.GetService();
+
+ var result = await service.GetDatabasesAsync(options.ConnectionString!, dbName, cancellationToken);
+
+ DocumentDbResponseHelper.ProcessResponse(context, result);
+
+ return context.Response;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to get DocumentDB database details. Database: {DbName}", options?.DbName);
+ HandleException(context, ex);
+ return context.Response;
+ }
+ }
+}
diff --git a/tools/Azure.Mcp.Tools.DocumentDb/src/Commands/DocumentDbHelpers.cs b/tools/Azure.Mcp.Tools.DocumentDb/src/Commands/DocumentDbHelpers.cs
new file mode 100644
index 0000000000..749d28ed9a
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.DocumentDb/src/Commands/DocumentDbHelpers.cs
@@ -0,0 +1,125 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using MongoDB.Bson;
+using MongoDB.Bson.IO;
+using MongoDB.Bson.Serialization;
+
+namespace Azure.Mcp.Tools.DocumentDb.Commands;
+
+internal static class DocumentDbHelpers
+{
+ public static BsonDocument? ParseBsonDocument(string? json)
+ {
+ if (string.IsNullOrWhiteSpace(json))
+ return null;
+
+ try
+ {
+ return BsonDocument.Parse(json);
+ }
+ catch (Exception ex)
+ {
+ throw new ArgumentException("The provided value is not valid BSON/JSON document content.", nameof(json), ex);
+ }
+ }
+
+ public static BsonDocument? ParseBsonDocument(object? value)
+ {
+ if (value == null)
+ return null;
+
+ if (value is string str)
+ return ParseBsonDocument(str);
+
+ if (value is BsonDocument doc)
+ return doc;
+
+ try
+ {
+ var json = DocumentDbResponseHelper.SerializeToJson(value);
+ return BsonDocument.Parse(json);
+ }
+ catch (Exception ex)
+ {
+ throw new InvalidOperationException($"The value of type '{value.GetType().FullName}' could not be converted to a BSON document.", ex);
+ }
+ }
+
+ public static List? ParseBsonDocumentList(string? json)
+ {
+ if (string.IsNullOrWhiteSpace(json))
+ return null;
+
+ try
+ {
+ var bsonArray = BsonSerializer.Deserialize(json);
+ return bsonArray.Select(item => item.AsBsonDocument).ToList();
+ }
+ catch (Exception ex)
+ {
+ throw new ArgumentException("The provided value is not valid BSON/JSON array content.", nameof(json), ex);
+ }
+ }
+
+ public static List? ParseBsonDocumentList(object? value)
+ {
+ if (value == null)
+ return null;
+
+ if (value is string str)
+ return ParseBsonDocumentList(str);
+
+ if (value is List list)
+ return list;
+
+ try
+ {
+ var json = DocumentDbResponseHelper.SerializeToJson(value);
+ return ParseBsonDocumentList(json);
+ }
+ catch (Exception ex)
+ {
+ throw new InvalidOperationException($"The value of type '{value.GetType().FullName}' could not be converted to a BSON document list.", ex);
+ }
+ }
+
+ public static bool ParseBoolean(string? value, bool defaultValue = false)
+ {
+ if (string.IsNullOrWhiteSpace(value))
+ return defaultValue;
+
+ if (bool.TryParse(value, out var result))
+ return result;
+
+ // Handle common string representations
+ return value.Trim().ToLowerInvariant() switch
+ {
+ "true" or "1" or "yes" => true,
+ "false" or "0" or "no" => false,
+ _ => defaultValue
+ };
+ }
+
+ public static int ParseInt(string? value, int defaultValue = 0)
+ {
+ if (string.IsNullOrWhiteSpace(value))
+ return defaultValue;
+
+ return int.TryParse(value, out var result) ? result : defaultValue;
+ }
+
+ public static string SerializeBsonToJson(BsonDocument document)
+ {
+ var jsonWriterSettings = new JsonWriterSettings { OutputMode = JsonOutputMode.RelaxedExtendedJson };
+ return document.ToJson(jsonWriterSettings);
+ }
+
+ public static string SerializeBsonToJson(object obj)
+ {
+ if (obj is BsonDocument doc)
+ return SerializeBsonToJson(doc);
+
+ return DocumentDbResponseHelper.SerializeToJson(obj);
+ }
+}
diff --git a/tools/Azure.Mcp.Tools.DocumentDb/src/Commands/DocumentDbJsonContext.cs b/tools/Azure.Mcp.Tools.DocumentDb/src/Commands/DocumentDbJsonContext.cs
new file mode 100644
index 0000000000..225ca9ded4
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.DocumentDb/src/Commands/DocumentDbJsonContext.cs
@@ -0,0 +1,98 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Text.Json.Serialization;
+using Azure.Mcp.Tools.DocumentDb.Models;
+using MongoDB.Bson;
+
+namespace Azure.Mcp.Tools.DocumentDb.Commands;
+
+[JsonSourceGenerationOptions(
+ WriteIndented = false,
+ PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
+[JsonSerializable(typeof(string))]
+[JsonSerializable(typeof(object))]
+[JsonSerializable(typeof(int))]
+[JsonSerializable(typeof(bool))]
+[JsonSerializable(typeof(long))]
+[JsonSerializable(typeof(List))]
+[JsonSerializable(typeof(Dictionary))]
+[JsonSerializable(typeof(Dictionary))]
+[JsonSerializable(typeof(List>))]
+[JsonSerializable(typeof(List>))]
+[JsonSerializable(typeof(System.Text.Json.JsonElement))]
+internal partial class DocumentDbJsonContext : JsonSerializerContext;
+
+///
+/// Helper class for creating ResponseResult from JSON strings
+///
+internal static class DocumentDbResponseHelper
+{
+ public static Microsoft.Mcp.Core.Models.Command.ResponseResult CreateFromJson(string json)
+ {
+ // Parse the JSON string to a JsonElement to get proper serialization
+ var element = System.Text.Json.JsonSerializer.Deserialize(json, DocumentDbJsonContext.Default.JsonElement);
+ return Microsoft.Mcp.Core.Models.Command.ResponseResult.Create(element, DocumentDbJsonContext.Default.JsonElement);
+ }
+
+ public static string SerializeToJson(object value)
+ {
+ return value switch
+ {
+ // Handle BsonDocument by converting to JSON first
+ MongoDB.Bson.BsonDocument bsonDoc => bsonDoc.ToJson(new MongoDB.Bson.IO.JsonWriterSettings { OutputMode = MongoDB.Bson.IO.JsonOutputMode.RelaxedExtendedJson }),
+ List bsonList => "[" + string.Join(",", bsonList.Select(doc => doc.ToJson(new MongoDB.Bson.IO.JsonWriterSettings { OutputMode = MongoDB.Bson.IO.JsonOutputMode.RelaxedExtendedJson }))) + "]",
+
+ // Handle standard types
+ Dictionary dict => System.Text.Json.JsonSerializer.Serialize(dict, DocumentDbJsonContext.Default.DictionaryStringObject),
+ List> list => System.Text.Json.JsonSerializer.Serialize(list, DocumentDbJsonContext.Default.ListDictionaryStringObject),
+ List strList => System.Text.Json.JsonSerializer.Serialize(strList, DocumentDbJsonContext.Default.ListString),
+ string str => System.Text.Json.JsonSerializer.Serialize(str, DocumentDbJsonContext.Default.String),
+ int i => System.Text.Json.JsonSerializer.Serialize(i, DocumentDbJsonContext.Default.Int32),
+ long l => System.Text.Json.JsonSerializer.Serialize(l, DocumentDbJsonContext.Default.Int64),
+ bool b => System.Text.Json.JsonSerializer.Serialize(b, DocumentDbJsonContext.Default.Boolean),
+ System.Text.Json.JsonElement element => System.Text.Json.JsonSerializer.Serialize(element, DocumentDbJsonContext.Default.JsonElement),
+
+ // Handle IEnumerable (LINQ results)
+ System.Collections.Generic.IEnumerable enumStr => System.Text.Json.JsonSerializer.Serialize(enumStr.ToList(), DocumentDbJsonContext.Default.ListString),
+
+ _ => throw new NotSupportedException($"Type {value.GetType().FullName} is not supported for AOT serialization. Please add it to DocumentDbJsonContext.")
+ };
+ }
+
+ public static T? DeserializeFromJson(string json) where T : class
+ {
+ // Only supports object type for AOT compatibility
+ if (typeof(T) == typeof(object))
+ {
+ return System.Text.Json.JsonSerializer.Deserialize(json, DocumentDbJsonContext.Default.Object) as T;
+ }
+
+ throw new NotSupportedException($"Type {typeof(T).Name} is not supported. Only 'object' type is AOT-compatible.");
+ }
+
+ ///
+ /// Processes a DocumentDb service response and applies it to the command context.
+ ///
+ /// The command context to update.
+ /// The service response.
+ public static void ProcessResponse(Microsoft.Mcp.Core.Models.Command.CommandContext context, DocumentDbResponse response)
+ {
+ context.Response.Status = response.StatusCode;
+
+ if (response.Success)
+ {
+ // For success with no data, create an empty result with the message
+ var dataToSerialize = response.Data ?? new Dictionary
+ {
+ ["message"] = response.Message
+ };
+ context.Response.Results = CreateFromJson(SerializeToJson(dataToSerialize));
+ }
+ else
+ {
+ context.Response.Message = response.Message ?? "Unknown error";
+ }
+ }
+}
diff --git a/tools/Azure.Mcp.Tools.DocumentDb/src/Commands/DocumentDbOptionDefinitions.cs b/tools/Azure.Mcp.Tools.DocumentDb/src/Commands/DocumentDbOptionDefinitions.cs
new file mode 100644
index 0000000000..a808705379
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.DocumentDb/src/Commands/DocumentDbOptionDefinitions.cs
@@ -0,0 +1,108 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.CommandLine;
+
+namespace Azure.Mcp.Tools.DocumentDb.Commands;
+
+internal static class DocumentDbOptionDefinitions
+{
+ public static readonly Option ConnectionString = new("--connection-string")
+ {
+ Description = "Azure DocumentDB connection string used for this request.",
+ Required = true
+ };
+
+ public static readonly Option DbName = new("--db-name")
+ {
+ Description = "Database name",
+ Required = true
+ };
+
+ public static readonly Option CollectionName = new("--collection-name")
+ {
+ Description = "Collection name",
+ Required = true
+ };
+
+ public static readonly Option NewCollectionName = new("--new-collection-name")
+ {
+ Description = "New collection name",
+ Required = true
+ };
+
+ public static readonly Option SampleSize = new("--sample-size")
+ {
+ Description = "Number of documents to sample",
+ DefaultValueFactory = _ => 10
+ };
+
+ public static readonly Option Query = new("--query")
+ {
+ Description = "Query filter in JSON format"
+ };
+
+ public static readonly Option Options = new("--options")
+ {
+ Description = "Query options"
+ };
+
+ public static readonly Option Document = new("--document")
+ {
+ Description = "Document to insert",
+ Required = true
+ };
+
+ public static readonly Option Documents = new("--documents")
+ {
+ Description = "Documents to insert",
+ Required = true
+ };
+
+ public static readonly Option Filter = new("--filter")
+ {
+ Description = "Filter for update/delete",
+ Required = true
+ };
+
+ public static readonly Option Update = new("--update")
+ {
+ Description = "Update operations",
+ Required = true
+ };
+
+ public static readonly Option Upsert = new("--upsert")
+ {
+ Description = "Create document if it doesn't exist",
+ DefaultValueFactory = _ => false
+ };
+
+ public static readonly Option Pipeline = new("--pipeline")
+ {
+ Description = "Aggregation pipeline",
+ Required = true
+ };
+
+ public static readonly Option AllowDiskUse = new("--allow-disk-use")
+ {
+ Description = "Allow pipeline stages to write to disk",
+ DefaultValueFactory = _ => false
+ };
+
+ public static readonly Option Keys = new("--keys")
+ {
+ Description = "Index keys",
+ Required = true
+ };
+
+ public static readonly Option IndexName = new("--index-name")
+ {
+ Description = "Index name",
+ Required = true
+ };
+
+ public static readonly Option Ops = new("--ops")
+ {
+ Description = "Filter for current operations"
+ };
+}
diff --git a/tools/Azure.Mcp.Tools.DocumentDb/src/Commands/Index/CreateIndexCommand.cs b/tools/Azure.Mcp.Tools.DocumentDb/src/Commands/Index/CreateIndexCommand.cs
new file mode 100644
index 0000000000..f39549a8bb
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.DocumentDb/src/Commands/Index/CreateIndexCommand.cs
@@ -0,0 +1,97 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.CommandLine;
+using Azure.Mcp.Core.Commands;
+using Azure.Mcp.Core.Extensions;
+using Azure.Mcp.Tools.DocumentDb.Models;
+using Azure.Mcp.Tools.DocumentDb.Options;
+using Azure.Mcp.Tools.DocumentDb.Services;
+using Microsoft.Extensions.Logging;
+using Microsoft.Mcp.Core.Commands;
+using Microsoft.Mcp.Core.Models.Command;
+
+namespace Azure.Mcp.Tools.DocumentDb.Commands.Index;
+
+public sealed class CreateIndexCommand(ILogger logger)
+ : BaseDocumentDbCommand()
+{
+ private readonly ILogger _logger = logger;
+
+ public override string Id => "a5b6c7d8-e9f0-4a5b-2c3d-4e5f6a7b8c9d";
+
+ public override string Name => "create_index";
+
+ public override string Description => "Create an index on a collection";
+
+ public override string Title => "Create Index";
+
+ public override ToolMetadata Metadata => new()
+ {
+ Destructive = true,
+ Idempotent = false,
+ OpenWorld = false,
+ ReadOnly = false,
+ LocalRequired = false,
+ Secret = false
+ };
+
+ protected override void RegisterOptions(Command command)
+ {
+ base.RegisterOptions(command);
+ command.Options.Add(DocumentDbOptionDefinitions.DbName);
+ command.Options.Add(DocumentDbOptionDefinitions.CollectionName);
+ command.Options.Add(DocumentDbOptionDefinitions.Keys);
+ command.Options.Add(DocumentDbOptionDefinitions.Options);
+ }
+
+ protected override CreateIndexOptions BindOptions(ParseResult parseResult)
+ {
+ var options = base.BindOptions(parseResult);
+ options.DbName = parseResult.GetValueOrDefault(DocumentDbOptionDefinitions.DbName.Name);
+ options.CollectionName = parseResult.GetValueOrDefault(DocumentDbOptionDefinitions.CollectionName.Name);
+ options.Keys = parseResult.GetValueOrDefault(DocumentDbOptionDefinitions.Keys.Name);
+ options.Options = parseResult.GetValueOrDefault(DocumentDbOptionDefinitions.Options.Name);
+ return options;
+ }
+
+ public override async Task ExecuteAsync(
+ CommandContext context,
+ ParseResult parseResult,
+ CancellationToken cancellationToken)
+ {
+ CreateIndexOptions? commandOptions = null;
+
+ try
+ {
+ if (!Validate(parseResult.CommandResult, context.Response).IsValid)
+ {
+ return context.Response;
+ }
+
+ var options = commandOptions = BindOptions(parseResult);
+
+ var service = context.GetService();
+
+ var keys = DocumentDbHelpers.ParseBsonDocument(options.Keys);
+ if (keys == null)
+ {
+ throw new ArgumentException("Invalid keys format");
+ }
+
+ var indexOptions = DocumentDbHelpers.ParseBsonDocument(options.Options);
+
+ DocumentDbResponse result = await service.CreateIndexAsync(options.ConnectionString!, options.DbName!, options.CollectionName!, keys, indexOptions, cancellationToken);
+
+ DocumentDbResponseHelper.ProcessResponse(context, result);
+
+ return context.Response;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to create index on collection: {CollectionName}, database: {DbName}, keys: {Keys}", commandOptions?.CollectionName, commandOptions?.DbName, commandOptions?.Keys);
+ HandleException(context, ex);
+ return context.Response;
+ }
+ }
+}
diff --git a/tools/Azure.Mcp.Tools.DocumentDb/src/Commands/Index/CurrentOpsCommand.cs b/tools/Azure.Mcp.Tools.DocumentDb/src/Commands/Index/CurrentOpsCommand.cs
new file mode 100644
index 0000000000..073ac3bc36
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.DocumentDb/src/Commands/Index/CurrentOpsCommand.cs
@@ -0,0 +1,85 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.CommandLine;
+using Azure.Mcp.Core.Commands;
+using Azure.Mcp.Core.Extensions;
+using Azure.Mcp.Tools.DocumentDb.Models;
+using Azure.Mcp.Tools.DocumentDb.Options;
+using Azure.Mcp.Tools.DocumentDb.Services;
+using Microsoft.Extensions.Logging;
+using Microsoft.Mcp.Core.Commands;
+using Microsoft.Mcp.Core.Models.Command;
+
+namespace Azure.Mcp.Tools.DocumentDb.Commands.Index;
+
+public sealed class CurrentOpsCommand(ILogger logger)
+ : BaseDocumentDbCommand()
+{
+ private readonly ILogger _logger = logger;
+
+ public override string Id => "e9f0a1b2-c3d4-4e9f-6a7b-8c9d0e1f2a3b";
+
+ public override string Name => "current_ops";
+
+ public override string Description => "Get information about current DocumentDB operations";
+
+ public override string Title => "Current Operations";
+
+ public override ToolMetadata Metadata => new()
+ {
+ Destructive = false,
+ Idempotent = true,
+ OpenWorld = false,
+ ReadOnly = true,
+ LocalRequired = false,
+ Secret = false
+ };
+
+ protected override void RegisterOptions(Command command)
+ {
+ base.RegisterOptions(command);
+ command.Options.Add(DocumentDbOptionDefinitions.Ops);
+ }
+
+ protected override CurrentOpsOptions BindOptions(ParseResult parseResult)
+ {
+ var options = base.BindOptions(parseResult);
+ options.Ops = parseResult.GetValueOrDefault(DocumentDbOptionDefinitions.Ops.Name);
+ return options;
+ }
+
+ public override async Task ExecuteAsync(
+ CommandContext context,
+ ParseResult parseResult,
+ CancellationToken cancellationToken)
+ {
+ CurrentOpsOptions? commandOptions = null;
+
+ try
+ {
+ if (!Validate(parseResult.CommandResult, context.Response).IsValid)
+ {
+ return context.Response;
+ }
+
+ var options = commandOptions = BindOptions(parseResult);
+
+ var service = context.GetService();
+
+ var filter = DocumentDbHelpers.ParseBsonDocument(options.Ops);
+
+ DocumentDbResponse result = await service.GetCurrentOpsAsync(options.ConnectionString!, filter, cancellationToken);
+
+ DocumentDbResponseHelper.ProcessResponse(context, result);
+
+ return context.Response;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to get current operations with filter: {Ops}", commandOptions?.Ops);
+ HandleException(context, ex);
+ return context.Response;
+ }
+ }
+}
diff --git a/tools/Azure.Mcp.Tools.DocumentDb/src/Commands/Index/DropIndexCommand.cs b/tools/Azure.Mcp.Tools.DocumentDb/src/Commands/Index/DropIndexCommand.cs
new file mode 100644
index 0000000000..67849acbc3
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.DocumentDb/src/Commands/Index/DropIndexCommand.cs
@@ -0,0 +1,87 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.CommandLine;
+using Azure.Mcp.Core.Commands;
+using Azure.Mcp.Core.Extensions;
+using Azure.Mcp.Tools.DocumentDb.Models;
+using Azure.Mcp.Tools.DocumentDb.Options;
+using Azure.Mcp.Tools.DocumentDb.Services;
+using Microsoft.Extensions.Logging;
+using Microsoft.Mcp.Core.Commands;
+using Microsoft.Mcp.Core.Models.Command;
+
+namespace Azure.Mcp.Tools.DocumentDb.Commands.Index;
+
+public sealed class DropIndexCommand(ILogger logger)
+ : BaseDocumentDbCommand()
+{
+ private readonly ILogger _logger = logger;
+
+ public override string Id => "c7d8e9f0-a1b2-4c7d-4e5f-6a7b8c9d0e1f";
+
+ public override string Name => "drop_index";
+
+ public override string Description => "Drop an index from a collection";
+
+ public override string Title => "Drop Index";
+
+ public override ToolMetadata Metadata => new()
+ {
+ Destructive = true,
+ Idempotent = false,
+ OpenWorld = false,
+ ReadOnly = false,
+ LocalRequired = false,
+ Secret = false
+ };
+
+ protected override void RegisterOptions(Command command)
+ {
+ base.RegisterOptions(command);
+ command.Options.Add(DocumentDbOptionDefinitions.DbName);
+ command.Options.Add(DocumentDbOptionDefinitions.CollectionName);
+ command.Options.Add(DocumentDbOptionDefinitions.IndexName);
+ }
+
+ protected override DropIndexOptions BindOptions(ParseResult parseResult)
+ {
+ var options = base.BindOptions(parseResult);
+ options.DbName = parseResult.GetValueOrDefault(DocumentDbOptionDefinitions.DbName.Name);
+ options.CollectionName = parseResult.GetValueOrDefault(DocumentDbOptionDefinitions.CollectionName.Name);
+ options.IndexName = parseResult.GetValueOrDefault(DocumentDbOptionDefinitions.IndexName.Name);
+ return options;
+ }
+
+ public override async Task ExecuteAsync(
+ CommandContext context,
+ ParseResult parseResult,
+ CancellationToken cancellationToken)
+ {
+ DropIndexOptions? commandOptions = null;
+
+ try
+ {
+ if (!Validate(parseResult.CommandResult, context.Response).IsValid)
+ {
+ return context.Response;
+ }
+
+ var options = commandOptions = BindOptions(parseResult);
+
+ var service = context.GetService();
+
+ DocumentDbResponse result = await service.DropIndexAsync(options.ConnectionString!, options.DbName!, options.CollectionName!, options.IndexName!, cancellationToken);
+
+ DocumentDbResponseHelper.ProcessResponse(context, result);
+
+ return context.Response;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to drop index: {IndexName} from collection: {CollectionName}, database: {DbName}", commandOptions?.IndexName, commandOptions?.CollectionName, commandOptions?.DbName);
+ HandleException(context, ex);
+ return context.Response;
+ }
+ }
+}
diff --git a/tools/Azure.Mcp.Tools.DocumentDb/src/Commands/Index/IndexStatsCommand.cs b/tools/Azure.Mcp.Tools.DocumentDb/src/Commands/Index/IndexStatsCommand.cs
new file mode 100644
index 0000000000..a0d3714f36
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.DocumentDb/src/Commands/Index/IndexStatsCommand.cs
@@ -0,0 +1,85 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.CommandLine;
+using Azure.Mcp.Core.Commands;
+using Azure.Mcp.Core.Extensions;
+using Azure.Mcp.Tools.DocumentDb.Models;
+using Azure.Mcp.Tools.DocumentDb.Options;
+using Azure.Mcp.Tools.DocumentDb.Services;
+using Microsoft.Extensions.Logging;
+using Microsoft.Mcp.Core.Commands;
+using Microsoft.Mcp.Core.Models.Command;
+
+namespace Azure.Mcp.Tools.DocumentDb.Commands.Index;
+
+public sealed class IndexStatsCommand(ILogger logger)
+ : BaseDocumentDbCommand()
+{
+ private readonly ILogger _logger = logger;
+
+ public override string Id => "d8e9f0a1-b2c3-4d8e-5f6a-7b8c9d0e1f2a";
+
+ public override string Name => "index_stats";
+
+ public override string Description => "Get statistics for indexes on a collection";
+
+ public override string Title => "Index Statistics";
+
+ public override ToolMetadata Metadata => new()
+ {
+ Destructive = false,
+ Idempotent = true,
+ OpenWorld = false,
+ ReadOnly = true,
+ LocalRequired = false,
+ Secret = false
+ };
+
+ protected override void RegisterOptions(Command command)
+ {
+ base.RegisterOptions(command);
+ command.Options.Add(DocumentDbOptionDefinitions.DbName);
+ command.Options.Add(DocumentDbOptionDefinitions.CollectionName);
+ }
+
+ protected override IndexStatsOptions BindOptions(ParseResult parseResult)
+ {
+ var options = base.BindOptions(parseResult);
+ options.DbName = parseResult.GetValueOrDefault(DocumentDbOptionDefinitions.DbName.Name);
+ options.CollectionName = parseResult.GetValueOrDefault(DocumentDbOptionDefinitions.CollectionName.Name);
+ return options;
+ }
+
+ public override async Task ExecuteAsync(
+ CommandContext context,
+ ParseResult parseResult,
+ CancellationToken cancellationToken)
+ {
+ IndexStatsOptions? commandOptions = null;
+
+ try
+ {
+ if (!Validate(parseResult.CommandResult, context.Response).IsValid)
+ {
+ return context.Response;
+ }
+
+ var options = commandOptions = BindOptions(parseResult);
+
+ var service = context.GetService();
+
+ DocumentDbResponse result = await service.GetIndexStatsAsync(options.ConnectionString!, options.DbName!, options.CollectionName!, cancellationToken);
+
+ DocumentDbResponseHelper.ProcessResponse(context, result);
+
+ return context.Response;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to get index statistics for collection: {CollectionName}, database: {DbName}", commandOptions?.CollectionName, commandOptions?.DbName);
+ HandleException(context, ex);
+ return context.Response;
+ }
+ }
+}
diff --git a/tools/Azure.Mcp.Tools.DocumentDb/src/Commands/Index/ListIndexesCommand.cs b/tools/Azure.Mcp.Tools.DocumentDb/src/Commands/Index/ListIndexesCommand.cs
new file mode 100644
index 0000000000..54fd61343a
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.DocumentDb/src/Commands/Index/ListIndexesCommand.cs
@@ -0,0 +1,85 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.CommandLine;
+using Azure.Mcp.Core.Commands;
+using Azure.Mcp.Core.Extensions;
+using Azure.Mcp.Tools.DocumentDb.Models;
+using Azure.Mcp.Tools.DocumentDb.Options;
+using Azure.Mcp.Tools.DocumentDb.Services;
+using Microsoft.Extensions.Logging;
+using Microsoft.Mcp.Core.Commands;
+using Microsoft.Mcp.Core.Models.Command;
+
+namespace Azure.Mcp.Tools.DocumentDb.Commands.Index;
+
+public sealed class ListIndexesCommand(ILogger logger)
+ : BaseDocumentDbCommand()
+{
+ private readonly ILogger _logger = logger;
+
+ public override string Id => "b6c7d8e9-f0a1-4b6c-3d4e-5f6a7b8c9d0e";
+
+ public override string Name => "list_indexes";
+
+ public override string Description => "List all indexes on a collection";
+
+ public override string Title => "List Indexes";
+
+ public override ToolMetadata Metadata => new()
+ {
+ Destructive = false,
+ Idempotent = true,
+ OpenWorld = false,
+ ReadOnly = true,
+ LocalRequired = false,
+ Secret = false
+ };
+
+ protected override void RegisterOptions(Command command)
+ {
+ base.RegisterOptions(command);
+ command.Options.Add(DocumentDbOptionDefinitions.DbName);
+ command.Options.Add(DocumentDbOptionDefinitions.CollectionName);
+ }
+
+ protected override ListIndexesOptions BindOptions(ParseResult parseResult)
+ {
+ var options = base.BindOptions(parseResult);
+ options.DbName = parseResult.GetValueOrDefault(DocumentDbOptionDefinitions.DbName.Name);
+ options.CollectionName = parseResult.GetValueOrDefault(DocumentDbOptionDefinitions.CollectionName.Name);
+ return options;
+ }
+
+ public override async Task ExecuteAsync(
+ CommandContext context,
+ ParseResult parseResult,
+ CancellationToken cancellationToken)
+ {
+ ListIndexesOptions? commandOptions = null;
+
+ try
+ {
+ if (!Validate(parseResult.CommandResult, context.Response).IsValid)
+ {
+ return context.Response;
+ }
+
+ var options = commandOptions = BindOptions(parseResult);
+
+ var service = context.GetService();
+
+ DocumentDbResponse result = await service.ListIndexesAsync(options.ConnectionString!, options.DbName!, options.CollectionName!, cancellationToken);
+
+ DocumentDbResponseHelper.ProcessResponse(context, result);
+
+ return context.Response;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to list indexes on collection: {CollectionName}, database: {DbName}", commandOptions?.CollectionName, commandOptions?.DbName);
+ HandleException(context, ex);
+ return context.Response;
+ }
+ }
+}
diff --git a/tools/Azure.Mcp.Tools.DocumentDb/src/DocumentDbSetup.cs b/tools/Azure.Mcp.Tools.DocumentDb/src/DocumentDbSetup.cs
new file mode 100644
index 0000000000..4ebcd82d95
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.DocumentDb/src/DocumentDbSetup.cs
@@ -0,0 +1,69 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+using Azure.Mcp.Tools.DocumentDb.Commands.Database;
+using Azure.Mcp.Tools.DocumentDb.Commands.Index;
+using Azure.Mcp.Tools.DocumentDb.Services;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Mcp.Core.Areas;
+using Microsoft.Mcp.Core.Commands;
+
+namespace Azure.Mcp.Tools.DocumentDb;
+
+public class DocumentDbSetup : IAreaSetup
+{
+ public string Name => "documentdb";
+ public string Title => "Azure DocumentDB (with MongoDB compatibility)";
+
+ public void ConfigureServices(IServiceCollection services)
+ {
+ services.AddSingleton();
+
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ }
+
+ public CommandGroup RegisterCommands(IServiceProvider serviceProvider)
+ {
+ // Create DocumentDB root command group
+ var documentDb = new CommandGroup(
+ Name,
+ "Azure DocumentDB index, database, and diagnostics operations for Azure DocumentDB.",
+ Title);
+
+ var index = new CommandGroup(
+ "index",
+ "Manage indexes and inspect index-related diagnostics by providing a DocumentDB connection string per request.");
+ var database = new CommandGroup(
+ "database",
+ "Inspect and manage DocumentDB databases by providing a DocumentDB connection string per request.");
+ documentDb.AddSubGroup(index);
+ documentDb.AddSubGroup(database);
+
+ var createIndexCommand = serviceProvider.GetRequiredService();
+ var listIndexesCommand = serviceProvider.GetRequiredService();
+ var dropIndexCommand = serviceProvider.GetRequiredService();
+ var indexStatsCommand = serviceProvider.GetRequiredService();
+ var currentOpsCommand = serviceProvider.GetRequiredService();
+ var listDatabasesCommand = serviceProvider.GetRequiredService();
+ var dbStatsCommand = serviceProvider.GetRequiredService();
+ var dropDatabaseCommand = serviceProvider.GetRequiredService();
+
+ index.AddCommand(createIndexCommand.Name, createIndexCommand);
+ index.AddCommand(listIndexesCommand.Name, listIndexesCommand);
+ index.AddCommand(dropIndexCommand.Name, dropIndexCommand);
+ index.AddCommand(indexStatsCommand.Name, indexStatsCommand);
+ index.AddCommand(currentOpsCommand.Name, currentOpsCommand);
+
+ database.AddCommand(listDatabasesCommand.Name, listDatabasesCommand);
+ database.AddCommand(dbStatsCommand.Name, dbStatsCommand);
+ database.AddCommand(dropDatabaseCommand.Name, dropDatabaseCommand);
+
+ return documentDb;
+ }
+}
diff --git a/tools/Azure.Mcp.Tools.DocumentDb/src/GlobalUsings.cs b/tools/Azure.Mcp.Tools.DocumentDb/src/GlobalUsings.cs
new file mode 100644
index 0000000000..b41cc886b4
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.DocumentDb/src/GlobalUsings.cs
@@ -0,0 +1,4 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+global using System.CommandLine;
diff --git a/tools/Azure.Mcp.Tools.DocumentDb/src/Models/DocumentDbResponse.cs b/tools/Azure.Mcp.Tools.DocumentDb/src/Models/DocumentDbResponse.cs
new file mode 100644
index 0000000000..834143a678
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.DocumentDb/src/Models/DocumentDbResponse.cs
@@ -0,0 +1,32 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Net;
+
+namespace Azure.Mcp.Tools.DocumentDb.Models;
+
+///
+/// Represents a unified response structure for all DocumentDb MCP commands.
+///
+public class DocumentDbResponse
+{
+ ///
+ /// Gets or sets a value indicating whether the operation was successful.
+ ///
+ public bool Success { get; set; }
+
+ ///
+ /// Gets or sets the HTTP status code of the operation.
+ ///
+ public HttpStatusCode StatusCode { get; set; }
+
+ ///
+ /// Gets or sets the message (error or informational) from the operation.
+ ///
+ public string? Message { get; set; }
+
+ ///
+ /// Gets or sets the response data payload from the operation.
+ ///
+ public object? Data { get; set; }
+}
diff --git a/tools/Azure.Mcp.Tools.DocumentDb/src/Options/BaseDocumentDbOptions.cs b/tools/Azure.Mcp.Tools.DocumentDb/src/Options/BaseDocumentDbOptions.cs
new file mode 100644
index 0000000000..f81c87ca5d
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.DocumentDb/src/Options/BaseDocumentDbOptions.cs
@@ -0,0 +1,11 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using Azure.Mcp.Core.Options;
+
+namespace Azure.Mcp.Tools.DocumentDb.Options;
+
+public class BaseDocumentDbOptions : GlobalOptions
+{
+ public string? ConnectionString { get; set; }
+}
diff --git a/tools/Azure.Mcp.Tools.DocumentDb/src/Options/CreateIndexOptions.cs b/tools/Azure.Mcp.Tools.DocumentDb/src/Options/CreateIndexOptions.cs
new file mode 100644
index 0000000000..5260b6857f
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.DocumentDb/src/Options/CreateIndexOptions.cs
@@ -0,0 +1,15 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+namespace Azure.Mcp.Tools.DocumentDb.Options;
+
+public class CreateIndexOptions : BaseDocumentDbOptions
+{
+ public string? DbName { get; set; }
+
+ public string? CollectionName { get; set; }
+
+ public string? Keys { get; set; }
+
+ public string? Options { get; set; }
+}
diff --git a/tools/Azure.Mcp.Tools.DocumentDb/src/Options/CurrentOpsOptions.cs b/tools/Azure.Mcp.Tools.DocumentDb/src/Options/CurrentOpsOptions.cs
new file mode 100644
index 0000000000..fbba9cfd7b
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.DocumentDb/src/Options/CurrentOpsOptions.cs
@@ -0,0 +1,9 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+namespace Azure.Mcp.Tools.DocumentDb.Options;
+
+public class CurrentOpsOptions : BaseDocumentDbOptions
+{
+ public string? Ops { get; set; }
+}
diff --git a/tools/Azure.Mcp.Tools.DocumentDb/src/Options/DbStatsOptions.cs b/tools/Azure.Mcp.Tools.DocumentDb/src/Options/DbStatsOptions.cs
new file mode 100644
index 0000000000..f0ad69a72d
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.DocumentDb/src/Options/DbStatsOptions.cs
@@ -0,0 +1,9 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+namespace Azure.Mcp.Tools.DocumentDb.Options;
+
+public class DbStatsOptions : BaseDocumentDbOptions
+{
+ public string? DbName { get; set; }
+}
diff --git a/tools/Azure.Mcp.Tools.DocumentDb/src/Options/DropDatabaseOptions.cs b/tools/Azure.Mcp.Tools.DocumentDb/src/Options/DropDatabaseOptions.cs
new file mode 100644
index 0000000000..a040ab7317
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.DocumentDb/src/Options/DropDatabaseOptions.cs
@@ -0,0 +1,9 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+namespace Azure.Mcp.Tools.DocumentDb.Options;
+
+public class DropDatabaseOptions : BaseDocumentDbOptions
+{
+ public string? DbName { get; set; }
+}
diff --git a/tools/Azure.Mcp.Tools.DocumentDb/src/Options/DropIndexOptions.cs b/tools/Azure.Mcp.Tools.DocumentDb/src/Options/DropIndexOptions.cs
new file mode 100644
index 0000000000..712fbb2268
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.DocumentDb/src/Options/DropIndexOptions.cs
@@ -0,0 +1,13 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+namespace Azure.Mcp.Tools.DocumentDb.Options;
+
+public class DropIndexOptions : BaseDocumentDbOptions
+{
+ public string? DbName { get; set; }
+
+ public string? CollectionName { get; set; }
+
+ public string? IndexName { get; set; }
+}
diff --git a/tools/Azure.Mcp.Tools.DocumentDb/src/Options/IndexStatsOptions.cs b/tools/Azure.Mcp.Tools.DocumentDb/src/Options/IndexStatsOptions.cs
new file mode 100644
index 0000000000..fba3ad8c63
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.DocumentDb/src/Options/IndexStatsOptions.cs
@@ -0,0 +1,11 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+namespace Azure.Mcp.Tools.DocumentDb.Options;
+
+public class IndexStatsOptions : BaseDocumentDbOptions
+{
+ public string? DbName { get; set; }
+
+ public string? CollectionName { get; set; }
+}
diff --git a/tools/Azure.Mcp.Tools.DocumentDb/src/Options/ListDatabasesOptions.cs b/tools/Azure.Mcp.Tools.DocumentDb/src/Options/ListDatabasesOptions.cs
new file mode 100644
index 0000000000..2195f39e25
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.DocumentDb/src/Options/ListDatabasesOptions.cs
@@ -0,0 +1,9 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+namespace Azure.Mcp.Tools.DocumentDb.Options;
+
+public class ListDatabasesOptions : BaseDocumentDbOptions
+{
+ public string? DbName { get; set; }
+}
diff --git a/tools/Azure.Mcp.Tools.DocumentDb/src/Options/ListIndexesOptions.cs b/tools/Azure.Mcp.Tools.DocumentDb/src/Options/ListIndexesOptions.cs
new file mode 100644
index 0000000000..582e715843
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.DocumentDb/src/Options/ListIndexesOptions.cs
@@ -0,0 +1,11 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+namespace Azure.Mcp.Tools.DocumentDb.Options;
+
+public class ListIndexesOptions : BaseDocumentDbOptions
+{
+ public string? DbName { get; set; }
+
+ public string? CollectionName { get; set; }
+}
diff --git a/tools/Azure.Mcp.Tools.DocumentDb/src/Services/DocumentDbService.cs b/tools/Azure.Mcp.Tools.DocumentDb/src/Services/DocumentDbService.cs
new file mode 100644
index 0000000000..4268ea30b5
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.DocumentDb/src/Services/DocumentDbService.cs
@@ -0,0 +1,498 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Net;
+using Azure.Mcp.Tools.DocumentDb.Models;
+using Microsoft.Extensions.Logging;
+using MongoDB.Bson;
+using MongoDB.Bson.IO;
+using MongoDB.Driver;
+
+namespace Azure.Mcp.Tools.DocumentDb.Services;
+
+public sealed class DocumentDbService(ILogger logger) : IDocumentDbService
+{
+ private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ private static readonly JsonWriterSettings s_jsonWriterSettings = new() { OutputMode = JsonOutputMode.RelaxedExtendedJson };
+
+ #region Index Management
+
+ public async Task CreateIndexAsync(string connectionString, string databaseName, string collectionName, BsonDocument keys, BsonDocument? options = null, CancellationToken cancellationToken = default)
+ {
+ ValidateParameter(connectionString, nameof(connectionString));
+ ValidateParameter(databaseName, nameof(databaseName));
+ ValidateParameter(collectionName, nameof(collectionName));
+ ArgumentNullException.ThrowIfNull(keys);
+
+ try
+ {
+ var collection = GetCollection(connectionString, databaseName, collectionName);
+ var createIndexOptions = CreateIndexOptions(options);
+ var model = new CreateIndexModel(new BsonDocumentIndexKeysDefinition(keys), createIndexOptions);
+ var indexName = await collection.Indexes.CreateOneAsync(model, cancellationToken: cancellationToken);
+
+ _logger.LogInformation("Created index {IndexName} on {DatabaseName}.{CollectionName}", indexName, databaseName, collectionName);
+
+ return Success(
+ "Index created successfully",
+ new Dictionary
+ {
+ ["index_name"] = indexName,
+ ["keys"] = BsonDocumentToJson(keys),
+ ["options"] = BsonDocumentToJson(options)
+ });
+ }
+ catch (MongoCommandException ex) when (ex.Code == 26)
+ {
+ _logger.LogWarning("Database '{DatabaseName}' not found", databaseName);
+ return Failure(HttpStatusCode.BadRequest, $"Database '{databaseName}' not found");
+ }
+ catch (MongoCommandException ex) when (ex.CodeName == "NamespaceNotFound")
+ {
+ _logger.LogWarning("Collection '{CollectionName}' not found in database '{DatabaseName}'", collectionName, databaseName);
+ return Failure(HttpStatusCode.BadRequest, $"Collection '{collectionName}' not found");
+ }
+ catch (MongoAuthenticationException ex)
+ {
+ _logger.LogWarning(ex, "Unauthorized access creating index on {DatabaseName}.{CollectionName}", databaseName, collectionName);
+ return Failure(HttpStatusCode.Unauthorized, $"Unauthorized access: {ex.Message}");
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error creating index on {DatabaseName}.{CollectionName}", databaseName, collectionName);
+ return Failure(HttpStatusCode.InternalServerError, $"Failed to create index: {ex.Message}");
+ }
+ }
+
+ public async Task ListIndexesAsync(string connectionString, string databaseName, string collectionName, CancellationToken cancellationToken = default)
+ {
+ ValidateParameter(connectionString, nameof(connectionString));
+ ValidateParameter(databaseName, nameof(databaseName));
+ ValidateParameter(collectionName, nameof(collectionName));
+
+ try
+ {
+ var collection = GetCollection(connectionString, databaseName, collectionName);
+ var indexes = await collection.Indexes.List(cancellationToken: cancellationToken).ToListAsync(cancellationToken);
+
+ return Success(
+ "Indexes retrieved successfully",
+ new Dictionary
+ {
+ ["indexes"] = BsonDocumentListToJson(indexes),
+ ["count"] = indexes.Count
+ });
+ }
+ catch (MongoCommandException ex) when (ex.Code == 26)
+ {
+ _logger.LogWarning("Database '{DatabaseName}' not found", databaseName);
+ return Failure(HttpStatusCode.BadRequest, $"Database '{databaseName}' not found");
+ }
+ catch (MongoCommandException ex) when (ex.CodeName == "NamespaceNotFound")
+ {
+ _logger.LogWarning("Collection '{CollectionName}' not found in database '{DatabaseName}'", collectionName, databaseName);
+ return Failure(HttpStatusCode.BadRequest, $"Collection '{collectionName}' not found");
+ }
+ catch (MongoAuthenticationException ex)
+ {
+ _logger.LogWarning(ex, "Unauthorized access listing indexes for {DatabaseName}.{CollectionName}", databaseName, collectionName);
+ return Failure(HttpStatusCode.Unauthorized, $"Unauthorized access: {ex.Message}");
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error listing indexes for {DatabaseName}.{CollectionName}", databaseName, collectionName);
+ return Failure(HttpStatusCode.InternalServerError, $"Failed to list indexes: {ex.Message}");
+ }
+ }
+
+ public async Task DropIndexAsync(string connectionString, string databaseName, string collectionName, string indexName, CancellationToken cancellationToken = default)
+ {
+ ValidateParameter(connectionString, nameof(connectionString));
+ ValidateParameter(databaseName, nameof(databaseName));
+ ValidateParameter(collectionName, nameof(collectionName));
+ ValidateParameter(indexName, nameof(indexName));
+
+ try
+ {
+ var collection = GetCollection(connectionString, databaseName, collectionName);
+ await collection.Indexes.DropOneAsync(indexName, cancellationToken: cancellationToken);
+
+ _logger.LogInformation("Dropped index {IndexName} from {DatabaseName}.{CollectionName}", indexName, databaseName, collectionName);
+
+ return Success(
+ $"Index '{indexName}' dropped successfully",
+ new Dictionary
+ {
+ ["index_name"] = indexName
+ });
+ }
+ catch (MongoCommandException ex) when (ex.Code == 26)
+ {
+ _logger.LogWarning("Database '{DatabaseName}' not found", databaseName);
+ return Failure(HttpStatusCode.BadRequest, $"Database '{databaseName}' not found");
+ }
+ catch (MongoCommandException ex) when (ex.CodeName == "NamespaceNotFound")
+ {
+ _logger.LogWarning("Collection '{CollectionName}' not found in database '{DatabaseName}'", collectionName, databaseName);
+ return Failure(HttpStatusCode.BadRequest, $"Collection '{collectionName}' not found");
+ }
+ catch (MongoCommandException ex) when (ex.CodeName == "IndexNotFound")
+ {
+ _logger.LogWarning("Index '{IndexName}' not found in {DatabaseName}.{CollectionName}", indexName, databaseName, collectionName);
+ return Failure(HttpStatusCode.BadRequest, $"Index '{indexName}' not found");
+ }
+ catch (MongoAuthenticationException ex)
+ {
+ _logger.LogWarning(ex, "Unauthorized access dropping index {IndexName} from {DatabaseName}.{CollectionName}", indexName, databaseName, collectionName);
+ return Failure(HttpStatusCode.Unauthorized, $"Unauthorized access: {ex.Message}");
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error dropping index {IndexName} from {DatabaseName}.{CollectionName}", indexName, databaseName, collectionName);
+ return Failure(HttpStatusCode.InternalServerError, $"Failed to drop index: {ex.Message}");
+ }
+ }
+
+ public async Task GetIndexStatsAsync(string connectionString, string databaseName, string collectionName, CancellationToken cancellationToken = default)
+ {
+ ValidateParameter(connectionString, nameof(connectionString));
+ ValidateParameter(databaseName, nameof(databaseName));
+ ValidateParameter(collectionName, nameof(collectionName));
+
+ try
+ {
+ var collection = GetCollection(connectionString, databaseName, collectionName);
+ var pipeline = new[]
+ {
+ new BsonDocument("$indexStats", new BsonDocument())
+ };
+
+ var stats = await collection.Aggregate(pipeline, cancellationToken: cancellationToken).ToListAsync(cancellationToken);
+
+ return Success(
+ "Index statistics retrieved successfully",
+ new Dictionary
+ {
+ ["stats"] = BsonDocumentListToJson(stats),
+ ["count"] = stats.Count
+ });
+ }
+ catch (MongoCommandException ex) when (ex.Code == 26)
+ {
+ _logger.LogWarning("Database '{DatabaseName}' not found", databaseName);
+ return Failure(HttpStatusCode.BadRequest, $"Database '{databaseName}' not found");
+ }
+ catch (MongoCommandException ex) when (ex.CodeName == "NamespaceNotFound")
+ {
+ _logger.LogWarning("Collection '{CollectionName}' not found in database '{DatabaseName}'", collectionName, databaseName);
+ return Failure(HttpStatusCode.BadRequest, $"Collection '{collectionName}' not found");
+ }
+ catch (MongoAuthenticationException ex)
+ {
+ _logger.LogWarning(ex, "Unauthorized access getting index stats for {DatabaseName}.{CollectionName}", databaseName, collectionName);
+ return Failure(HttpStatusCode.Unauthorized, $"Unauthorized access: {ex.Message}");
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error getting index stats for {DatabaseName}.{CollectionName}", databaseName, collectionName);
+ return Failure(HttpStatusCode.InternalServerError, $"Failed to get index stats: {ex.Message}");
+ }
+ }
+
+ public async Task GetCurrentOpsAsync(string connectionString, BsonDocument? filter = null, CancellationToken cancellationToken = default)
+ {
+ ValidateParameter(connectionString, nameof(connectionString));
+
+ try
+ {
+ var adminDb = CreateClient(connectionString).GetDatabase("admin");
+ var command = new BsonDocument("currentOp", 1);
+
+ if (filter != null && filter.ElementCount > 0)
+ {
+ foreach (var element in filter)
+ {
+ if (string.Equals(element.Name, "currentOp", StringComparison.Ordinal))
+ {
+ return Failure(HttpStatusCode.BadRequest, "The 'currentOp' filter field is reserved and cannot be overridden.");
+ }
+
+ command[element.Name] = element.Value;
+ }
+ }
+
+ var result = await adminDb.RunCommandAsync(command, cancellationToken: cancellationToken);
+
+ return Success(
+ "Current operations retrieved successfully",
+ new Dictionary
+ {
+ ["operations"] = BsonDocumentToJson(result)
+ });
+ }
+ catch (MongoCommandException ex) when (ex.Code == 26)
+ {
+ _logger.LogWarning("Admin database not found");
+ return Failure(HttpStatusCode.BadRequest, "Admin database not found");
+ }
+ catch (MongoCommandException ex) when (ex.CodeName == "NamespaceNotFound")
+ {
+ _logger.LogWarning("Namespace not found for current operations");
+ return Failure(HttpStatusCode.BadRequest, "Namespace not found");
+ }
+ catch (MongoAuthenticationException ex)
+ {
+ _logger.LogWarning(ex, "Unauthorized access getting current operations");
+ return Failure(HttpStatusCode.Unauthorized, $"Unauthorized access: {ex.Message}");
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error getting current operations");
+ return Failure(HttpStatusCode.InternalServerError, $"Failed to get current operations: {ex.Message}");
+ }
+ }
+
+ #endregion
+
+ #region Database Management
+
+ public async Task GetDatabasesAsync(string connectionString, string? dbName = null, CancellationToken cancellationToken = default)
+ {
+ ValidateParameter(connectionString, nameof(connectionString));
+
+ try
+ {
+ var client = CreateClient(connectionString);
+ var databaseNames = await client.ListDatabaseNames(cancellationToken: cancellationToken).ToListAsync(cancellationToken);
+
+ if (!string.IsNullOrWhiteSpace(dbName) && !databaseNames.Contains(dbName, StringComparer.Ordinal))
+ {
+ return Failure(HttpStatusCode.NotFound, $"Database '{dbName}' was not found.");
+ }
+
+ List> databases;
+ if (string.IsNullOrWhiteSpace(dbName))
+ {
+ databases = databaseNames
+ .Select(databaseName => new Dictionary
+ {
+ ["name"] = databaseName
+ })
+ .ToList();
+ }
+ else
+ {
+ databases = [await GetDatabaseInfoAsync(client, dbName, cancellationToken)];
+ }
+
+ return Success(
+ string.IsNullOrWhiteSpace(dbName)
+ ? "Databases retrieved successfully."
+ : $"Database '{dbName}' retrieved successfully.",
+ databases);
+ }
+ catch (MongoAuthenticationException ex)
+ {
+ _logger.LogWarning(ex, "Unauthorized access listing databases");
+ return Failure(HttpStatusCode.Unauthorized, $"Unauthorized access: {ex.Message}");
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error listing databases. Database: {DatabaseName}", dbName);
+ return Failure(HttpStatusCode.InternalServerError, $"Failed to list databases: {ex.Message}");
+ }
+ }
+
+ public async Task GetDatabaseStatsAsync(string connectionString, string dbName, CancellationToken cancellationToken = default)
+ {
+ ValidateParameter(connectionString, nameof(connectionString));
+ ValidateParameter(dbName, nameof(dbName));
+
+ try
+ {
+ var client = CreateClient(connectionString);
+
+ if (!await DatabaseExistsAsync(client, dbName, cancellationToken))
+ {
+ return Failure(HttpStatusCode.NotFound, $"Database '{dbName}' was not found.");
+ }
+
+ var database = client.GetDatabase(dbName);
+ var stats = await database.RunCommandAsync(new BsonDocument("dbStats", 1), cancellationToken: cancellationToken);
+
+ return Success($"Database statistics for '{dbName}' retrieved successfully.", stats);
+ }
+ catch (MongoAuthenticationException ex)
+ {
+ _logger.LogWarning(ex, "Unauthorized access getting stats for database {DatabaseName}", dbName);
+ return Failure(HttpStatusCode.Unauthorized, $"Unauthorized access: {ex.Message}");
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error getting database stats for {DatabaseName}", dbName);
+ return Failure(HttpStatusCode.InternalServerError, $"Failed to get database stats: {ex.Message}");
+ }
+ }
+
+ public async Task DropDatabaseAsync(string connectionString, string dbName, CancellationToken cancellationToken = default)
+ {
+ ValidateParameter(connectionString, nameof(connectionString));
+ ValidateParameter(dbName, nameof(dbName));
+
+ try
+ {
+ var client = CreateClient(connectionString);
+
+ if (!await DatabaseExistsAsync(client, dbName, cancellationToken))
+ {
+ return Failure(HttpStatusCode.NotFound, $"Database '{dbName}' was not found.");
+ }
+
+ await client.DropDatabaseAsync(dbName, cancellationToken);
+
+ _logger.LogInformation("Dropped DocumentDB database {DatabaseName}", dbName);
+
+ return Success(
+ $"Database '{dbName}' dropped successfully.",
+ new Dictionary
+ {
+ ["name"] = dbName,
+ ["deleted"] = true
+ });
+ }
+ catch (MongoAuthenticationException ex)
+ {
+ _logger.LogWarning(ex, "Unauthorized access dropping database {DatabaseName}", dbName);
+ return Failure(HttpStatusCode.Unauthorized, $"Unauthorized access: {ex.Message}");
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error dropping database {DatabaseName}", dbName);
+ return Failure(HttpStatusCode.InternalServerError, $"Failed to drop database: {ex.Message}");
+ }
+ }
+
+ #endregion
+
+ #region Helper Functions
+
+ private static async Task DatabaseExistsAsync(MongoClient client, string dbName, CancellationToken cancellationToken)
+ {
+ var databaseNames = await client.ListDatabaseNames(cancellationToken: cancellationToken).ToListAsync(cancellationToken);
+ return databaseNames.Contains(dbName, StringComparer.Ordinal);
+ }
+
+ private static async Task> GetDatabaseInfoAsync(MongoClient client, string dbName, CancellationToken cancellationToken)
+ {
+ var database = client.GetDatabase(dbName);
+ var collectionNames = await database.ListCollectionNames(cancellationToken: cancellationToken).ToListAsync(cancellationToken);
+
+ var collections = new List>(collectionNames.Count);
+ foreach (var collectionName in collectionNames)
+ {
+ var collection = database.GetCollection(collectionName);
+ var documentCount = await collection.CountDocumentsAsync(FilterDefinition.Empty, cancellationToken: cancellationToken);
+
+ collections.Add(new Dictionary
+ {
+ ["name"] = collectionName,
+ ["documentCount"] = documentCount
+ });
+ }
+
+ return new Dictionary
+ {
+ ["name"] = dbName,
+ ["collectionCount"] = collectionNames.Count,
+ ["collections"] = collections
+ };
+ }
+
+ private static IMongoCollection GetCollection(string connectionString, string databaseName, string collectionName)
+ {
+ return CreateClient(connectionString)
+ .GetDatabase(databaseName)
+ .GetCollection(collectionName);
+ }
+
+ private static MongoClient CreateClient(string connectionString)
+ {
+ var settings = MongoClientSettings.FromConnectionString(connectionString);
+ settings.ServerSelectionTimeout = TimeSpan.FromSeconds(10);
+ return new MongoClient(settings);
+ }
+
+ private static CreateIndexOptions CreateIndexOptions(BsonDocument? options)
+ {
+ var createIndexOptions = new CreateIndexOptions();
+
+ if (options == null)
+ {
+ return createIndexOptions;
+ }
+
+ if (options.Contains("unique"))
+ {
+ createIndexOptions.Unique = options["unique"].AsBoolean;
+ }
+
+ if (options.Contains("name"))
+ {
+ createIndexOptions.Name = options["name"].AsString;
+ }
+
+ if (options.Contains("sparse"))
+ {
+ createIndexOptions.Sparse = options["sparse"].AsBoolean;
+ }
+
+ if (options.Contains("expireAfterSeconds"))
+ {
+ createIndexOptions.ExpireAfter = TimeSpan.FromSeconds(options["expireAfterSeconds"].ToInt32());
+ }
+
+ return createIndexOptions;
+ }
+
+ private static string? BsonDocumentToJson(BsonDocument? doc)
+ {
+ return doc?.ToJson(s_jsonWriterSettings);
+ }
+
+ private static List BsonDocumentListToJson(List docs)
+ {
+ return docs.Select(doc => doc.ToJson(s_jsonWriterSettings)).ToList();
+ }
+
+ private static DocumentDbResponse Success(string message, object? data = null)
+ {
+ return new DocumentDbResponse
+ {
+ Success = true,
+ StatusCode = HttpStatusCode.OK,
+ Message = message,
+ Data = data
+ };
+ }
+
+ private static DocumentDbResponse Failure(HttpStatusCode statusCode, string message)
+ {
+ return new DocumentDbResponse
+ {
+ Success = false,
+ StatusCode = statusCode,
+ Message = message,
+ Data = null
+ };
+ }
+
+ private static void ValidateParameter(string? value, string paramName)
+ {
+ if (string.IsNullOrWhiteSpace(value))
+ {
+ throw new ArgumentException($"{paramName} cannot be null or empty", paramName);
+ }
+ }
+
+ #endregion
+}
diff --git a/tools/Azure.Mcp.Tools.DocumentDb/src/Services/IDocumentDbService.cs b/tools/Azure.Mcp.Tools.DocumentDb/src/Services/IDocumentDbService.cs
new file mode 100644
index 0000000000..75a436412e
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.DocumentDb/src/Services/IDocumentDbService.cs
@@ -0,0 +1,22 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using Azure.Mcp.Tools.DocumentDb.Models;
+using MongoDB.Bson;
+
+namespace Azure.Mcp.Tools.DocumentDb.Services;
+
+public interface IDocumentDbService
+{
+ // Index Management
+ Task CreateIndexAsync(string connectionString, string databaseName, string collectionName, BsonDocument keys, BsonDocument? options = null, CancellationToken cancellationToken = default);
+ Task ListIndexesAsync(string connectionString, string databaseName, string collectionName, CancellationToken cancellationToken = default);
+ Task DropIndexAsync(string connectionString, string databaseName, string collectionName, string indexName, CancellationToken cancellationToken = default);
+ Task GetIndexStatsAsync(string connectionString, string databaseName, string collectionName, CancellationToken cancellationToken = default);
+ Task GetCurrentOpsAsync(string connectionString, BsonDocument? filter = null, CancellationToken cancellationToken = default);
+
+ // Database Management
+ Task GetDatabasesAsync(string connectionString, string? dbName = null, CancellationToken cancellationToken = default);
+ Task GetDatabaseStatsAsync(string connectionString, string dbName, CancellationToken cancellationToken = default);
+ Task DropDatabaseAsync(string connectionString, string dbName, CancellationToken cancellationToken = default);
+}
diff --git a/tools/Azure.Mcp.Tools.DocumentDb/tests/Azure.Mcp.Tools.DocumentDb.LiveTests/Azure.Mcp.Tools.DocumentDb.LiveTests.csproj b/tools/Azure.Mcp.Tools.DocumentDb/tests/Azure.Mcp.Tools.DocumentDb.LiveTests/Azure.Mcp.Tools.DocumentDb.LiveTests.csproj
new file mode 100644
index 0000000000..fa0e6b88e4
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.DocumentDb/tests/Azure.Mcp.Tools.DocumentDb.LiveTests/Azure.Mcp.Tools.DocumentDb.LiveTests.csproj
@@ -0,0 +1,17 @@
+
+
+ true
+ Exe
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tools/Azure.Mcp.Tools.DocumentDb/tests/Azure.Mcp.Tools.DocumentDb.LiveTests/DocumentDbCommandTests.cs b/tools/Azure.Mcp.Tools.DocumentDb/tests/Azure.Mcp.Tools.DocumentDb.LiveTests/DocumentDbCommandTests.cs
new file mode 100644
index 0000000000..eb60be2a9b
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.DocumentDb/tests/Azure.Mcp.Tools.DocumentDb.LiveTests/DocumentDbCommandTests.cs
@@ -0,0 +1,292 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Text.Json;
+using Microsoft.Mcp.Tests;
+using Microsoft.Mcp.Tests.Client;
+using MongoDB.Bson;
+using MongoDB.Driver;
+using Xunit;
+
+namespace Azure.Mcp.Tools.DocumentDb.LiveTests;
+
+public class DocumentDbCommandTests(ITestOutputHelper output, LiveServerFixture serverFixture)
+ : CommandTestsBase(output, serverFixture)
+{
+ private const string TestDatabaseName = "test";
+ private const string CollectionName = "items";
+ private static bool _testDataInitialized;
+ private static readonly SemaphoreSlim InitLock = new(1, 1);
+
+ private string ConnectionString => Settings.DeploymentOutputs["DOCUMENTDB_CONNECTION_STRING"];
+
+ public override async ValueTask InitializeAsync()
+ {
+ await LoadSettingsAsync();
+
+ Assert.SkipWhen(TestMode != Microsoft.Mcp.Tests.Helpers.TestMode.Live,
+ "DocumentDb index tests are live-only and do not support record/playback mode");
+
+ SetArguments("server", "start", "--mode", "all", "--dangerously-disable-elicitation");
+ await base.InitializeAsync();
+
+ if (_testDataInitialized)
+ {
+ return;
+ }
+
+ await InitLock.WaitAsync();
+
+ try
+ {
+ if (_testDataInitialized)
+ {
+ return;
+ }
+
+ await SeedTestDatabaseAsync();
+ _testDataInitialized = true;
+ }
+ finally
+ {
+ InitLock.Release();
+ }
+ }
+
+ [Fact]
+ public async Task Should_list_indexes_with_connection_string()
+ {
+ var result = await CallToolAsync(
+ "documentdb_index_list_indexes",
+ new()
+ {
+ { "connection-string", ConnectionString },
+ { "db-name", TestDatabaseName },
+ { "collection-name", CollectionName }
+ });
+
+ var indexesArray = result.AssertProperty("indexes");
+ Assert.Equal(JsonValueKind.Array, indexesArray.ValueKind);
+ Assert.NotEmpty(indexesArray.EnumerateArray());
+ }
+
+ [Fact]
+ public async Task Should_create_and_drop_index_with_connection_string()
+ {
+ var indexName = $"value_1_mcp_{Guid.NewGuid():N}";
+
+ var createResult = await CallToolAsync(
+ "documentdb_index_create_index",
+ new()
+ {
+ { "connection-string", ConnectionString },
+ { "db-name", TestDatabaseName },
+ { "collection-name", CollectionName },
+ { "keys", "{\"value\":1}" },
+ { "options", $"{{\"name\":\"{indexName}\"}}" }
+ });
+
+ Assert.Equal(indexName, createResult.AssertProperty("index_name").GetString());
+
+ var listResult = await CallToolAsync(
+ "documentdb_index_list_indexes",
+ new()
+ {
+ { "connection-string", ConnectionString },
+ { "db-name", TestDatabaseName },
+ { "collection-name", CollectionName }
+ });
+
+ Assert.Contains(listResult.AssertProperty("indexes").EnumerateArray(), element =>
+ element.GetString()?.Contains(indexName, StringComparison.Ordinal) == true);
+
+ var dropResult = await CallToolAsync(
+ "documentdb_index_drop_index",
+ new()
+ {
+ { "connection-string", ConnectionString },
+ { "db-name", TestDatabaseName },
+ { "collection-name", CollectionName },
+ { "index-name", indexName }
+ });
+
+ Assert.Equal(indexName, dropResult.AssertProperty("index_name").GetString());
+ }
+
+ [Fact]
+ public async Task Should_get_index_stats_with_connection_string()
+ {
+ var indexName = $"category_1_mcp_{Guid.NewGuid():N}";
+
+ await CallToolAsync(
+ "documentdb_index_create_index",
+ new()
+ {
+ { "connection-string", ConnectionString },
+ { "db-name", TestDatabaseName },
+ { "collection-name", CollectionName },
+ { "keys", "{\"category\":1}" },
+ { "options", $"{{\"name\":\"{indexName}\"}}" }
+ });
+
+ var statsResult = await CallToolAsync(
+ "documentdb_index_index_stats",
+ new()
+ {
+ { "connection-string", ConnectionString },
+ { "db-name", TestDatabaseName },
+ { "collection-name", CollectionName }
+ });
+
+ var stats = statsResult.AssertProperty("stats");
+ Assert.Equal(JsonValueKind.Array, stats.ValueKind);
+ Assert.True(stats.EnumerateArray().Any());
+
+ await CallToolAsync(
+ "documentdb_index_drop_index",
+ new()
+ {
+ { "connection-string", ConnectionString },
+ { "db-name", TestDatabaseName },
+ { "collection-name", CollectionName },
+ { "index-name", indexName }
+ });
+ }
+
+ [Fact]
+ public async Task Should_list_all_databases()
+ {
+ var result = await CallToolAsync(
+ "documentdb_database_list_databases",
+ new()
+ {
+ { "connection-string", ConnectionString }
+ });
+
+ Assert.NotNull(result);
+ Assert.Equal(JsonValueKind.Array, result.Value.ValueKind);
+ Assert.NotEmpty(result.Value.EnumerateArray());
+
+ foreach (var database in result.Value.EnumerateArray())
+ {
+ var name = database.AssertProperty("name");
+ Assert.False(string.IsNullOrWhiteSpace(name.GetString()));
+ }
+ }
+
+ [Fact]
+ public async Task Should_get_single_database_details_when_db_name_is_provided()
+ {
+ var result = await CallToolAsync(
+ "documentdb_database_list_databases",
+ new()
+ {
+ { "connection-string", ConnectionString },
+ { "db-name", "test" }
+ });
+
+ Assert.NotNull(result);
+ Assert.Equal(JsonValueKind.Array, result.Value.ValueKind);
+
+ var database = Assert.Single(result.Value.EnumerateArray());
+ var name = database.AssertProperty("name");
+ Assert.Equal("test", name.GetString());
+
+ var collectionCount = database.AssertProperty("collectionCount");
+ Assert.True(collectionCount.GetInt32() >= 1);
+
+ var collections = database.AssertProperty("collections");
+ Assert.Equal(JsonValueKind.Array, collections.ValueKind);
+ Assert.NotEmpty(collections.EnumerateArray());
+ }
+
+ [Fact]
+ public async Task Should_get_database_statistics()
+ {
+ var result = await CallToolAsync(
+ "documentdb_database_db_stats",
+ new()
+ {
+ { "connection-string", ConnectionString },
+ { "db-name", "test" }
+ });
+
+ Assert.NotNull(result);
+
+ var database = result.Value.AssertProperty("db");
+ Assert.Equal("test", database.GetString());
+
+ var collections = result.Value.AssertProperty("collections");
+ Assert.True(collections.GetInt32() >= 1);
+ }
+
+ [Fact]
+ public async Task Should_drop_database()
+ {
+ const string databaseName = "dropme";
+
+ var result = await CallToolAsync(
+ "documentdb_database_drop_database",
+ new()
+ {
+ { "connection-string", ConnectionString },
+ { "db-name", databaseName }
+ });
+
+ Assert.NotNull(result);
+
+ var name = result.Value.AssertProperty("name");
+ Assert.Equal(databaseName, name.GetString());
+
+ var deleted = result.Value.AssertProperty("deleted");
+ Assert.True(deleted.GetBoolean());
+ }
+
+ private async Task SeedTestDatabaseAsync()
+ {
+ const int maxAttempts = 3;
+ Exception? lastException = null;
+
+ for (var attempt = 1; attempt <= maxAttempts; attempt++)
+ {
+ try
+ {
+ Output.WriteLine($"Seeding DocumentDB index test data (attempt {attempt}/{maxAttempts})...");
+
+ var client = new MongoClient(ConnectionString);
+ var database = client.GetDatabase(TestDatabaseName);
+
+ var existingCollections = await (await database.ListCollectionNamesAsync()).ToListAsync();
+ if (!existingCollections.Contains(CollectionName, StringComparer.Ordinal))
+ {
+ await database.CreateCollectionAsync(CollectionName);
+ }
+
+ var collection = database.GetCollection(CollectionName);
+ await collection.DeleteManyAsync(Builders.Filter.Empty);
+ await collection.InsertManyAsync([
+ new BsonDocument { { "name", "item1" }, { "value", 100 }, { "category", "A" } },
+ new BsonDocument { { "name", "item2" }, { "value", 200 }, { "category", "B" } },
+ new BsonDocument { { "name", "item3" }, { "value", 300 }, { "category", "A" } }
+ ]);
+
+ Output.WriteLine("DocumentDB index test data seeded successfully.");
+ return;
+ }
+ catch (Exception ex)
+ {
+ lastException = ex;
+
+ if (attempt == maxAttempts)
+ {
+ break;
+ }
+
+ Output.WriteLine($"DocumentDB seeding attempt {attempt} failed: {ex.Message}");
+ await Task.Delay(TimeSpan.FromSeconds(10));
+ }
+ }
+
+ throw new InvalidOperationException("Failed to seed DocumentDB index test database.", lastException);
+ }
+}
\ No newline at end of file
diff --git a/tools/Azure.Mcp.Tools.DocumentDb/tests/Azure.Mcp.Tools.DocumentDb.LiveTests/assets.json b/tools/Azure.Mcp.Tools.DocumentDb/tests/Azure.Mcp.Tools.DocumentDb.LiveTests/assets.json
new file mode 100644
index 0000000000..f838934cca
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.DocumentDb/tests/Azure.Mcp.Tools.DocumentDb.LiveTests/assets.json
@@ -0,0 +1,6 @@
+{
+ "AssetsRepo": "Azure/azure-sdk-assets",
+ "AssetsRepoPrefixPath": "",
+ "TagPrefix": "Azure.Mcp.Tools.DocumentDb.LiveTests",
+ "Tag": ""
+}
\ No newline at end of file
diff --git a/tools/Azure.Mcp.Tools.DocumentDb/tests/Azure.Mcp.Tools.DocumentDb.UnitTests/Azure.Mcp.Tools.DocumentDb.UnitTests.csproj b/tools/Azure.Mcp.Tools.DocumentDb/tests/Azure.Mcp.Tools.DocumentDb.UnitTests/Azure.Mcp.Tools.DocumentDb.UnitTests.csproj
new file mode 100644
index 0000000000..7b8c62121b
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.DocumentDb/tests/Azure.Mcp.Tools.DocumentDb.UnitTests/Azure.Mcp.Tools.DocumentDb.UnitTests.csproj
@@ -0,0 +1,17 @@
+
+
+ true
+ Exe
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tools/Azure.Mcp.Tools.DocumentDb/tests/Azure.Mcp.Tools.DocumentDb.UnitTests/Database/DbStatsCommandTests.cs b/tools/Azure.Mcp.Tools.DocumentDb/tests/Azure.Mcp.Tools.DocumentDb.UnitTests/Database/DbStatsCommandTests.cs
new file mode 100644
index 0000000000..9831de4eaa
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.DocumentDb/tests/Azure.Mcp.Tools.DocumentDb.UnitTests/Database/DbStatsCommandTests.cs
@@ -0,0 +1,149 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.CommandLine;
+using System.Net;
+using Azure.Mcp.Tools.DocumentDb.Commands.Database;
+using Azure.Mcp.Tools.DocumentDb.Models;
+using Azure.Mcp.Tools.DocumentDb.Services;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Mcp.Core.Models.Command;
+using MongoDB.Bson;
+using NSubstitute;
+using Xunit;
+
+namespace Azure.Mcp.Tools.DocumentDb.UnitTests.Database;
+
+public class DbStatsCommandTests
+{
+ private const string ConnectionString = "mongodb://localhost:27017";
+
+ private readonly IServiceProvider _serviceProvider;
+ private readonly IDocumentDbService _documentDbService;
+ private readonly ILogger _logger;
+ private readonly DbStatsCommand _command;
+ private readonly CommandContext _context;
+ private readonly Command _commandDefinition;
+
+ public DbStatsCommandTests()
+ {
+ _documentDbService = Substitute.For();
+ _logger = Substitute.For>();
+ _command = new(_logger);
+ _commandDefinition = _command.GetCommand();
+ _serviceProvider = new ServiceCollection()
+ .AddSingleton(_documentDbService)
+ .BuildServiceProvider();
+ _context = new(_serviceProvider);
+ }
+
+ [Fact]
+ public async Task ExecuteAsync_ReturnsDbStats_WhenDatabaseExists()
+ {
+ // Arrange
+ var dbName = "testdb";
+ var stats = new BsonDocument
+ {
+ { "db", dbName },
+ { "collections", 5 },
+ { "views", 0 },
+ { "objects", 1000 },
+ { "avgObjSize", 512.5 },
+ { "dataSize", 512500 },
+ { "storageSize", 1048576 },
+ { "indexes", 10 },
+ { "indexSize", 204800 }
+ };
+
+ _documentDbService.GetDatabaseStatsAsync(
+ Arg.Is(ConnectionString),
+ Arg.Is(dbName),
+ Arg.Any())
+ .Returns(new DocumentDbResponse
+ {
+ Success = true,
+ StatusCode = HttpStatusCode.OK,
+ Message = "Database statistics retrieved successfully",
+ Data = stats
+ });
+
+ var args = _commandDefinition.Parse(["--connection-string", ConnectionString, "--db-name", dbName]);
+
+ // Act
+ var response = await _command.ExecuteAsync(_context, args, TestContext.Current.CancellationToken);
+
+ // Assert
+ Assert.NotNull(response);
+ Assert.Equal(HttpStatusCode.OK, response.Status);
+ Assert.NotNull(response.Results);
+ }
+
+ [Fact]
+ public async Task ExecuteAsync_Returns404_WhenDatabaseNotFound()
+ {
+ // Arrange
+ var dbName = "nonexistentdb";
+
+ _documentDbService.GetDatabaseStatsAsync(
+ Arg.Is(ConnectionString),
+ Arg.Is(dbName),
+ Arg.Any())
+ .Returns(new DocumentDbResponse
+ {
+ Success = false,
+ StatusCode = HttpStatusCode.NotFound,
+ Message = $"Database '{dbName}' was not found."
+ });
+
+ var args = _commandDefinition.Parse(["--connection-string", ConnectionString, "--db-name", dbName]);
+
+ // Act
+ var response = await _command.ExecuteAsync(_context, args, TestContext.Current.CancellationToken);
+
+ // Assert
+ Assert.NotNull(response);
+ Assert.Equal(HttpStatusCode.NotFound, response.Status);
+ Assert.Contains("not found", response.Message.ToLower());
+ }
+
+ [Fact]
+ public async Task ExecuteAsync_Returns400_WhenDbNameIsMissing()
+ {
+ // Arrange & Act
+ var response = await _command.ExecuteAsync(_context, _commandDefinition.Parse([]), TestContext.Current.CancellationToken);
+
+ // Assert
+ Assert.Equal(HttpStatusCode.BadRequest, response.Status);
+ Assert.Contains("required", response.Message.ToLower());
+ }
+
+ [Fact]
+ public async Task ExecuteAsync_Returns500_OnUnexpectedError()
+ {
+ // Arrange
+ var dbName = "testdb";
+ var expectedError = "Unexpected error occurred";
+
+ _documentDbService.GetDatabaseStatsAsync(
+ Arg.Is(ConnectionString),
+ Arg.Is(dbName),
+ Arg.Any())
+ .Returns(new DocumentDbResponse
+ {
+ Success = false,
+ StatusCode = HttpStatusCode.InternalServerError,
+ Message = $"Failed to get database stats: {expectedError}"
+ });
+
+ var args = _commandDefinition.Parse(["--connection-string", ConnectionString, "--db-name", dbName]);
+
+ // Act
+ var response = await _command.ExecuteAsync(_context, args, TestContext.Current.CancellationToken);
+
+ // Assert
+ Assert.NotNull(response);
+ Assert.Equal(HttpStatusCode.InternalServerError, response.Status);
+ Assert.Contains("Failed to get database stats", response.Message);
+ }
+}
\ No newline at end of file
diff --git a/tools/Azure.Mcp.Tools.DocumentDb/tests/Azure.Mcp.Tools.DocumentDb.UnitTests/Database/DropDatabaseCommandTests.cs b/tools/Azure.Mcp.Tools.DocumentDb/tests/Azure.Mcp.Tools.DocumentDb.UnitTests/Database/DropDatabaseCommandTests.cs
new file mode 100644
index 0000000000..d2a871dcab
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.DocumentDb/tests/Azure.Mcp.Tools.DocumentDb.UnitTests/Database/DropDatabaseCommandTests.cs
@@ -0,0 +1,140 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.CommandLine;
+using System.Net;
+using Azure.Mcp.Tools.DocumentDb.Commands.Database;
+using Azure.Mcp.Tools.DocumentDb.Models;
+using Azure.Mcp.Tools.DocumentDb.Services;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Mcp.Core.Models.Command;
+using NSubstitute;
+using Xunit;
+
+namespace Azure.Mcp.Tools.DocumentDb.UnitTests.Database;
+
+public class DropDatabaseCommandTests
+{
+ private const string ConnectionString = "mongodb://localhost:27017";
+
+ private readonly IServiceProvider _serviceProvider;
+ private readonly IDocumentDbService _documentDbService;
+ private readonly ILogger _logger;
+ private readonly DropDatabaseCommand _command;
+ private readonly CommandContext _context;
+ private readonly Command _commandDefinition;
+
+ public DropDatabaseCommandTests()
+ {
+ _documentDbService = Substitute.For();
+ _logger = Substitute.For>();
+ _command = new(_logger);
+ _commandDefinition = _command.GetCommand();
+ _serviceProvider = new ServiceCollection()
+ .AddSingleton(_documentDbService)
+ .BuildServiceProvider();
+ _context = new(_serviceProvider);
+ }
+
+ [Fact]
+ public async Task ExecuteAsync_DropsDatabase_WhenDatabaseExists()
+ {
+ // Arrange
+ var dbName = "testdb";
+
+ _documentDbService.DropDatabaseAsync(
+ Arg.Is(ConnectionString),
+ Arg.Is(dbName),
+ Arg.Any())
+ .Returns(new DocumentDbResponse
+ {
+ Success = true,
+ StatusCode = HttpStatusCode.OK,
+ Message = $"Database '{dbName}' dropped successfully",
+ Data = new Dictionary
+ {
+ ["name"] = dbName,
+ ["deleted"] = true
+ }
+ });
+
+ var args = _commandDefinition.Parse(["--connection-string", ConnectionString, "--db-name", dbName]);
+
+ // Act
+ var response = await _command.ExecuteAsync(_context, args, TestContext.Current.CancellationToken);
+
+ // Assert
+ Assert.NotNull(response);
+ Assert.Equal(HttpStatusCode.OK, response.Status);
+ Assert.NotNull(response.Results);
+ }
+
+ [Fact]
+ public async Task ExecuteAsync_Returns404_WhenDatabaseNotFound()
+ {
+ // Arrange
+ var dbName = "nonexistentdb";
+
+ _documentDbService.DropDatabaseAsync(
+ Arg.Is(ConnectionString),
+ Arg.Is(dbName),
+ Arg.Any())
+ .Returns(new DocumentDbResponse
+ {
+ Success = false,
+ StatusCode = HttpStatusCode.NotFound,
+ Message = $"Database '{dbName}' was not found."
+ });
+
+ var args = _commandDefinition.Parse(["--connection-string", ConnectionString, "--db-name", dbName]);
+
+ // Act
+ var response = await _command.ExecuteAsync(_context, args, TestContext.Current.CancellationToken);
+
+ // Assert
+ Assert.NotNull(response);
+ Assert.Equal(HttpStatusCode.NotFound, response.Status);
+ Assert.Contains("not found", response.Message.ToLower());
+ }
+
+ [Fact]
+ public async Task ExecuteAsync_Returns400_WhenDbNameIsMissing()
+ {
+ // Arrange & Act
+ var response = await _command.ExecuteAsync(_context, _commandDefinition.Parse([]), TestContext.Current.CancellationToken);
+
+ // Assert
+ Assert.Equal(HttpStatusCode.BadRequest, response.Status);
+ Assert.Contains("required", response.Message.ToLower());
+ }
+
+ [Fact]
+ public async Task ExecuteAsync_Returns500_OnUnexpectedError()
+ {
+ // Arrange
+ var dbName = "testdb";
+ var expectedError = "Unexpected error occurred";
+
+ _documentDbService.DropDatabaseAsync(
+ Arg.Is(ConnectionString),
+ Arg.Is(dbName),
+ Arg.Any())
+ .Returns(new DocumentDbResponse
+ {
+ Success = false,
+ StatusCode = HttpStatusCode.InternalServerError,
+ Message = $"Failed to drop database: {expectedError}"
+ });
+
+ var args = _commandDefinition.Parse(["--connection-string", ConnectionString, "--db-name", dbName]);
+
+ // Act
+ var response = await _command.ExecuteAsync(_context, args, TestContext.Current.CancellationToken);
+
+ // Assert
+ Assert.NotNull(response);
+ Assert.Equal(HttpStatusCode.InternalServerError, response.Status);
+ Assert.Contains("Failed to drop database", response.Message);
+ }
+}
\ No newline at end of file
diff --git a/tools/Azure.Mcp.Tools.DocumentDb/tests/Azure.Mcp.Tools.DocumentDb.UnitTests/Database/ListDatabasesCommandTests.cs b/tools/Azure.Mcp.Tools.DocumentDb/tests/Azure.Mcp.Tools.DocumentDb.UnitTests/Database/ListDatabasesCommandTests.cs
new file mode 100644
index 0000000000..84ee0ff6f0
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.DocumentDb/tests/Azure.Mcp.Tools.DocumentDb.UnitTests/Database/ListDatabasesCommandTests.cs
@@ -0,0 +1,141 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.CommandLine;
+using System.Net;
+using Azure.Mcp.Tools.DocumentDb.Commands.Database;
+using Azure.Mcp.Tools.DocumentDb.Models;
+using Azure.Mcp.Tools.DocumentDb.Services;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Mcp.Core.Models.Command;
+using NSubstitute;
+using Xunit;
+
+namespace Azure.Mcp.Tools.DocumentDb.UnitTests.Database;
+
+public class ListDatabasesCommandTests
+{
+ private const string ConnectionString = "mongodb://localhost:27017";
+
+ private readonly IServiceProvider _serviceProvider;
+ private readonly IDocumentDbService _documentDbService;
+ private readonly ILogger _logger;
+ private readonly ListDatabasesCommand _command;
+ private readonly CommandContext _context;
+ private readonly Command _commandDefinition;
+
+ public ListDatabasesCommandTests()
+ {
+ _documentDbService = Substitute.For();
+ _logger = Substitute.For>();
+ _command = new(_logger);
+ _commandDefinition = _command.GetCommand();
+ _serviceProvider = new ServiceCollection()
+ .AddSingleton(_documentDbService)
+ .BuildServiceProvider();
+ _context = new(_serviceProvider);
+ }
+
+ [Fact]
+ public async Task ExecuteAsync_ReturnsDatabases_WhenDatabasesExist()
+ {
+ // Arrange
+ var expectedDatabases = new List>
+ {
+ new()
+ {
+ ["name"] = "database1"
+ },
+ new()
+ {
+ ["name"] = "database2"
+ }
+ };
+
+ _documentDbService.GetDatabasesAsync(ConnectionString, null, Arg.Any())
+ .Returns(new DocumentDbResponse
+ {
+ Success = true,
+ StatusCode = HttpStatusCode.OK,
+ Message = "Databases retrieved successfully.",
+ Data = expectedDatabases
+ });
+
+ var args = _commandDefinition.Parse(["--connection-string", ConnectionString]);
+
+ // Act
+ var response = await _command.ExecuteAsync(_context, args, TestContext.Current.CancellationToken);
+
+ // Assert
+ Assert.NotNull(response);
+ Assert.Equal(HttpStatusCode.OK, response.Status);
+ Assert.NotNull(response.Results);
+
+ await _documentDbService.Received(1).GetDatabasesAsync(ConnectionString, null, Arg.Any());
+ }
+
+ [Fact]
+ public async Task ExecuteAsync_ReturnsSingleDatabase_WhenDbNameIsProvided()
+ {
+ // Arrange
+ const string dbName = "database1";
+
+ _documentDbService.GetDatabasesAsync(ConnectionString, dbName, Arg.Any())
+ .Returns(new DocumentDbResponse
+ {
+ Success = true,
+ StatusCode = HttpStatusCode.OK,
+ Message = $"Database '{dbName}' retrieved successfully.",
+ Data = new List>
+ {
+ new()
+ {
+ ["name"] = dbName,
+ ["collectionCount"] = 1,
+ ["collections"] = new List>
+ {
+ new() { ["name"] = "items", ["documentCount"] = 42L }
+ }
+ }
+ }
+ });
+
+ var args = _commandDefinition.Parse(["--connection-string", ConnectionString, "--db-name", dbName]);
+
+ // Act
+ var response = await _command.ExecuteAsync(_context, args, TestContext.Current.CancellationToken);
+
+ // Assert
+ Assert.NotNull(response);
+ Assert.Equal(HttpStatusCode.OK, response.Status);
+ Assert.NotNull(response.Results);
+
+ await _documentDbService.Received(1).GetDatabasesAsync(ConnectionString, dbName, Arg.Any());
+ }
+
+ [Fact]
+ public async Task ExecuteAsync_Returns404_WhenDatabaseIsMissing()
+ {
+ // Arrange
+ const string dbName = "missingdb";
+
+ _documentDbService.GetDatabasesAsync(ConnectionString, dbName, Arg.Any())
+ .Returns(new DocumentDbResponse
+ {
+ Success = false,
+ StatusCode = HttpStatusCode.NotFound,
+ Message = $"Database '{dbName}' was not found."
+ });
+
+ var args = _commandDefinition.Parse(["--connection-string", ConnectionString, "--db-name", dbName]);
+
+ // Act
+ var response = await _command.ExecuteAsync(_context, args, TestContext.Current.CancellationToken);
+
+ // Assert
+ Assert.NotNull(response);
+ Assert.Equal(HttpStatusCode.NotFound, response.Status);
+ Assert.Contains("not found", response.Message, StringComparison.OrdinalIgnoreCase);
+ }
+}
\ No newline at end of file
diff --git a/tools/Azure.Mcp.Tools.DocumentDb/tests/Azure.Mcp.Tools.DocumentDb.UnitTests/Index/CreateIndexCommandTests.cs b/tools/Azure.Mcp.Tools.DocumentDb/tests/Azure.Mcp.Tools.DocumentDb.UnitTests/Index/CreateIndexCommandTests.cs
new file mode 100644
index 0000000000..e9f2675491
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.DocumentDb/tests/Azure.Mcp.Tools.DocumentDb.UnitTests/Index/CreateIndexCommandTests.cs
@@ -0,0 +1,123 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.CommandLine;
+using System.Net;
+using Azure.Mcp.Tools.DocumentDb.Commands.Index;
+using Azure.Mcp.Tools.DocumentDb.Models;
+using Azure.Mcp.Tools.DocumentDb.Services;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Mcp.Core.Models.Command;
+using MongoDB.Bson;
+using NSubstitute;
+using Xunit;
+
+namespace Azure.Mcp.Tools.DocumentDb.UnitTests.Index;
+
+public class CreateIndexCommandTests
+{
+ private readonly IDocumentDbService _documentDbService;
+ private readonly CreateIndexCommand _command;
+ private readonly CommandContext _context;
+ private readonly Command _commandDefinition;
+
+ public CreateIndexCommandTests()
+ {
+ _documentDbService = Substitute.For();
+ _command = new(Substitute.For>());
+ _commandDefinition = _command.GetCommand();
+ _context = new(new ServiceCollection().AddSingleton(_documentDbService).BuildServiceProvider());
+ }
+
+ [Fact]
+ public async Task ExecuteAsync_CreatesIndex_WhenValidKeysProvided()
+ {
+ const string connectionString = "mongodb://localhost:27017";
+ const string dbName = "testdb";
+ const string collectionName = "testcollection";
+ const string keys = "{\"status\": 1}";
+
+ _documentDbService.CreateIndexAsync(connectionString, dbName, collectionName, Arg.Any(), Arg.Any(), Arg.Any())
+ .Returns(new DocumentDbResponse
+ {
+ Success = true,
+ StatusCode = HttpStatusCode.OK,
+ Message = "Index created successfully",
+ Data = new Dictionary { ["index_name"] = "status_1" }
+ });
+
+ var response = await _command.ExecuteAsync(_context, _commandDefinition.Parse([
+ "--connection-string", connectionString,
+ "--db-name", dbName,
+ "--collection-name", collectionName,
+ "--keys", keys]), TestContext.Current.CancellationToken);
+
+ Assert.Equal(HttpStatusCode.OK, response.Status);
+ Assert.NotNull(response.Results);
+ }
+
+ [Fact]
+ public async Task ExecuteAsync_CreatesIndexWithOptions_WhenOptionsProvided()
+ {
+ const string connectionString = "mongodb://localhost:27017";
+ const string dbName = "testdb";
+ const string collectionName = "testcollection";
+ const string keys = "{\"email\": 1}";
+ const string options = "{\"unique\": true}";
+
+ _documentDbService.CreateIndexAsync(connectionString, dbName, collectionName, Arg.Any(), Arg.Any(), Arg.Any())
+ .Returns(new DocumentDbResponse
+ {
+ Success = true,
+ StatusCode = HttpStatusCode.OK,
+ Message = "Index created successfully",
+ Data = new Dictionary { ["index_name"] = "email_1" }
+ });
+
+ var response = await _command.ExecuteAsync(_context, _commandDefinition.Parse([
+ "--connection-string", connectionString,
+ "--db-name", dbName,
+ "--collection-name", collectionName,
+ "--keys", keys,
+ "--options", options]), TestContext.Current.CancellationToken);
+
+ Assert.Equal(HttpStatusCode.OK, response.Status);
+ Assert.NotNull(response.Results);
+ }
+
+ [Fact]
+ public async Task ExecuteAsync_Returns400_WhenCollectionNotFound()
+ {
+ const string connectionString = "mongodb://localhost:27017";
+
+ _documentDbService.CreateIndexAsync(connectionString, "testdb", "nonexistent", Arg.Any(), Arg.Any(), Arg.Any())
+ .Returns(new DocumentDbResponse
+ {
+ Success = false,
+ StatusCode = HttpStatusCode.BadRequest,
+ Message = "Collection 'nonexistent' not found"
+ });
+
+ var response = await _command.ExecuteAsync(_context, _commandDefinition.Parse([
+ "--connection-string", connectionString,
+ "--db-name", "testdb",
+ "--collection-name", "nonexistent",
+ "--keys", "{\"status\": 1}"]), TestContext.Current.CancellationToken);
+
+ Assert.Equal(HttpStatusCode.BadRequest, response.Status);
+ Assert.Contains("not found", response.Message);
+ }
+
+ [Theory]
+ [InlineData("--connection-string", "mongodb://localhost:27017", "--db-name", "testdb", "--collection-name", "coll")]
+ [InlineData("--connection-string", "mongodb://localhost:27017", "--db-name", "testdb", "--keys", "{\"a\":1}")]
+ [InlineData("--connection-string", "mongodb://localhost:27017", "--collection-name", "coll", "--keys", "{\"a\":1}")]
+ public async Task ExecuteAsync_Returns400_WhenRequiredParametersAreMissing(params string[] args)
+ {
+ var response = await _command.ExecuteAsync(_context, _commandDefinition.Parse(args), TestContext.Current.CancellationToken);
+
+ Assert.Equal(HttpStatusCode.BadRequest, response.Status);
+ Assert.Contains("required", response.Message.ToLowerInvariant());
+ }
+}
\ No newline at end of file
diff --git a/tools/Azure.Mcp.Tools.DocumentDb/tests/Azure.Mcp.Tools.DocumentDb.UnitTests/Index/CurrentOpsCommandTests.cs b/tools/Azure.Mcp.Tools.DocumentDb/tests/Azure.Mcp.Tools.DocumentDb.UnitTests/Index/CurrentOpsCommandTests.cs
new file mode 100644
index 0000000000..9cbab91b12
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.DocumentDb/tests/Azure.Mcp.Tools.DocumentDb.UnitTests/Index/CurrentOpsCommandTests.cs
@@ -0,0 +1,95 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.CommandLine;
+using System.Net;
+using Azure.Mcp.Tools.DocumentDb.Commands.Index;
+using Azure.Mcp.Tools.DocumentDb.Models;
+using Azure.Mcp.Tools.DocumentDb.Services;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Mcp.Core.Models.Command;
+using MongoDB.Bson;
+using NSubstitute;
+using Xunit;
+
+namespace Azure.Mcp.Tools.DocumentDb.UnitTests.Index;
+
+public class CurrentOpsCommandTests
+{
+ private readonly IDocumentDbService _documentDbService;
+ private readonly CurrentOpsCommand _command;
+ private readonly CommandContext _context;
+ private readonly Command _commandDefinition;
+
+ public CurrentOpsCommandTests()
+ {
+ _documentDbService = Substitute.For();
+ _command = new(Substitute.For>());
+ _commandDefinition = _command.GetCommand();
+ _context = new(new ServiceCollection().AddSingleton(_documentDbService).BuildServiceProvider());
+ }
+
+ [Fact]
+ public async Task ExecuteAsync_ReturnsOps_WhenOpsExist()
+ {
+ const string connectionString = "mongodb://localhost:27017";
+
+ _documentDbService.GetCurrentOpsAsync(connectionString, Arg.Any(), Arg.Any())
+ .Returns(new DocumentDbResponse
+ {
+ Success = true,
+ StatusCode = HttpStatusCode.OK,
+ Message = "Current operations retrieved successfully",
+ Data = new Dictionary { ["operations"] = "{\"inprog\":[]}" }
+ });
+
+ var response = await _command.ExecuteAsync(_context, _commandDefinition.Parse([
+ "--connection-string", connectionString]), TestContext.Current.CancellationToken);
+
+ Assert.Equal(HttpStatusCode.OK, response.Status);
+ Assert.NotNull(response.Results);
+ }
+
+ [Fact]
+ public async Task ExecuteAsync_ReturnsFilteredOps_WhenFilterProvided()
+ {
+ const string connectionString = "mongodb://localhost:27017";
+
+ _documentDbService.GetCurrentOpsAsync(connectionString, Arg.Any(), Arg.Any())
+ .Returns(new DocumentDbResponse
+ {
+ Success = true,
+ StatusCode = HttpStatusCode.OK,
+ Message = "Current operations retrieved successfully",
+ Data = new Dictionary { ["operations"] = "{\"inprog\":[{\"op\":\"query\"}]}" }
+ });
+
+ var response = await _command.ExecuteAsync(_context, _commandDefinition.Parse([
+ "--connection-string", connectionString,
+ "--ops", "{\"op\":\"query\"}"]), TestContext.Current.CancellationToken);
+
+ Assert.Equal(HttpStatusCode.OK, response.Status);
+ Assert.NotNull(response.Results);
+ }
+
+ [Fact]
+ public async Task ExecuteAsync_Returns500_WhenServiceFails()
+ {
+ const string connectionString = "mongodb://localhost:27017";
+
+ _documentDbService.GetCurrentOpsAsync(connectionString, Arg.Any(), Arg.Any())
+ .Returns(new DocumentDbResponse
+ {
+ Success = false,
+ StatusCode = HttpStatusCode.InternalServerError,
+ Message = "Failed to retrieve current operations"
+ });
+
+ var response = await _command.ExecuteAsync(_context, _commandDefinition.Parse([
+ "--connection-string", connectionString]), TestContext.Current.CancellationToken);
+
+ Assert.Equal(HttpStatusCode.InternalServerError, response.Status);
+ Assert.Contains("Failed to retrieve current operations", response.Message);
+ }
+}
\ No newline at end of file
diff --git a/tools/Azure.Mcp.Tools.DocumentDb/tests/Azure.Mcp.Tools.DocumentDb.UnitTests/Index/DropIndexCommandTests.cs b/tools/Azure.Mcp.Tools.DocumentDb/tests/Azure.Mcp.Tools.DocumentDb.UnitTests/Index/DropIndexCommandTests.cs
new file mode 100644
index 0000000000..865d23a4f8
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.DocumentDb/tests/Azure.Mcp.Tools.DocumentDb.UnitTests/Index/DropIndexCommandTests.cs
@@ -0,0 +1,113 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.CommandLine;
+using System.Net;
+using Azure.Mcp.Tools.DocumentDb.Commands.Index;
+using Azure.Mcp.Tools.DocumentDb.Models;
+using Azure.Mcp.Tools.DocumentDb.Services;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Mcp.Core.Models.Command;
+using NSubstitute;
+using Xunit;
+
+namespace Azure.Mcp.Tools.DocumentDb.UnitTests.Index;
+
+public class DropIndexCommandTests
+{
+ private readonly IDocumentDbService _documentDbService;
+ private readonly DropIndexCommand _command;
+ private readonly CommandContext _context;
+ private readonly Command _commandDefinition;
+
+ public DropIndexCommandTests()
+ {
+ _documentDbService = Substitute.For();
+ _command = new(Substitute.For