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..159a5c1ac1 100644
--- a/servers/Azure.Mcp.Server/README.md
+++ b/servers/Azure.Mcp.Server/README.md
@@ -963,6 +963,33 @@ 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 Azure DocumentDB database 'test'"
+* "Create an index on field 'category' for collection 'items' in Azure DocumentDB database 'test'"
+* "Drop index 'category_1' from collection 'items' in Azure DocumentDB database 'test'"
+* "Show index statistics for collection 'items' in Azure DocumentDB database 'test'"
+* "Show current Azure DocumentDB operations"
+* "List all databases in Azure DocumentDB"
+* "Get statistics for database 'mydb'"
+* "Get details for database 'analytics'"
+* "Drop database 'testdb'"
+* "Get statistics for collection 'users'"
+* "Rename collection 'old-name' to 'new-name'"
+* "Sample documents from collection 'products'"
+* "Find documents in collection 'users' where status is active"
+* "Count documents in collection 'orders'"
+* "Insert a document into collection 'products'"
+* "Insert multiple documents into collection 'inventory'"
+* "Update multiple documents in collection 'inventory' where quantity is low"
+* "Delete multiple documents from collection 'logs' where date is old"
+* "Run an aggregation pipeline on collection 'sales'"
+* "Find and modify a document in collection 'users'"
+* "Explain a find query plan for collection 'users'"
+* "Explain a count query for collection 'orders'"
+* "Explain an aggregation pipeline for collection 'sales'"
+
+
### 📣 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/changelog-entries/1773584979801.yaml b/servers/Azure.Mcp.Server/changelog-entries/1773584979801.yaml
new file mode 100644
index 0000000000..2ee231df4f
--- /dev/null
+++ b/servers/Azure.Mcp.Server/changelog-entries/1773584979801.yaml
@@ -0,0 +1,3 @@
+changes:
+ - section: "Features Added"
+ description: "Added DocumentDB tools for managing Azure DocumentDB (with MongoDB compatibility) collection"
\ No newline at end of file
diff --git a/servers/Azure.Mcp.Server/changelog-entries/1773641631392.yaml b/servers/Azure.Mcp.Server/changelog-entries/1773641631392.yaml
new file mode 100644
index 0000000000..08f0fd209e
--- /dev/null
+++ b/servers/Azure.Mcp.Server/changelog-entries/1773641631392.yaml
@@ -0,0 +1,3 @@
+changes:
+ - section: "Features Added"
+ description: "Added mcp tools for managing Azure DocumentDB (with MongoDB compatibility) document; refactor previous file structure for Azure DocumentDB commands"
\ 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..03c614e575 100644
--- a/servers/Azure.Mcp.Server/docs/azmcp-commands.md
+++ b/servers/Azure.Mcp.Server/docs/azmcp-commands.md
@@ -1694,6 +1694,152 @@ 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
+
+# List all databases or inspect a single database
+# ❌ Destructive | ✅ Idempotent | ❌ OpenWorld | ✅ ReadOnly | ❌ Secret | ❌ LocalRequired
+azmcp documentdb database list databases --connection-string \
+ [--db-name ]
+
+# Rename a collection
+# ✅ Destructive | ❌ Idempotent | ❌ OpenWorld | ❌ ReadOnly | ❌ Secret | ❌ LocalRequired
+azmcp documentdb collection rename collection --connection-string \
+ --db-name \
+ --collection-name \
+ --new-collection-name
+
+# Drop a collection
+# ✅ Destructive | ❌ Idempotent | ❌ OpenWorld | ❌ ReadOnly | ❌ Secret | ❌ LocalRequired
+azmcp documentdb collection drop collection --connection-string \
+ --db-name \
+ --collection-name
+
+# Sample documents from a collection
+# ❌ Destructive | ❌ Idempotent | ❌ OpenWorld | ✅ ReadOnly | ❌ Secret | ❌ LocalRequired
+azmcp documentdb collection sample documents --connection-string \
+ --db-name \
+ --collection-name \
+ [--sample-size ]
+
+# Get Azure DocumentDB statistics for a database
+# ❌ Destructive | ✅ Idempotent | ❌ OpenWorld | ✅ ReadOnly | ❌ Secret | ❌ LocalRequired
+azmcp documentdb others get stats --connection-string \
+ --resource-type database \
+ --db-name
+
+# Get Azure DocumentDB statistics for a collection
+# ❌ Destructive | ✅ Idempotent | ❌ OpenWorld | ✅ ReadOnly | ❌ Secret | ❌ LocalRequired
+azmcp documentdb others get stats --connection-string \
+ --resource-type collection \
+ --db-name \
+ --collection-name
+
+# Get Azure DocumentDB statistics for indexes on a collection
+# ❌ Destructive | ✅ Idempotent | ❌ OpenWorld | ✅ ReadOnly | ❌ Secret | ❌ LocalRequired
+azmcp documentdb others get stats --connection-string \
+ --resource-type index \
+ --db-name \
+ --collection-name
+
+# Get current Azure DocumentDB operations
+# ❌ Destructive | ✅ Idempotent | ❌ OpenWorld | ✅ ReadOnly | ❌ Secret | ❌ LocalRequired
+azmcp documentdb others current ops --connection-string \
+ [--ops-filter ]
+
+# Drop a database
+# ✅ Destructive | ❌ Idempotent | ❌ OpenWorld | ❌ ReadOnly | ❌ Secret | ❌ LocalRequired
+azmcp documentdb database drop database --connection-string \
+ --db-name
+
+# Find and retrieve documents from a collection matching an optional filter, with limit, skip, sort, and projection options
+# ❌ Destructive | ✅ Idempotent | ❌ OpenWorld | ✅ ReadOnly | ❌ Secret | ❌ LocalRequired
+azmcp documentdb document find documents --connection-string \
+ --db-name \
+ --collection-name \
+ [--filter ] \
+ [--options ]
+
+# Count documents in a collection matching an optional filter
+# ❌ Destructive | ✅ Idempotent | ❌ OpenWorld | ✅ ReadOnly | ❌ Secret | ❌ LocalRequired
+azmcp documentdb document count documents --connection-string \
+ --db-name \
+ --collection-name \
+ [--filter ]
+
+# Insert one document or many documents into a collection. If --mode is omitted, the command auto-detects a single document versus a JSON array payload.
+# ❌ Destructive | ❌ Idempotent | ❌ OpenWorld | ❌ ReadOnly | ❌ Secret | ❌ LocalRequired
+azmcp documentdb document insert documents --connection-string \
+ --db-name \
+ --collection-name \
+ --documents \
+ [--mode ]
+
+# Update one or more documents in a collection matching a required filter. If --mode is omitted, the command defaults to single.
+# ✅ Destructive | ❌ Idempotent | ❌ OpenWorld | ❌ ReadOnly | ❌ Secret | ❌ LocalRequired
+azmcp documentdb document update documents --connection-string \
+ --db-name \
+ --collection-name \
+ --filter \
+ --update \
+ [--upsert] \
+ [--mode ]
+
+# Delete one or more documents from a collection matching a required filter. If --mode is omitted, the command defaults to single.
+# ✅ Destructive | ❌ Idempotent | ❌ OpenWorld | ❌ ReadOnly | ❌ Secret | ❌ LocalRequired
+azmcp documentdb document delete documents --connection-string \
+ --db-name \
+ --collection-name \
+ --filter \
+ [--mode ]
+
+# Run an aggregation pipeline on a collection. Pipelines that use write stages such as $out or $merge may modify data.
+# ✅ Destructive | ❌ Idempotent | ❌ OpenWorld | ❌ ReadOnly | ❌ Secret | ❌ LocalRequired
+azmcp documentdb document aggregate --connection-string \
+ --db-name \
+ --collection-name \
+ --pipeline \
+ [--allow-disk-use]
+
+# Find and update a single document in a collection matching a required filter, returning the document before modification
+# ✅ Destructive | ❌ Idempotent | ❌ OpenWorld | ❌ ReadOnly | ❌ Secret | ❌ LocalRequired
+azmcp documentdb document find and modify --connection-string \
+ --db-name \
+ --collection-name \
+ --filter \
+ --update \
+ [--upsert]
+
+# Explain a find, count, or aggregate operation for a collection by passing an operation-specific JSON body with --query-body
+# ❌ Destructive | ✅ Idempotent | ❌ OpenWorld | ✅ ReadOnly | ❌ Secret | ❌ LocalRequired
+azmcp documentdb document explain query --connection-string \
+ --db-name \
+ --collection-name \
+ --operation \
+ [--query-body ]
+```
+
### 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..e7d5c0a755 100644
--- a/servers/Azure.Mcp.Server/docs/e2eTestPrompts.md
+++ b/servers/Azure.Mcp.Server/docs/e2eTestPrompts.md
@@ -356,6 +356,34 @@ 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_create_index | Add an index for collection in DocumentDB database with keys and options |
+| documentdb_index_drop_index | Remove the index from DocumentDB collection in database |
+| documentdb_others_current_ops | Get current DocumentDB operations filtered by |
+| documentdb_others_get_stats | Get index stats for collection in DocumentDB database |
+| documentdb_others_get_stats | Show me stats for DocumentDB database |
+| documentdb_database_drop_database | Delete the database from DocumentDB cluster |
+| documentdb_database_list_databases | List all databases in DocumentDB |
+| documentdb_database_list_databases | Get details for database in DocumentDB |
+| documentdb_document_find_documents | Get all the documents in collection in DocumentDB database |
+| documentdb_document_find_documents | Find documents from collection in DocumentDB database with filter |
+| documentdb_document_count_documents | How many documents are in collection in DocumentDB database |
+| documentdb_document_insert_documents | Add a new document to collection in DocumentDB database |
+| documentdb_document_insert_documents | Bulk insert documents into collection in DocumentDB database |
+| documentdb_document_update_documents | Update documents where id equals in collection in DocumentDB database |
+| documentdb_document_update_documents | Bulk update documents in collection in DocumentDB database |
+| documentdb_document_delete_documents | Remove document whose id equals from collection in DocumentDB database |
+| documentdb_document_delete_documents | Remove all documents with from collection in DocumentDB database |
+| documentdb_document_aggregate | Run an aggregation pipeline on collection in database from DocumentDB cluster |
+| documentdb_document_find_and_modify | Find and update documents with in collection in DocumentDB database |
+| documentdb_document_explain_query | Explain the find query described by against collection in DocumentDB database |
+| documentdb_document_explain_query | Show me the execution plan for the count query described by on collection in DocumentDB database |
+| documentdb_document_explain_query | Explain the aggregation query described by on collection in DocumentDB database |
+
## 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..686f9482f7 100644
--- a/servers/Azure.Mcp.Server/src/Resources/consolidated-tools.json
+++ b/servers/Azure.Mcp.Server/src/Resources/consolidated-tools.json
@@ -137,7 +137,7 @@
},
{
"name": "get_azure_databases_details",
- "description": "Comprehensive Azure database management tool for MySQL, PostgreSQL, SQL Database, SQL Server, and Cosmos DB. List and query databases, retrieve server configurations and parameters, explore table schemas, execute database queries, manage Cosmos DB containers and items, list and view detailed information about SQL servers, and view database server details across all Azure database services.",
+ "description": "Comprehensive Azure database management tool for MySQL, PostgreSQL, SQL Database, SQL Server, Cosmos DB, and Azure DocumentDB (with MongoDB compatibility). List and query databases, retrieve server configurations and parameters, explore table schemas, inspect Azure DocumentDB documents and indexes, explain queries, review current operations and stats, manage Cosmos DB containers and items, list and view detailed information about SQL servers, and view database server details across all Azure database services.",
"toolMetadata": {
"destructive": {
"value": false,
@@ -178,7 +178,121 @@
"sql_db_get",
"sql_server_get",
"cosmos_list",
- "cosmos_database_container_item_query"
+ "cosmos_database_container_item_query",
+ "documentdb_database_list_databases",
+ "documentdb_document_count_documents",
+ "documentdb_document_explain_query",
+ "documentdb_document_find_documents",
+ "documentdb_index_list_indexes",
+ "documentdb_others_current_ops",
+ "documentdb_others_get_stats"
+ ]
+ },
+ {
+ "name": "sample_azure_documentdb_documents",
+ "description": "Sample documents from Azure DocumentDB (with MongoDB compatibility) collections to quickly inspect data shape and example records for schema exploration and query generation.",
+ "toolMetadata": {
+ "destructive": {
+ "value": false,
+ "description": "This tool performs only additive updates without deleting or modifying existing resources."
+ },
+ "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."
+ },
+ "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_collection_sample_documents"
+ ]
+ },
+ {
+ "name": "insert_azure_documentdb_documents",
+ "description": "Insert one or more documents into Azure DocumentDB (with MongoDB compatibility) collections.",
+ "toolMetadata": {
+ "destructive": {
+ "value": false,
+ "description": "This tool performs only additive updates without deleting or modifying existing resources."
+ },
+ "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."
+ },
+ "readOnly": {
+ "value": false,
+ "description": "This tool may modify its environment by creating, updating, or deleting 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_document_insert_documents"
+ ]
+ },
+ {
+ "name": "manage_azure_documentdb_resources",
+ "description": "Manage Azure DocumentDB (with MongoDB compatibility) databases, collections, indexes, and documents. Supports dropping and renaming collections, dropping databases, running aggregation pipelines that may write results, updating or deleting documents, atomic find-and-modify operations, and creating or dropping indexes.",
+ "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."
+ },
+ "readOnly": {
+ "value": false,
+ "description": "This tool may modify its environment by creating, updating, or deleting 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_collection_drop_collection",
+ "documentdb_collection_rename_collection",
+ "documentdb_database_drop_database",
+ "documentdb_document_aggregate",
+ "documentdb_document_delete_documents",
+ "documentdb_document_find_and_modify",
+ "documentdb_document_update_documents",
+ "documentdb_index_create_index",
+ "documentdb_index_drop_index"
]
},
{
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..d515332bb8
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.DocumentDb/src/Commands/BaseDocumentDbCommand.cs
@@ -0,0 +1,37 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.CommandLine;
+using System.Diagnostics.CodeAnalysis;
+using System.Net;
+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)
+ };
+ }
+
+ protected override HttpStatusCode GetStatusCode(Exception ex) => ex switch
+ {
+ ArgumentException => HttpStatusCode.BadRequest,
+ InvalidOperationException => HttpStatusCode.UnprocessableEntity,
+ _ => base.GetStatusCode(ex)
+ };
+}
diff --git a/tools/Azure.Mcp.Tools.DocumentDb/src/Commands/Collection/DropCollectionCommand.cs b/tools/Azure.Mcp.Tools.DocumentDb/src/Commands/Collection/DropCollectionCommand.cs
new file mode 100644
index 0000000000..4a61790822
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.DocumentDb/src/Commands/Collection/DropCollectionCommand.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;
+
+namespace Azure.Mcp.Tools.DocumentDb.Commands.Collection;
+
+public sealed class DropCollectionCommand(ILogger logger)
+ : BaseDocumentDbCommand()
+{
+ private readonly ILogger _logger = logger;
+
+ public override string Id => "d0e1f2a3-b4c5-4d0e-7f8a-9b0c1d2e3f4a";
+
+ public override string Name => "drop_collection";
+
+ public override string Description => "Drop a collection from a database";
+
+ public override string Title => "Drop Collection";
+
+ public override ToolMetadata Metadata => new()
+ {
+ Destructive = true,
+ Idempotent = false,
+ OpenWorld = false,
+ Secret = false,
+ LocalRequired = false,
+ ReadOnly = false
+ };
+
+ protected override void RegisterOptions(Command command)
+ {
+ base.RegisterOptions(command);
+ command.Options.Add(DocumentDbOptionDefinitions.DbName);
+ command.Options.Add(DocumentDbOptionDefinitions.CollectionName);
+ }
+
+ protected override DropCollectionOptions 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)
+ {
+ DropCollectionOptions? options = null;
+
+ try
+ {
+ if (!Validate(parseResult.CommandResult, context.Response).IsValid)
+ {
+ return context.Response;
+ }
+
+ options = BindOptions(parseResult);
+
+ var service = context.GetService();
+
+ var result = await service.DropCollectionAsync(options.ConnectionString!, options.DbName!, options.CollectionName!, cancellationToken);
+
+ DocumentDbResponseHelper.ProcessResponse(context, result);
+
+ return context.Response;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to drop collection: {CollectionName} from database: {DbName}", options?.CollectionName, options?.DbName);
+ HandleException(context, ex);
+ return context.Response;
+ }
+ }
+}
diff --git a/tools/Azure.Mcp.Tools.DocumentDb/src/Commands/Collection/RenameCollectionCommand.cs b/tools/Azure.Mcp.Tools.DocumentDb/src/Commands/Collection/RenameCollectionCommand.cs
new file mode 100644
index 0000000000..4b4f7ffada
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.DocumentDb/src/Commands/Collection/RenameCollectionCommand.cs
@@ -0,0 +1,86 @@
+// 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.Collection;
+
+public sealed class RenameCollectionCommand(ILogger logger)
+ : BaseDocumentDbCommand()
+{
+ private readonly ILogger _logger = logger;
+
+ public override string Id => "c9d0e1f2-a3b4-4c9d-6e7f-8a9b0c1d2e3f";
+
+ public override string Name => "rename_collection";
+
+ public override string Description => "Rename a collection";
+
+ public override string Title => "Rename Collection";
+
+ public override ToolMetadata Metadata => new()
+ {
+ Destructive = true,
+ Idempotent = false,
+ OpenWorld = false,
+ Secret = false,
+ LocalRequired = false,
+ ReadOnly = false
+ };
+
+ protected override void RegisterOptions(Command command)
+ {
+ base.RegisterOptions(command);
+ command.Options.Add(DocumentDbOptionDefinitions.DbName);
+ command.Options.Add(DocumentDbOptionDefinitions.CollectionName);
+ command.Options.Add(DocumentDbOptionDefinitions.NewCollectionName);
+ }
+
+ protected override RenameCollectionOptions BindOptions(ParseResult parseResult)
+ {
+ var options = base.BindOptions(parseResult);
+ options.DbName = parseResult.GetValueOrDefault(DocumentDbOptionDefinitions.DbName.Name);
+ options.CollectionName = parseResult.GetValueOrDefault(DocumentDbOptionDefinitions.CollectionName.Name);
+ options.NewCollectionName = parseResult.GetValueOrDefault(DocumentDbOptionDefinitions.NewCollectionName.Name);
+ return options;
+ }
+
+ public override async Task ExecuteAsync(
+ CommandContext context,
+ ParseResult parseResult,
+ CancellationToken cancellationToken)
+ {
+ RenameCollectionOptions? options = null;
+
+ try
+ {
+ if (!Validate(parseResult.CommandResult, context.Response).IsValid)
+ {
+ return context.Response;
+ }
+
+ options = BindOptions(parseResult);
+
+ var service = context.GetService();
+
+ var result = await service.RenameCollectionAsync(options.ConnectionString!, options.DbName!, options.CollectionName!, options.NewCollectionName!, cancellationToken);
+
+ DocumentDbResponseHelper.ProcessResponse(context, result);
+
+ return context.Response;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to rename collection from {OldName} to {NewName} in database: {DbName}", options?.CollectionName, options?.NewCollectionName, options?.DbName);
+ HandleException(context, ex);
+ return context.Response;
+ }
+ }
+}
diff --git a/tools/Azure.Mcp.Tools.DocumentDb/src/Commands/Collection/SampleDocumentsCommand.cs b/tools/Azure.Mcp.Tools.DocumentDb/src/Commands/Collection/SampleDocumentsCommand.cs
new file mode 100644
index 0000000000..5dd391d4d5
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.DocumentDb/src/Commands/Collection/SampleDocumentsCommand.cs
@@ -0,0 +1,86 @@
+// 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.Collection;
+
+public sealed class SampleDocumentsCommand(ILogger logger)
+ : BaseDocumentDbCommand()
+{
+ private readonly ILogger _logger = logger;
+
+ public override string Id => "e1f2a3b4-c5d6-4e1f-8a9b-0c1d2e3f4a5b";
+
+ public override string Name => "sample_documents";
+
+ public override string Description => "Retrieve sample documents from a specific collection. Useful for understanding data schema and query generation";
+
+ public override string Title => "Sample Documents";
+
+ public override ToolMetadata Metadata => new()
+ {
+ Destructive = false,
+ Idempotent = false,
+ OpenWorld = false,
+ Secret = false,
+ LocalRequired = false,
+ ReadOnly = true
+ };
+
+ protected override void RegisterOptions(Command command)
+ {
+ base.RegisterOptions(command);
+ command.Options.Add(DocumentDbOptionDefinitions.DbName);
+ command.Options.Add(DocumentDbOptionDefinitions.CollectionName);
+ command.Options.Add(DocumentDbOptionDefinitions.SampleSize);
+ }
+
+ protected override SampleDocumentsOptions BindOptions(ParseResult parseResult)
+ {
+ var options = base.BindOptions(parseResult);
+ options.DbName = parseResult.GetValueOrDefault(DocumentDbOptionDefinitions.DbName.Name);
+ options.CollectionName = parseResult.GetValueOrDefault(DocumentDbOptionDefinitions.CollectionName.Name);
+ options.SampleSize = parseResult.GetValueOrDefault(DocumentDbOptionDefinitions.SampleSize.Name);
+ return options;
+ }
+
+ public override async Task ExecuteAsync(
+ CommandContext context,
+ ParseResult parseResult,
+ CancellationToken cancellationToken)
+ {
+ SampleDocumentsOptions? options = null;
+
+ try
+ {
+ if (!Validate(parseResult.CommandResult, context.Response).IsValid)
+ {
+ return context.Response;
+ }
+
+ options = BindOptions(parseResult);
+
+ var service = context.GetService();
+
+ var result = await service.SampleDocumentsAsync(options.ConnectionString!, options.DbName!, options.CollectionName!, options.SampleSize, cancellationToken);
+
+ DocumentDbResponseHelper.ProcessResponse(context, result);
+
+ return context.Response;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to sample documents from collection: {CollectionName} in database: {DbName} with sample size: {SampleSize}", options?.CollectionName, options?.DbName, options?.SampleSize);
+ 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..041724f92f
--- /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 database.";
+
+ 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..48d1ab603c
--- /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 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/Document/AggregateCommand.cs b/tools/Azure.Mcp.Tools.DocumentDb/src/Commands/Document/AggregateCommand.cs
new file mode 100644
index 0000000000..b14473c709
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.DocumentDb/src/Commands/Document/AggregateCommand.cs
@@ -0,0 +1,96 @@
+// 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.Document;
+
+public sealed class AggregateCommand(ILogger logger)
+ : BaseDocumentDbCommand()
+{
+ private readonly ILogger _logger = logger;
+
+ public override string Id => "b0c1d2e3-f4a5-4b0c-7d8e-9f0a1b2c3d4e";
+
+ public override string Name => "aggregate";
+
+ public override string Description => "Run an aggregation pipeline on a collection.";
+
+ public override string Title => "Aggregate Pipeline";
+
+ 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);
+ command.Options.Add(DocumentDbOptionDefinitions.CollectionName);
+ command.Options.Add(DocumentDbOptionDefinitions.Pipeline);
+ command.Options.Add(DocumentDbOptionDefinitions.AllowDiskUse);
+ }
+
+ protected override AggregateOptions BindOptions(ParseResult parseResult)
+ {
+ var options = base.BindOptions(parseResult);
+ options.DbName = parseResult.GetValueOrDefault(DocumentDbOptionDefinitions.DbName.Name);
+ options.CollectionName = parseResult.GetValueOrDefault(DocumentDbOptionDefinitions.CollectionName.Name);
+ options.Pipeline = parseResult.GetValueOrDefault(DocumentDbOptionDefinitions.Pipeline.Name);
+ options.AllowDiskUse = parseResult.GetValueOrDefault(DocumentDbOptionDefinitions.AllowDiskUse.Name);
+ return options;
+ }
+
+ public override async Task ExecuteAsync(
+ CommandContext context,
+ ParseResult parseResult,
+ CancellationToken cancellationToken)
+ {
+ AggregateOptions? options = null;
+
+ try
+ {
+ if (!Validate(parseResult.CommandResult, context.Response).IsValid)
+ {
+ return context.Response;
+ }
+
+ options = BindOptions(parseResult);
+
+ var service = context.GetService();
+
+ var pipeline = DocumentDbHelpers.ParseBsonDocumentList(options.Pipeline);
+
+ if (pipeline == null || pipeline.Count == 0)
+ {
+ throw new ArgumentException("Invalid pipeline format or empty pipeline");
+ }
+
+ var result = await service.AggregateAsync(options.ConnectionString!, options.DbName!, options.CollectionName!, pipeline, options.AllowDiskUse, cancellationToken);
+
+ // Process response using unified DocumentDbResponse type
+ DocumentDbResponseHelper.ProcessResponse(context, result);
+
+ return context.Response;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to run aggregation pipeline on collection: {CollectionName}, database: {DbName}, allowDiskUse: {AllowDiskUse}", options?.CollectionName, options?.DbName, options?.AllowDiskUse);
+ HandleException(context, ex);
+ return context.Response;
+ }
+ }
+}
diff --git a/tools/Azure.Mcp.Tools.DocumentDb/src/Commands/Document/CountDocumentsCommand.cs b/tools/Azure.Mcp.Tools.DocumentDb/src/Commands/Document/CountDocumentsCommand.cs
new file mode 100644
index 0000000000..e20a20253c
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.DocumentDb/src/Commands/Document/CountDocumentsCommand.cs
@@ -0,0 +1,92 @@
+// 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.Document;
+
+public sealed class CountDocumentsCommand(ILogger logger)
+ : BaseDocumentDbCommand()
+{
+ private readonly ILogger _logger = logger;
+
+ public override string Id => "a3b4c5d6-e7f8-4a3b-0c1d-2e3f4a5b6c7d";
+
+ public override string Name => "count_documents";
+
+ public override string Description => "Count documents in a collection matching an optional filter.";
+
+ public override string Title => "Count Documents";
+
+ 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);
+ command.Options.Add(DocumentDbOptionDefinitions.CollectionName);
+ command.Options.Add(DocumentDbOptionDefinitions.Filter);
+ }
+
+ protected override CountDocumentsOptions BindOptions(ParseResult parseResult)
+ {
+ var options = base.BindOptions(parseResult);
+ options.DbName = parseResult.GetValueOrDefault(DocumentDbOptionDefinitions.DbName.Name);
+ options.CollectionName = parseResult.GetValueOrDefault(DocumentDbOptionDefinitions.CollectionName.Name);
+ options.Filter = parseResult.GetValueOrDefault(DocumentDbOptionDefinitions.Filter.Name);
+ return options;
+ }
+
+ public override async Task ExecuteAsync(
+ CommandContext context,
+ ParseResult parseResult,
+ CancellationToken cancellationToken)
+ {
+ CountDocumentsOptions? options = null;
+
+ try
+ {
+ if (!Validate(parseResult.CommandResult, context.Response).IsValid)
+ {
+ return context.Response;
+ }
+
+ options = BindOptions(parseResult);
+
+ var service = context.GetService();
+
+ var filter = DocumentDbHelpers.ParseBsonDocument(options.Filter);
+
+ var result = await service.CountDocumentsAsync(options.ConnectionString!, options.DbName!, options.CollectionName!, filter, cancellationToken);
+
+ // Process response using unified DocumentDbResponse type
+ DocumentDbResponseHelper.ProcessResponse(context, result);
+
+ return context.Response;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to count documents in collection: {CollectionName}, database: {DbName}, filter: {Filter}", options?.CollectionName, options?.DbName, options?.Filter);
+ HandleException(context, ex);
+ return context.Response;
+ }
+ }
+}
diff --git a/tools/Azure.Mcp.Tools.DocumentDb/src/Commands/Document/DeleteDocumentsCommand.cs b/tools/Azure.Mcp.Tools.DocumentDb/src/Commands/Document/DeleteDocumentsCommand.cs
new file mode 100644
index 0000000000..eb35dee54b
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.DocumentDb/src/Commands/Document/DeleteDocumentsCommand.cs
@@ -0,0 +1,93 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.CommandLine;
+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.Document;
+
+public sealed class DeleteDocumentsCommand(ILogger logger)
+ : BaseDocumentDbCommand()
+{
+ private readonly ILogger _logger = logger;
+
+ public override string Id => "5c8844e8-07d8-461f-b640-d954a60d6420";
+
+ public override string Name => "delete_documents";
+
+ public override string Description => "Delete one or more documents from a collection matching a required filter. If --mode is omitted, the command defaults to single. Use --mode many to delete multiple documents.";
+
+ public override string Title => "Delete Documents";
+
+ 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);
+ command.Options.Add(DocumentDbOptionDefinitions.CollectionName);
+ command.Options.Add(DocumentDbOptionDefinitions.RequiredFilter);
+ command.Options.Add(DocumentDbOptionDefinitions.Mode);
+ }
+
+ protected override DeleteDocumentsOptions BindOptions(ParseResult parseResult)
+ {
+ var options = base.BindOptions(parseResult);
+ options.DbName = parseResult.GetValueOrDefault(DocumentDbOptionDefinitions.DbName.Name);
+ options.CollectionName = parseResult.GetValueOrDefault(DocumentDbOptionDefinitions.CollectionName.Name);
+ options.Filter = parseResult.GetValueOrDefault(DocumentDbOptionDefinitions.Filter.Name);
+ options.Mode = parseResult.GetValueOrDefault(DocumentDbOptionDefinitions.Mode.Name);
+ return options;
+ }
+
+ public override async Task ExecuteAsync(CommandContext context, ParseResult parseResult, CancellationToken cancellationToken)
+ {
+ DeleteDocumentsOptions? options = null;
+
+ try
+ {
+ if (!Validate(parseResult.CommandResult, context.Response).IsValid)
+ {
+ return context.Response;
+ }
+
+ options = BindOptions(parseResult);
+
+ var service = context.GetService();
+ var filter = DocumentDbHelpers.ParseBsonDocument(options.Filter);
+
+ if (filter == null)
+ {
+ throw new ArgumentException("Invalid filter format");
+ }
+
+ var result = options.Mode switch
+ {
+ "many" => await service.DeleteManyAsync(options.ConnectionString!, options.DbName!, options.CollectionName!, filter, cancellationToken),
+ _ => await service.DeleteDocumentAsync(options.ConnectionString!, options.DbName!, options.CollectionName!, filter, cancellationToken)
+ };
+
+ DocumentDbResponseHelper.ProcessResponse(context, result);
+ return context.Response;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to delete documents from collection: {CollectionName}, database: {DbName}, mode: {Mode}", options?.CollectionName, options?.DbName, options?.Mode);
+ HandleException(context, ex);
+ return context.Response;
+ }
+ }
+}
diff --git a/tools/Azure.Mcp.Tools.DocumentDb/src/Commands/Document/ExplainQueryCommand.cs b/tools/Azure.Mcp.Tools.DocumentDb/src/Commands/Document/ExplainQueryCommand.cs
new file mode 100644
index 0000000000..07d1ff9ebb
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.DocumentDb/src/Commands/Document/ExplainQueryCommand.cs
@@ -0,0 +1,169 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.CommandLine;
+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;
+using MongoDB.Bson;
+using MongoDB.Bson.Serialization;
+
+namespace Azure.Mcp.Tools.DocumentDb.Commands.Document;
+
+public sealed class ExplainQueryCommand(ILogger logger)
+ : BaseDocumentDbCommand()
+{
+ private readonly ILogger _logger = logger;
+
+ public override string Id => "111d3104-aac1-49e4-b3c9-e75f74aca73d";
+
+ public override string Name => "explain_query";
+
+ public override string Description => "Explain a find, count, or aggregate operation for a collection by passing an operation-specific JSON body with --query-body.";
+
+ public override string Title => "Explain Query";
+
+ 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);
+ command.Options.Add(DocumentDbOptionDefinitions.CollectionName);
+ command.Options.Add(DocumentDbOptionDefinitions.Operation);
+ command.Options.Add(DocumentDbOptionDefinitions.QueryBody);
+ }
+
+ protected override ExplainQueryOptions BindOptions(ParseResult parseResult)
+ {
+ var options = base.BindOptions(parseResult);
+ options.DbName = parseResult.GetValueOrDefault(DocumentDbOptionDefinitions.DbName.Name);
+ options.CollectionName = parseResult.GetValueOrDefault(DocumentDbOptionDefinitions.CollectionName.Name);
+ options.Operation = parseResult.GetValueOrDefault(DocumentDbOptionDefinitions.Operation.Name);
+ options.QueryBody = parseResult.GetValueOrDefault(DocumentDbOptionDefinitions.QueryBody.Name);
+ return options;
+ }
+
+ public override async Task ExecuteAsync(CommandContext context, ParseResult parseResult, CancellationToken cancellationToken)
+ {
+ ExplainQueryOptions? options = null;
+
+ try
+ {
+ if (!Validate(parseResult.CommandResult, context.Response).IsValid)
+ {
+ return context.Response;
+ }
+
+ options = BindOptions(parseResult);
+
+ var service = context.GetService();
+ var queryBody = ParseQueryBody(options.QueryBody);
+
+ var result = options.Operation switch
+ {
+ "count" => await service.ExplainCountQueryAsync(options.ConnectionString!, options.DbName!, options.CollectionName!, GetCountFilter(queryBody), cancellationToken),
+ "aggregate" => await service.ExplainAggregateQueryAsync(options.ConnectionString!, options.DbName!, options.CollectionName!, GetAggregatePipeline(queryBody), cancellationToken),
+ _ => await service.ExplainFindQueryAsync(options.ConnectionString!, options.DbName!, options.CollectionName!, GetFindFilter(queryBody), GetFindOptions(queryBody), cancellationToken)
+ };
+
+ DocumentDbResponseHelper.ProcessResponse(context, result);
+ return context.Response;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to explain operation on collection: {CollectionName}, database: {DbName}, operation: {Operation}", options?.CollectionName, options?.DbName, options?.Operation);
+ HandleException(context, ex);
+ return context.Response;
+ }
+ }
+
+ private static BsonDocument? ParseQueryBody(string? queryBody)
+ {
+ return DocumentDbHelpers.ParseBsonDocument(queryBody);
+ }
+
+ private static BsonDocument? GetFindFilter(BsonDocument? queryBody)
+ {
+ EnsureAllowedFields(queryBody, "filter", "options");
+ return GetOptionalDocument(queryBody, "filter");
+ }
+
+ private static BsonDocument? GetFindOptions(BsonDocument? queryBody)
+ {
+ EnsureAllowedFields(queryBody, "filter", "options");
+ return GetOptionalDocument(queryBody, "options");
+ }
+
+ private static BsonDocument? GetCountFilter(BsonDocument? queryBody)
+ {
+ EnsureAllowedFields(queryBody, "filter");
+ return GetOptionalDocument(queryBody, "filter");
+ }
+
+ private static List GetAggregatePipeline(BsonDocument? queryBody)
+ {
+ EnsureAllowedFields(queryBody, "pipeline");
+
+ if (queryBody == null || !queryBody.TryGetValue("pipeline", out var pipelineValue))
+ {
+ throw new ArgumentException("The --query-body JSON must contain a 'pipeline' array when --operation is 'aggregate'.");
+ }
+
+ if (pipelineValue is not BsonArray pipelineArray)
+ {
+ throw new ArgumentException("The 'pipeline' field in --query-body must be a JSON array.");
+ }
+
+ try
+ {
+ return pipelineArray.Select(item => item.AsBsonDocument).ToList();
+ }
+ catch (Exception ex)
+ {
+ throw new ArgumentException("Each item in the 'pipeline' array must be a JSON document.", ex);
+ }
+ }
+
+ private static BsonDocument? GetOptionalDocument(BsonDocument? queryBody, string fieldName)
+ {
+ if (queryBody == null || !queryBody.TryGetValue(fieldName, out var value))
+ {
+ return null;
+ }
+
+ if (value is not BsonDocument document)
+ {
+ throw new ArgumentException($"The '{fieldName}' field in --query-body must be a JSON document.");
+ }
+
+ return document;
+ }
+
+ private static void EnsureAllowedFields(BsonDocument? queryBody, params string[] allowedFields)
+ {
+ if (queryBody == null)
+ {
+ return;
+ }
+
+ var disallowedFields = queryBody.Names.Where(name => !allowedFields.Contains(name, StringComparer.Ordinal)).ToList();
+
+ if (disallowedFields.Count > 0)
+ {
+ throw new ArgumentException($"The --query-body JSON contains unsupported fields for this operation: {string.Join(", ", disallowedFields)}.");
+ }
+ }
+}
diff --git a/tools/Azure.Mcp.Tools.DocumentDb/src/Commands/Document/FindAndModifyCommand.cs b/tools/Azure.Mcp.Tools.DocumentDb/src/Commands/Document/FindAndModifyCommand.cs
new file mode 100644
index 0000000000..e6683ce4eb
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.DocumentDb/src/Commands/Document/FindAndModifyCommand.cs
@@ -0,0 +1,99 @@
+// 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.Document;
+
+public sealed class FindAndModifyCommand(ILogger logger)
+ : BaseDocumentDbCommand()
+{
+ private readonly ILogger _logger = logger;
+
+ public override string Id => "c1d2e3f4-a5b6-4c1d-8e9f-0a1b2c3d4e5f";
+
+ public override string Name => "find_and_modify";
+
+ public override string Description => "Find and update a single document in a collection matching a required filter.";
+
+ public override string Title => "Find And Modify Document";
+
+ 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);
+ command.Options.Add(DocumentDbOptionDefinitions.CollectionName);
+ command.Options.Add(DocumentDbOptionDefinitions.RequiredFilter);
+ command.Options.Add(DocumentDbOptionDefinitions.Update);
+ command.Options.Add(DocumentDbOptionDefinitions.Upsert);
+ }
+
+ protected override FindAndModifyOptions BindOptions(ParseResult parseResult)
+ {
+ var options = base.BindOptions(parseResult);
+ options.DbName = parseResult.GetValueOrDefault(DocumentDbOptionDefinitions.DbName.Name);
+ options.CollectionName = parseResult.GetValueOrDefault(DocumentDbOptionDefinitions.CollectionName.Name);
+ options.Filter = parseResult.GetValueOrDefault(DocumentDbOptionDefinitions.Filter.Name);
+ options.Update = parseResult.GetValueOrDefault(DocumentDbOptionDefinitions.Update.Name);
+ options.Upsert = parseResult.GetValueOrDefault(DocumentDbOptionDefinitions.Upsert.Name);
+ return options;
+ }
+
+ public override async Task ExecuteAsync(
+ CommandContext context,
+ ParseResult parseResult,
+ CancellationToken cancellationToken)
+ {
+ FindAndModifyOptions? options = null;
+
+ try
+ {
+ if (!Validate(parseResult.CommandResult, context.Response).IsValid)
+ {
+ return context.Response;
+ }
+
+ options = BindOptions(parseResult);
+
+ var service = context.GetService();
+
+ var filter = DocumentDbHelpers.ParseBsonDocument(options.Filter);
+ var update = DocumentDbHelpers.ParseBsonDocument(options.Update);
+
+ if (filter == null || update == null)
+ {
+ throw new ArgumentException("Invalid filter or update format");
+ }
+
+ var result = await service.FindAndModifyAsync(options.ConnectionString!, options.DbName!, options.CollectionName!, filter, update, options.Upsert, cancellationToken);
+
+ // Process response using unified DocumentDbResponse type
+ DocumentDbResponseHelper.ProcessResponse(context, result);
+
+ return context.Response;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to find and modify document in collection: {CollectionName}, database: {DbName}, filter: {Filter}, update: {Update}, upsert: {Upsert}", options?.CollectionName, options?.DbName, options?.Filter, options?.Update, options?.Upsert);
+ HandleException(context, ex);
+ return context.Response;
+ }
+ }
+}
diff --git a/tools/Azure.Mcp.Tools.DocumentDb/src/Commands/Document/FindDocumentsCommand.cs b/tools/Azure.Mcp.Tools.DocumentDb/src/Commands/Document/FindDocumentsCommand.cs
new file mode 100644
index 0000000000..16a8e1f525
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.DocumentDb/src/Commands/Document/FindDocumentsCommand.cs
@@ -0,0 +1,93 @@
+// 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.Document;
+
+public sealed class FindDocumentsCommand(ILogger logger)
+ : BaseDocumentDbCommand()
+{
+ private readonly ILogger _logger = logger;
+
+ public override string Id => "f2a3b4c5-d6e7-4f2a-9b0c-1d2e3f4a5b6c";
+
+ public override string Name => "find_documents";
+
+ public override string Description => "Find and retrieve documents from a collection matching an optional filter. Supports limit, skip, sort, and projection options.";
+
+ public override string Title => "Find Documents";
+
+ 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);
+ command.Options.Add(DocumentDbOptionDefinitions.CollectionName);
+ command.Options.Add(DocumentDbOptionDefinitions.Filter);
+ command.Options.Add(DocumentDbOptionDefinitions.Options);
+ }
+
+ protected override FindDocumentsOptions BindOptions(ParseResult parseResult)
+ {
+ var options = base.BindOptions(parseResult);
+ options.DbName = parseResult.GetValueOrDefault(DocumentDbOptionDefinitions.DbName.Name);
+ options.CollectionName = parseResult.GetValueOrDefault(DocumentDbOptionDefinitions.CollectionName.Name);
+ options.Filter = parseResult.GetValueOrDefault(DocumentDbOptionDefinitions.Filter.Name);
+ options.Options = parseResult.GetValueOrDefault(DocumentDbOptionDefinitions.Options.Name);
+ return options;
+ }
+
+ public override async Task ExecuteAsync(
+ CommandContext context,
+ ParseResult parseResult,
+ CancellationToken cancellationToken)
+ {
+ FindDocumentsOptions? options = null;
+
+ try
+ {
+ if (!Validate(parseResult.CommandResult, context.Response).IsValid)
+ {
+ return context.Response;
+ }
+
+ options = BindOptions(parseResult);
+
+ var service = context.GetService();
+
+ var filter = DocumentDbHelpers.ParseBsonDocument(options.Filter);
+ var queryOptions = DocumentDbHelpers.ParseBsonDocument(options.Options);
+
+ var result = await service.FindDocumentsAsync(options.ConnectionString!, options.DbName!, options.CollectionName!, filter, queryOptions, cancellationToken);
+
+ // Process response using unified DocumentDbResponse type
+ DocumentDbResponseHelper.ProcessResponse(context, result);
+
+ return context.Response;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to find documents in collection: {CollectionName}, database: {DbName}, filter: {Filter}", options?.CollectionName, options?.DbName, options?.Filter);
+ HandleException(context, ex);
+ return context.Response;
+ }
+ }
+}
diff --git a/tools/Azure.Mcp.Tools.DocumentDb/src/Commands/Document/InsertDocumentsCommand.cs b/tools/Azure.Mcp.Tools.DocumentDb/src/Commands/Document/InsertDocumentsCommand.cs
new file mode 100644
index 0000000000..79559aa4b5
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.DocumentDb/src/Commands/Document/InsertDocumentsCommand.cs
@@ -0,0 +1,136 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.CommandLine;
+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.Extensions;
+using Microsoft.Mcp.Core.Models.Command;
+using MongoDB.Bson;
+
+namespace Azure.Mcp.Tools.DocumentDb.Commands.Document;
+
+public sealed class InsertDocumentsCommand(ILogger logger)
+ : BaseDocumentDbCommand()
+{
+ private readonly ILogger _logger = logger;
+
+ public override string Id => "5217faf1-1323-4e68-b599-e680caff0c70";
+
+ public override string Name => "insert_documents";
+
+ public override string Description => "Insert one document or many documents into a collection. If --mode is omitted, the command auto-detects a single document versus a JSON array payload.";
+
+ public override string Title => "Insert Documents";
+
+ public override ToolMetadata Metadata => new()
+ {
+ Destructive = false,
+ Idempotent = false,
+ OpenWorld = false,
+ ReadOnly = false,
+ Secret = false,
+ LocalRequired = false
+ };
+
+ protected override void RegisterOptions(Command command)
+ {
+ base.RegisterOptions(command);
+ command.Options.Add(DocumentDbOptionDefinitions.DbName);
+ command.Options.Add(DocumentDbOptionDefinitions.CollectionName);
+ command.Options.Add(DocumentDbOptionDefinitions.DocumentsPayload);
+ command.Options.Add(DocumentDbOptionDefinitions.Mode);
+ }
+
+ protected override InsertDocumentsOptions BindOptions(ParseResult parseResult)
+ {
+ var options = base.BindOptions(parseResult);
+ options.DbName = parseResult.GetValueOrDefault(DocumentDbOptionDefinitions.DbName.Name);
+ options.CollectionName = parseResult.GetValueOrDefault(DocumentDbOptionDefinitions.CollectionName.Name);
+ options.Documents = parseResult.GetValueOrDefault(DocumentDbOptionDefinitions.DocumentsPayload.Name);
+ options.Mode = parseResult.GetValueOrDefault(DocumentDbOptionDefinitions.Mode.Name);
+ return options;
+ }
+
+ public override async Task ExecuteAsync(CommandContext context, ParseResult parseResult, CancellationToken cancellationToken)
+ {
+ InsertDocumentsOptions? options = null;
+
+ try
+ {
+ if (!Validate(parseResult.CommandResult, context.Response).IsValid)
+ {
+ return context.Response;
+ }
+
+ options = BindOptions(parseResult);
+
+ var service = context.GetService();
+ var mode = ResolveMode(options.Documents, options.Mode, parseResult.CommandResult.HasOptionResult(DocumentDbOptionDefinitions.Mode));
+
+ var result = mode switch
+ {
+ "many" => await service.InsertManyAsync(options.ConnectionString!, options.DbName!, options.CollectionName!, ParseDocuments(options.Documents, mode), cancellationToken),
+ _ => await service.InsertDocumentAsync(options.ConnectionString!, options.DbName!, options.CollectionName!, ParseDocument(options.Documents, mode), cancellationToken)
+ };
+
+ DocumentDbResponseHelper.ProcessResponse(context, result);
+ return context.Response;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to insert documents into collection: {CollectionName}, database: {DbName}, mode: {Mode}", options?.CollectionName, options?.DbName, options?.Mode);
+ HandleException(context, ex);
+ return context.Response;
+ }
+ }
+
+ private static string ResolveMode(string? payload, string? mode, bool modeSpecified)
+ {
+ var looksLikeArray = payload?.TrimStart().StartsWith('[') == true;
+
+ if (!modeSpecified)
+ {
+ return looksLikeArray ? "many" : "single";
+ }
+
+ if (mode == "single" && looksLikeArray)
+ {
+ throw new ArgumentException("JSON array payloads require --mode many or omitting --mode for auto-detection.");
+ }
+
+ if (mode == "many" && !looksLikeArray)
+ {
+ throw new ArgumentException("--mode many requires a JSON array payload.");
+ }
+
+ return mode ?? "single";
+ }
+
+ private static BsonDocument ParseDocument(string? payload, string mode)
+ {
+ var document = DocumentDbHelpers.ParseBsonDocument(payload);
+
+ if (document == null)
+ {
+ throw new ArgumentException($"Invalid {mode} document payload.");
+ }
+
+ return document;
+ }
+
+ private static List ParseDocuments(string? payload, string mode)
+ {
+ var documents = DocumentDbHelpers.ParseBsonDocumentList(payload);
+
+ if (documents == null || documents.Count == 0)
+ {
+ throw new ArgumentException($"Invalid {mode} documents payload.");
+ }
+
+ return documents;
+ }
+}
diff --git a/tools/Azure.Mcp.Tools.DocumentDb/src/Commands/Document/UpdateDocumentsCommand.cs b/tools/Azure.Mcp.Tools.DocumentDb/src/Commands/Document/UpdateDocumentsCommand.cs
new file mode 100644
index 0000000000..d91dbf6804
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.DocumentDb/src/Commands/Document/UpdateDocumentsCommand.cs
@@ -0,0 +1,98 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.CommandLine;
+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.Document;
+
+public sealed class UpdateDocumentsCommand(ILogger logger)
+ : BaseDocumentDbCommand()
+{
+ private readonly ILogger _logger = logger;
+
+ public override string Id => "16632ed3-edcc-44c5-aab6-38ca63a15521";
+
+ public override string Name => "update_documents";
+
+ public override string Description => "Update one or more documents in a collection matching a required filter. If --mode is omitted, the command defaults to single. Use --mode many to update multiple documents.";
+
+ public override string Title => "Update Documents";
+
+ 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);
+ command.Options.Add(DocumentDbOptionDefinitions.CollectionName);
+ command.Options.Add(DocumentDbOptionDefinitions.RequiredFilter);
+ command.Options.Add(DocumentDbOptionDefinitions.Update);
+ command.Options.Add(DocumentDbOptionDefinitions.Upsert);
+ command.Options.Add(DocumentDbOptionDefinitions.Mode);
+ }
+
+ protected override UpdateDocumentsOptions BindOptions(ParseResult parseResult)
+ {
+ var options = base.BindOptions(parseResult);
+ options.DbName = parseResult.GetValueOrDefault(DocumentDbOptionDefinitions.DbName.Name);
+ options.CollectionName = parseResult.GetValueOrDefault(DocumentDbOptionDefinitions.CollectionName.Name);
+ options.Filter = parseResult.GetValueOrDefault(DocumentDbOptionDefinitions.Filter.Name);
+ options.Update = parseResult.GetValueOrDefault(DocumentDbOptionDefinitions.Update.Name);
+ options.Upsert = parseResult.GetValueOrDefault(DocumentDbOptionDefinitions.Upsert.Name);
+ options.Mode = parseResult.GetValueOrDefault(DocumentDbOptionDefinitions.Mode.Name);
+ return options;
+ }
+
+ public override async Task ExecuteAsync(CommandContext context, ParseResult parseResult, CancellationToken cancellationToken)
+ {
+ UpdateDocumentsOptions? options = null;
+
+ try
+ {
+ if (!Validate(parseResult.CommandResult, context.Response).IsValid)
+ {
+ return context.Response;
+ }
+
+ options = BindOptions(parseResult);
+
+ var service = context.GetService();
+ var filter = DocumentDbHelpers.ParseBsonDocument(options.Filter);
+ var update = DocumentDbHelpers.ParseBsonDocument(options.Update);
+
+ if (filter == null || update == null)
+ {
+ throw new ArgumentException("Invalid filter or update format");
+ }
+
+ var result = options.Mode switch
+ {
+ "many" => await service.UpdateManyAsync(options.ConnectionString!, options.DbName!, options.CollectionName!, filter, update, options.Upsert, cancellationToken),
+ _ => await service.UpdateDocumentAsync(options.ConnectionString!, options.DbName!, options.CollectionName!, filter, update, options.Upsert, cancellationToken)
+ };
+
+ DocumentDbResponseHelper.ProcessResponse(context, result);
+ return context.Response;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to update documents in collection: {CollectionName}, database: {DbName}, mode: {Mode}, upsert: {Upsert}", options?.CollectionName, options?.DbName, options?.Mode, options?.Upsert);
+ 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