diff --git a/src/tools/mongodb/read/aggregate.ts b/src/tools/mongodb/read/aggregate.ts index c6160345..45df4547 100644 --- a/src/tools/mongodb/read/aggregate.ts +++ b/src/tools/mongodb/read/aggregate.ts @@ -49,16 +49,23 @@ export class AggregateTool extends MongoDBToolBase { } private assertOnlyUsesPermittedStages(pipeline: Record[]): void { - if (!this.config.readOnly) { + const writeOperations: OperationType[] = ["update", "create", "delete"]; + let writeStageForbiddenError = ""; + + if (this.config.readOnly) { + writeStageForbiddenError = "In readOnly mode you can not run pipelines with $out or $merge stages."; + } else if (this.config.disabledTools.some((t) => writeOperations.includes(t as OperationType))) { + writeStageForbiddenError = + "When 'create', 'update', or 'delete' operations are disabled, you can not run pipelines with $out or $merge stages."; + } + + if (!writeStageForbiddenError) { return; } for (const stage of pipeline) { if (stage.$out || stage.$merge) { - throw new MongoDBError( - ErrorCodes.ForbiddenWriteOperation, - "In readOnly mode you can not run pipelines with $out or $merge stages." - ); + throw new MongoDBError(ErrorCodes.ForbiddenWriteOperation, writeStageForbiddenError); } } } diff --git a/tests/integration/tools/mongodb/read/aggregate.test.ts b/tests/integration/tools/mongodb/read/aggregate.test.ts index 643c5ef3..57c7f8c7 100644 --- a/tests/integration/tools/mongodb/read/aggregate.test.ts +++ b/tests/integration/tools/mongodb/read/aggregate.test.ts @@ -4,10 +4,15 @@ import { validateThrowsForInvalidArguments, getResponseContent, } from "../../../helpers.js"; -import { expect, it } from "vitest"; +import { expect, it, afterEach } from "vitest"; import { describeWithMongoDB, getDocsFromUntrustedContent, validateAutoConnectBehavior } from "../mongodbHelpers.js"; describeWithMongoDB("aggregate tool", (integration) => { + afterEach(() => { + integration.mcpServer().userConfig.readOnly = false; + integration.mcpServer().userConfig.disabledTools = []; + }); + validateToolMetadata(integration, "aggregate", "Run an aggregation against a MongoDB collection", [ ...databaseCollectionParameters, { @@ -129,6 +134,42 @@ describeWithMongoDB("aggregate tool", (integration) => { ); }); + for (const disabledOpType of ["create", "update", "delete"] as const) { + it(`can not run $out stages when ${disabledOpType} operation is disabled`, async () => { + await integration.connectMcpClient(); + integration.mcpServer().userConfig.disabledTools = [disabledOpType]; + const response = await integration.mcpClient().callTool({ + name: "aggregate", + arguments: { + database: integration.randomDbName(), + collection: "people", + pipeline: [{ $out: "outpeople" }], + }, + }); + const content = getResponseContent(response); + expect(content).toEqual( + "Error running aggregate: When 'create', 'update', or 'delete' operations are disabled, you can not run pipelines with $out or $merge stages." + ); + }); + + it(`can not run $merge stages when ${disabledOpType} operation is disabled`, async () => { + await integration.connectMcpClient(); + integration.mcpServer().userConfig.disabledTools = [disabledOpType]; + const response = await integration.mcpClient().callTool({ + name: "aggregate", + arguments: { + database: integration.randomDbName(), + collection: "people", + pipeline: [{ $merge: "outpeople" }], + }, + }); + const content = getResponseContent(response); + expect(content).toEqual( + "Error running aggregate: When 'create', 'update', or 'delete' operations are disabled, you can not run pipelines with $out or $merge stages." + ); + }); + } + validateAutoConnectBehavior(integration, "aggregate", () => { return { args: {