diff --git a/agents/common/src/main/java/com/dbx/agent/AgentProtocol.java b/agents/common/src/main/java/com/dbx/agent/AgentProtocol.java index e0fb754b8..2dc672400 100644 --- a/agents/common/src/main/java/com/dbx/agent/AgentProtocol.java +++ b/agents/common/src/main/java/com/dbx/agent/AgentProtocol.java @@ -46,6 +46,7 @@ public final class AgentProtocol { public static final String MONGO_METHOD_SERVER_VERSION = "server_version"; public static final String MONGO_METHOD_CREATE_INDEX = "create_index"; public static final String MONGO_METHOD_DROP_INDEXES = "drop_indexes"; + public static final String MONGO_METHOD_DROP_COLLECTION = "drop_collection"; public static final String MONGO_METHOD_INSERT_DOCUMENT = "insert_document"; public static final String MONGO_METHOD_UPDATE_DOCUMENT = "update_document"; public static final String MONGO_METHOD_UPDATE_DOCUMENTS = "update_documents"; @@ -126,6 +127,7 @@ public final class AgentProtocol { MONGO_METHOD_SERVER_VERSION, MONGO_METHOD_CREATE_INDEX, MONGO_METHOD_DROP_INDEXES, + MONGO_METHOD_DROP_COLLECTION, MONGO_METHOD_INSERT_DOCUMENT, MONGO_METHOD_UPDATE_DOCUMENT, MONGO_METHOD_UPDATE_DOCUMENTS, diff --git a/agents/common/src/main/resources/agent-protocol-v1.json b/agents/common/src/main/resources/agent-protocol-v1.json index 28535c1f2..c10848306 100644 --- a/agents/common/src/main/resources/agent-protocol-v1.json +++ b/agents/common/src/main/resources/agent-protocol-v1.json @@ -72,6 +72,7 @@ "server_version", "create_index", "drop_indexes", + "drop_collection", "insert_document", "update_document", "update_documents", diff --git a/agents/drivers/mongodb/src/main/java/com/dbx/agent/mongodb/MongoAgent.java b/agents/drivers/mongodb/src/main/java/com/dbx/agent/mongodb/MongoAgent.java index 9d7a8ff5e..eee2af05c 100644 --- a/agents/drivers/mongodb/src/main/java/com/dbx/agent/mongodb/MongoAgent.java +++ b/agents/drivers/mongodb/src/main/java/com/dbx/agent/mongodb/MongoAgent.java @@ -504,6 +504,14 @@ public final class MongoAgent { return result; } + private static Object dropCollection(JsonObject params) { + MongoClient c = requireClient(); + String database = params.get("database").getAsString(); + String collection = params.get("collection").getAsString(); + c.getDatabase(database).getCollection(collection).drop(); + return Collections.singletonMap("ok", true); + } + private static Object parseDropIndexesValue(String indexesJson, boolean single) { if (indexesJson == null || indexesJson.isBlank()) { if (single) { @@ -808,6 +816,7 @@ public final class MongoAgent { case AgentProtocol.MONGO_METHOD_SERVER_VERSION -> serverVersion(params); case AgentProtocol.MONGO_METHOD_CREATE_INDEX -> createIndex(params); case AgentProtocol.MONGO_METHOD_DROP_INDEXES -> dropIndexes(params); + case AgentProtocol.MONGO_METHOD_DROP_COLLECTION -> dropCollection(params); case AgentProtocol.MONGO_METHOD_INSERT_DOCUMENT -> insertDocument(params); case AgentProtocol.MONGO_METHOD_UPDATE_DOCUMENT -> updateDocument(params); case AgentProtocol.MONGO_METHOD_UPDATE_DOCUMENTS -> updateDocuments(params); diff --git a/agents/drivers/mongodb/src/test/java/com/dbx/agent/mongodb/MongoAgentTest.java b/agents/drivers/mongodb/src/test/java/com/dbx/agent/mongodb/MongoAgentTest.java index bf1812603..17d9f3fc9 100644 --- a/agents/drivers/mongodb/src/test/java/com/dbx/agent/mongodb/MongoAgentTest.java +++ b/agents/drivers/mongodb/src/test/java/com/dbx/agent/mongodb/MongoAgentTest.java @@ -176,6 +176,19 @@ class MongoAgentTest { assertTrue(AgentProtocol.MONGO_LEGACY_METHODS.contains(AgentProtocol.MONGO_METHOD_DROP_INDEXES)); } + @Test + void dropCollectionMethodIsRecognizedOverJsonRpc() { + String response = MongoAgent.handleRequest( + "{\"jsonrpc\":\"2.0\",\"id\":14,\"method\":\"drop_collection\"," + + "\"params\":{\"database\":\"app\",\"collection\":\"orders\"}}"); + + JsonObject json = JsonParser.parseString(response).getAsJsonObject(); + assertEquals(14, json.get("id").getAsInt()); + assertEquals("Not connected", json.getAsJsonObject("error").get("message").getAsString()); + assertFalse(json.getAsJsonObject("error").get("message").getAsString().contains("Unknown method")); + assertTrue(AgentProtocol.MONGO_LEGACY_METHODS.contains(AgentProtocol.MONGO_METHOD_DROP_COLLECTION)); + } + @Test void updateDocumentsMethodIsRecognizedOverJsonRpc() { String response = MongoAgent.handleRequest( diff --git a/apps/desktop/src/lib/mongo/mongoShellCommand.ts b/apps/desktop/src/lib/mongo/mongoShellCommand.ts index 49f9efb1b..f7ea5fa69 100644 --- a/apps/desktop/src/lib/mongo/mongoShellCommand.ts +++ b/apps/desktop/src/lib/mongo/mongoShellCommand.ts @@ -39,7 +39,7 @@ export interface MongoCollectionStatsCommand { scale?: number; } -type MongoWriteKind = "insert" | "update" | "delete" | "createIndex" | "dropIndex" | "dropIndexes"; +type MongoWriteKind = "insert" | "update" | "delete" | "createIndex" | "dropIndex" | "dropIndexes" | "dropCollection"; export type MongoCommand = | ({ kind: "find" } & MongoFindCommand) @@ -54,7 +54,8 @@ export type MongoCommand = | { kind: "delete"; collection: string; filter: string; many: boolean } | { kind: "createIndex"; collection: string; keys: string; options?: string } | { kind: "dropIndex"; collection: string; index: string } - | { kind: "dropIndexes"; collection: string; indexes?: string }; + | { kind: "dropIndexes"; collection: string; indexes?: string } + | { kind: "dropCollection"; collection: string }; export type MongoWriteCommand = Extract; @@ -340,6 +341,13 @@ export function parseMongoWriteCommand(input: string): MongoWriteCommand | null return indexes !== null ? { kind: "dropIndexes", collection: dropIndexes.collection, ...(indexes ? { indexes } : {}) } : null; } + const dropCollection = parseCollectionMethodTarget(source, "drop"); + if (dropCollection) { + const args = parseMethodArgs(source, dropCollection.methodCallIndex); + if (!args || args.some((arg) => arg.trim())) return null; + return { kind: "dropCollection", collection: dropCollection.collection }; + } + return null; } @@ -427,6 +435,12 @@ export function evaluateMongoWriteSafety(command: MongoWriteCommand, options: Mo reason: "MongoDB dropIndexes() without a specific single index requires DBX_MCP_ALLOW_DANGEROUS_SQL=1.", }; } + if (!options.allowDangerous && command.kind === "dropCollection") { + return { + allowed: false, + reason: "MongoDB drop() requires DBX_MCP_ALLOW_DANGEROUS_SQL=1.", + }; + } return { allowed: true }; } diff --git a/apps/desktop/src/stores/queryStore.ts b/apps/desktop/src/stores/queryStore.ts index 1ca539ed2..174a58c39 100644 --- a/apps/desktop/src/stores/queryStore.ts +++ b/apps/desktop/src/stores/queryStore.ts @@ -2246,7 +2246,8 @@ export const useQueryStore = defineStore("query", () => { case "delete": case "createIndex": case "dropIndex": - case "dropIndexes": { + case "dropIndexes": + case "dropCollection": { if (options?.mongoSafety) { const safety = evaluateMongoWriteSafety(mongoCommand, options.mongoSafety); if (!safety.allowed) throw new Error(safety.reason); @@ -2270,6 +2271,9 @@ export const useQueryStore = defineStore("query", () => { } else if (mongoCommand.kind === "dropIndex" || mongoCommand.kind === "dropIndexes") { const result = await api.mongoDropIndexes(tab.connectionId, currentDatabase, mongoCommand.collection, mongoCommand.kind === "dropIndex" ? mongoCommand.index : mongoCommand.indexes, mongoCommand.kind === "dropIndex"); allResults.push(markQueryResultRowsRaw(annotateQueryResultSource(mongoDroppedIndexesToQueryResult(result.dropped_names, performance.now() - commandStartedAt), sourceStatement))); + } else if (mongoCommand.kind === "dropCollection") { + await api.mongoDropCollection(tab.connectionId, currentDatabase, mongoCommand.collection); + allResults.push(markQueryResultRowsRaw(annotateQueryResultSource(mongoWriteToQueryResult(1, performance.now() - commandStartedAt), sourceStatement))); } else { const result = await api.mongoDeleteDocuments(tab.connectionId, currentDatabase, mongoCommand.collection, mongoCommand.filter, mongoCommand.many); allResults.push(markQueryResultRowsRaw(annotateQueryResultSource(mongoWriteToQueryResult(result.affected_rows, performance.now() - commandStartedAt), sourceStatement))); diff --git a/crates/dbx-core/assets/agent-protocol-v1.json b/crates/dbx-core/assets/agent-protocol-v1.json index 28535c1f2..c10848306 100644 --- a/crates/dbx-core/assets/agent-protocol-v1.json +++ b/crates/dbx-core/assets/agent-protocol-v1.json @@ -72,6 +72,7 @@ "server_version", "create_index", "drop_indexes", + "drop_collection", "insert_document", "update_document", "update_documents", diff --git a/crates/dbx-core/src/db/agent_driver.rs b/crates/dbx-core/src/db/agent_driver.rs index a3a24922a..b38f142c1 100644 --- a/crates/dbx-core/src/db/agent_driver.rs +++ b/crates/dbx-core/src/db/agent_driver.rs @@ -256,6 +256,7 @@ pub enum MongoAgentMethod { ServerVersion, CreateIndex, DropIndexes, + DropCollection, InsertDocument, UpdateDocument, UpdateDocuments, @@ -264,7 +265,7 @@ pub enum MongoAgentMethod { } impl MongoAgentMethod { - pub const ALL: [Self; 12] = [ + pub const ALL: [Self; 13] = [ Self::ListDatabases, Self::ListCollections, Self::FindDocuments, @@ -272,6 +273,7 @@ impl MongoAgentMethod { Self::ServerVersion, Self::CreateIndex, Self::DropIndexes, + Self::DropCollection, Self::InsertDocument, Self::UpdateDocument, Self::UpdateDocuments, @@ -288,6 +290,7 @@ impl MongoAgentMethod { Self::ServerVersion => "server_version", Self::CreateIndex => "create_index", Self::DropIndexes => "drop_indexes", + Self::DropCollection => "drop_collection", Self::InsertDocument => "insert_document", Self::UpdateDocument => "update_document", Self::UpdateDocuments => "update_documents", @@ -1024,6 +1027,13 @@ impl AgentDriverClient { self.call_mongo_method(MongoAgentMethod::DropIndexes, params).await } + pub async fn mongo_drop_collection( + &mut self, + params: Value, + ) -> Result { + self.call_mongo_method(MongoAgentMethod::DropCollection, params).await + } + pub async fn mongo_insert_document( &mut self, params: Value, @@ -1698,6 +1708,7 @@ mod tests { assert_eq!(MongoAgentMethod::ServerVersion.as_str(), "server_version"); assert_eq!(MongoAgentMethod::CreateIndex.as_str(), "create_index"); assert_eq!(MongoAgentMethod::DropIndexes.as_str(), "drop_indexes"); + assert_eq!(MongoAgentMethod::DropCollection.as_str(), "drop_collection"); assert_eq!(MongoAgentMethod::InsertDocument.as_str(), "insert_document"); assert_eq!(MongoAgentMethod::UpdateDocument.as_str(), "update_document"); assert_eq!(MongoAgentMethod::UpdateDocuments.as_str(), "update_documents"); @@ -1744,6 +1755,7 @@ mod tests { let _mongo_server_version = AgentDriverClient::mongo_server_version::; let _mongo_create_index = AgentDriverClient::mongo_create_index::; let _mongo_drop_indexes = AgentDriverClient::mongo_drop_indexes::; + let _mongo_drop_collection = AgentDriverClient::mongo_drop_collection::; let _mongo_insert_document = AgentDriverClient::mongo_insert_document::; let _mongo_update_document = AgentDriverClient::mongo_update_document::; let _mongo_update_documents = AgentDriverClient::mongo_update_documents::; diff --git a/crates/dbx-core/src/mongo_ops.rs b/crates/dbx-core/src/mongo_ops.rs index 8f74d099e..e6ecea519 100644 --- a/crates/dbx-core/src/mongo_ops.rs +++ b/crates/dbx-core/src/mongo_ops.rs @@ -48,7 +48,16 @@ pub async fn mongo_drop_collection_core( let connections = state.connections.read().await; match connections.get(connection_id).ok_or("Not found")? { PoolKind::MongoDb(client) => mongo_driver::drop_collection(client, database, collection).await, - PoolKind::Agent(_) => Err("MongoDB legacy agent does not support drop collection".to_string()), + PoolKind::Agent(client) => { + let mut client = client.lock().await; + let _: serde_json::Value = client + .mongo_drop_collection(serde_json::json!({ + "database": database, + "collection": collection, + })) + .await?; + Ok(()) + } _ => Err("Not a MongoDB connection".to_string()), } } diff --git a/packages/app-tests/mongoShellCommand.test.ts b/packages/app-tests/mongoShellCommand.test.ts index 69d27076b..27fd5f8c0 100644 --- a/packages/app-tests/mongoShellCommand.test.ts +++ b/packages/app-tests/mongoShellCommand.test.ts @@ -184,6 +184,25 @@ test("parseMongoWriteCommand parses dropIndex and dropIndexes variants", () => { }); }); +test("parseMongoWriteCommand parses collection drop commands", () => { + assert.deepEqual(parseMongoWriteCommand("db.users.drop()"), { + kind: "dropCollection", + collection: "users", + }); + assert.deepEqual(parseMongoWriteCommand('db.getCollection("audit.logs").drop();'), { + kind: "dropCollection", + collection: "audit.logs", + }); + assert.deepEqual(parseMongoCommand("db.users.drop()")?.command, { + kind: "dropCollection", + collection: "users", + }); +}); + +test("parseMongoWriteCommand rejects collection drop arguments", () => { + assert.equal(parseMongoWriteCommand("db.users.drop({ writeConcern: 1 })"), null); +}); + test("parseMongoWriteCommand rejects invalid dropIndex/dropIndexes variants", () => { assert.equal(parseMongoWriteCommand("db.users.dropIndex()"), null); assert.equal(parseMongoWriteCommand('db.users.dropIndex("*")'), null); @@ -191,6 +210,13 @@ test("parseMongoWriteCommand rejects invalid dropIndex/dropIndexes variants", () assert.equal(parseMongoWriteCommand('db.users.dropIndexes([{"a":1}])'), null); }); +test("evaluateMongoWriteSafety blocks collection drop unless dangerous writes are enabled", () => { + const dropCollection = parseMongoWriteCommand("db.users.drop()"); + assert.ok(dropCollection); + assert.match(evaluateMongoWriteSafety(dropCollection, { allowWrites: true }).reason || "", /DBX_MCP_ALLOW_DANGEROUS_SQL=1/); + assert.equal(evaluateMongoWriteSafety(dropCollection, { allowWrites: true, allowDangerous: true }).allowed, true); +}); + test("evaluateMongoWriteSafety blocks dangerous dropIndexes shapes unless enabled", () => { const dropAll = parseMongoWriteCommand("db.users.dropIndexes()"); assert.ok(dropAll); diff --git a/packages/node-core/src/database.ts b/packages/node-core/src/database.ts index 2ce11dfe0..391077cdf 100644 --- a/packages/node-core/src/database.ts +++ b/packages/node-core/src/database.ts @@ -919,7 +919,7 @@ export async function executeQuery(config: ConnectionConfig, sql: string, option return { columns: [], rows: [], row_count: result.affectedRows }; } throw new Error( - 'Use MongoDB shell-style commands, for example: db.projects.find({}).limit(100), db.version(), db.projects.countDocuments({}), db.projects.count({}), db.projects.getIndexes(), db.projects.dataSize(), db.projects.storageSize(1024), db.projects.totalIndexSize(), db.projects.stats(), db.projects.createIndex({...}), db.projects.dropIndex("name"), db.projects.dropIndexes(), db.projects.insertOne({...}), db.projects.updateOne({...}, {$set: {...}}), or db.projects.deleteOne({...})', + 'Use MongoDB shell-style commands, for example: db.projects.find({}).limit(100), db.version(), db.projects.countDocuments({}), db.projects.count({}), db.projects.getIndexes(), db.projects.dataSize(), db.projects.storageSize(1024), db.projects.totalIndexSize(), db.projects.stats(), db.projects.createIndex({...}), db.projects.dropIndex("name"), db.projects.dropIndexes(), db.projects.drop(), db.projects.insertOne({...}), db.projects.updateOne({...}, {$set: {...}}), or db.projects.deleteOne({...})', ); } if (isDirectQueryType(config.db_type)) { @@ -1167,6 +1167,14 @@ async function executeMongoWrite(config: ConnectionConfig, command: MongoWriteCo }); return { affectedRows: result.affected_rows, droppedNames: result.dropped_names }; } + if (command.kind === "dropCollection") { + await bridgeDataRequest<{ ok: boolean }>("/data/mongo/drop-collection", { + connection_name: config.name, + database: config.database || "", + collection: command.collection, + }); + return { affectedRows: 1 }; + } const result = await bridgeDataRequest<{ affected_rows: number }>("/data/mongo/delete-documents", { connection_name: config.name, database: config.database || "", @@ -1288,7 +1296,8 @@ export type MongoWriteCommand = | { kind: "delete"; collection: string; filter: string; many: boolean } | { kind: "createIndex"; collection: string; keys: string; options?: string } | { kind: "dropIndex"; collection: string; index: string } - | { kind: "dropIndexes"; collection: string; indexes?: string }; + | { kind: "dropIndexes"; collection: string; indexes?: string } + | { kind: "dropCollection"; collection: string }; export function parseMongoFindCommand(input: string): MongoFindCommand | null { const source = input.trim().replace(/;$/, "").trim(); @@ -1480,6 +1489,13 @@ export function parseMongoWriteCommand(input: string): MongoWriteCommand | null return indexes !== null ? { kind: "dropIndexes", collection: dropIndexes.collection, ...(indexes ? { indexes } : {}) } : null; } + const dropCollection = parseCollectionMethodTarget(source, "drop"); + if (dropCollection) { + const args = parseMethodArgs(source, dropCollection.methodCallIndex); + if (!args || args.some((arg) => arg.trim())) return null; + return { kind: "dropCollection", collection: dropCollection.collection }; + } + return null; } @@ -1502,6 +1518,12 @@ export function evaluateMongoWriteSafety(command: MongoWriteCommand, options: { reason: "MongoDB dropIndexes() without a specific single index requires DBX_MCP_ALLOW_DANGEROUS_SQL=1.", }; } + if (!options.allowDangerous && command.kind === "dropCollection") { + return { + allowed: false, + reason: "MongoDB drop() requires DBX_MCP_ALLOW_DANGEROUS_SQL=1.", + }; + } return { allowed: true }; } diff --git a/packages/node-core/src/web-backend.ts b/packages/node-core/src/web-backend.ts index 6d486dc5f..46a56eb65 100644 --- a/packages/node-core/src/web-backend.ts +++ b/packages/node-core/src/web-backend.ts @@ -1,6 +1,22 @@ import type { ConnectionConfig } from "./connections.js"; import type { TableInfo, ColumnInfo, QueryOptions, QueryResult } from "./database.js"; -import { collectionListToTableInfos, evaluateMongoAggregateSafety, evaluateMongoWriteSafety, inferMongoColumns, mongoCollectionStatsToQueryResult, mongoDocumentsToQueryResult, parseMongoAggregateCommand, parseMongoCollectionStatsCommand, parseMongoCountDocumentsCommand, parseMongoFindCommand, parseMongoGetIndexesCommand, parseMongoVersionCommand, parseMongoWriteCommand, type CollectionInfo, type MongoWriteCommand } from "./database.js"; +import { + collectionListToTableInfos, + evaluateMongoAggregateSafety, + evaluateMongoWriteSafety, + inferMongoColumns, + mongoCollectionStatsToQueryResult, + mongoDocumentsToQueryResult, + parseMongoAggregateCommand, + parseMongoCollectionStatsCommand, + parseMongoCountDocumentsCommand, + parseMongoFindCommand, + parseMongoGetIndexesCommand, + parseMongoVersionCommand, + parseMongoWriteCommand, + type CollectionInfo, + type MongoWriteCommand, +} from "./database.js"; import type { RedisCommandOptions, RedisCommandResult } from "./redis-command.js"; import { sqlSafetyFromEnv } from "./sql-safety.js"; @@ -288,7 +304,7 @@ export async function executeQuery(config: ConnectionConfig, sql: string, option return { columns: [], rows: [], row_count: result.affectedRows }; } throw new Error( - "Use MongoDB shell-style commands, for example: db.projects.find({}).limit(100), db.version(), db.projects.countDocuments({}), db.projects.count({}), db.projects.getIndexes(), db.projects.dataSize(), db.projects.storageSize(1024), db.projects.totalIndexSize(), db.projects.stats(), db.projects.createIndex({...}), db.projects.dropIndex(\"name\"), db.projects.dropIndexes(), db.projects.insertOne({...}), db.projects.updateOne({...}, {$set: {...}}), or db.projects.deleteOne({...})", + 'Use MongoDB shell-style commands, for example: db.projects.find({}).limit(100), db.version(), db.projects.countDocuments({}), db.projects.count({}), db.projects.getIndexes(), db.projects.dataSize(), db.projects.storageSize(1024), db.projects.totalIndexSize(), db.projects.stats(), db.projects.createIndex({...}), db.projects.dropIndex("name"), db.projects.dropIndexes(), db.projects.drop(), db.projects.insertOne({...}), db.projects.updateOne({...}, {$set: {...}}), or db.projects.deleteOne({...})', ); } const res = await apiFetch("/api/query/execute", { @@ -328,10 +344,7 @@ export async function executeRedisCommand(config: ConnectionConfig, db: number, return (await res.json()) as RedisCommandResult; } -async function executeMongoWrite( - config: ConnectionConfig, - command: MongoWriteCommand, -): Promise<{ affectedRows: number; indexName?: string; droppedNames?: string[] }> { +async function executeMongoWrite(config: ConnectionConfig, command: MongoWriteCommand): Promise<{ affectedRows: number; indexName?: string; droppedNames?: string[] }> { if (command.kind === "insert") { const res = await apiFetch("/api/mongo/insert-documents", { method: "POST", @@ -388,6 +401,17 @@ async function executeMongoWrite( const result = (await res.json()) as { dropped_names: string[]; affected_rows: number }; return { affectedRows: result.affected_rows, droppedNames: result.dropped_names }; } + if (command.kind === "dropCollection") { + await apiFetch("/api/mongo/drop-collection", { + method: "POST", + body: JSON.stringify({ + connectionId: config.id, + database: config.database || "", + collection: command.collection, + }), + }); + return { affectedRows: 1 }; + } const res = await apiFetch("/api/mongo/delete-documents", { method: "POST", body: JSON.stringify({ diff --git a/packages/node-core/tests/mongo-query.test.ts b/packages/node-core/tests/mongo-query.test.ts index 5905bc41a..ee24ca63d 100644 --- a/packages/node-core/tests/mongo-query.test.ts +++ b/packages/node-core/tests/mongo-query.test.ts @@ -1,6 +1,19 @@ import assert from "node:assert/strict"; import { test } from "vitest"; -import { executeQuery, inferMongoColumns, mongoAggregateWriteStage, mongoCollectionStatsToQueryResult, mongoDocumentsToQueryResult, parseMongoAggregateCommand, parseMongoCollectionStatsCommand, parseMongoCountDocumentsCommand, parseMongoFindCommand, parseMongoGetIndexesCommand, parseMongoVersionCommand, parseMongoWriteCommand } from "../src/database.js"; +import { + executeQuery, + inferMongoColumns, + mongoAggregateWriteStage, + mongoCollectionStatsToQueryResult, + mongoDocumentsToQueryResult, + parseMongoAggregateCommand, + parseMongoCollectionStatsCommand, + parseMongoCountDocumentsCommand, + parseMongoFindCommand, + parseMongoGetIndexesCommand, + parseMongoVersionCommand, + parseMongoWriteCommand, +} from "../src/database.js"; test("parseMongoFindCommand accepts shell-style find commands", () => { assert.deepEqual(parseMongoFindCommand('db.getCollection("operation_logs").find({"level":"info"}).sort({"ts":-1}).skip(5).limit(10)'), { @@ -233,6 +246,14 @@ test("parseMongoWriteCommand accepts supported write commands", () => { collection: "projects", indexes: '["a_1","b_1"]', }); + assert.deepEqual(parseMongoWriteCommand("db.projects.drop()"), { + kind: "dropCollection", + collection: "projects", + }); + assert.deepEqual(parseMongoWriteCommand('db.getCollection("audit.logs").drop();'), { + kind: "dropCollection", + collection: "audit.logs", + }); }); test("parseMongoWriteCommand rejects invalid dropIndex and dropIndexes commands", () => { @@ -240,6 +261,7 @@ test("parseMongoWriteCommand rejects invalid dropIndex and dropIndexes commands" assert.equal(parseMongoWriteCommand('db.projects.dropIndex("*")'), null); assert.equal(parseMongoWriteCommand('db.projects.dropIndex(["a_1"])'), null); assert.equal(parseMongoWriteCommand('db.projects.dropIndexes([{"email":1}])'), null); + assert.equal(parseMongoWriteCommand("db.projects.drop({ writeConcern: 1 })"), null); }); test("mongodb executeQuery blocks writes when writes are explicitly disabled", async () => { @@ -339,6 +361,7 @@ test("mongodb executeQuery blocks dangerous dropIndexes shapes until dangerous S await assert.rejects(executeQuery(config, "db.projects.dropIndexes()"), /DBX_MCP_ALLOW_DANGEROUS_SQL=1/); await assert.rejects(executeQuery(config, 'db.projects.dropIndexes("*")'), /DBX_MCP_ALLOW_DANGEROUS_SQL=1/); await assert.rejects(executeQuery(config, 'db.projects.dropIndexes(["a_1","b_1"])'), /DBX_MCP_ALLOW_DANGEROUS_SQL=1/); + await assert.rejects(executeQuery(config, "db.projects.drop()"), /DBX_MCP_ALLOW_DANGEROUS_SQL=1/); if (oldAllowWrites === undefined) delete process.env.DBX_MCP_ALLOW_WRITES; else process.env.DBX_MCP_ALLOW_WRITES = oldAllowWrites; diff --git a/src-tauri/src/commands/mcp_bridge.rs b/src-tauri/src/commands/mcp_bridge.rs index 2f8607661..9ab09dc76 100644 --- a/src-tauri/src/commands/mcp_bridge.rs +++ b/src-tauri/src/commands/mcp_bridge.rs @@ -98,6 +98,13 @@ struct MongoDropIndexesRequest { single: bool, } +#[derive(Deserialize)] +struct MongoDropCollectionRequest { + connection_name: String, + database: Option, + collection: String, +} + #[derive(Deserialize)] struct MongoInsertDocumentsRequest { connection_name: String, @@ -203,6 +210,8 @@ pub fn start(app_handle: AppHandle, state: Arc, data_dir: PathBuf) { handle_mongo_create_index_data(&st, body, &mut stream).await; } else if first_line.starts_with("POST /data/mongo/drop-indexes") { handle_mongo_drop_indexes_data(&st, body, &mut stream).await; + } else if first_line.starts_with("POST /data/mongo/drop-collection") { + handle_mongo_drop_collection_data(&st, body, &mut stream).await; } else if first_line.starts_with("POST /data/mongo/insert-documents") { handle_mongo_insert_documents_data(&st, body, &mut stream).await; } else if first_line.starts_with("POST /data/mongo/update-documents") { @@ -628,6 +637,29 @@ async fn handle_mongo_drop_indexes_data(state: &Arc, body: &str, strea } } +async fn handle_mongo_drop_collection_data(state: &Arc, body: &str, stream: &mut tokio::net::TcpStream) { + let req: MongoDropCollectionRequest = match serde_json::from_str(body) { + Ok(r) => r, + Err(_) => { + respond_error(stream, "400 Bad Request", "Invalid JSON").await; + return; + } + }; + let Some((pool_key, database, connection_id)) = + resolve_mongo_pool_key(state, &req.connection_name, req.database, stream).await + else { + return; + }; + if let Err(e) = ensure_connection_writable(state, &connection_id, "Drop collection").await { + respond_error(stream, "403 Forbidden", &e).await; + return; + } + match dbx_core::mongo_ops::mongo_drop_collection_core(state, &pool_key, &database, &req.collection).await { + Ok(()) => respond_json(stream, &serde_json::json!({ "ok": true })).await, + Err(e) => respond_error(stream, "500 Internal Server Error", &e).await, + } +} + async fn handle_mongo_insert_documents_data(state: &Arc, body: &str, stream: &mut tokio::net::TcpStream) { let req: MongoInsertDocumentsRequest = match serde_json::from_str(body) { Ok(r) => r,