diff --git a/src/common/errors.ts b/src/common/errors.ts index 084d45ca7..1ef987de4 100644 --- a/src/common/errors.ts +++ b/src/common/errors.ts @@ -2,6 +2,7 @@ export enum ErrorCodes { NotConnectedToMongoDB = 1_000_000, MisconfiguredConnectionString = 1_000_001, ForbiddenCollscan = 1_000_002, + ForbiddenWriteOperation = 1_000_003, } export class MongoDBError extends Error { diff --git a/src/tools/mongodb/read/aggregate.ts b/src/tools/mongodb/read/aggregate.ts index 8492a61ce..c61603459 100644 --- a/src/tools/mongodb/read/aggregate.ts +++ b/src/tools/mongodb/read/aggregate.ts @@ -5,6 +5,7 @@ import type { ToolArgs, OperationType } from "../../tool.js"; import { formatUntrustedData } from "../../tool.js"; import { checkIndexUsage } from "../../../helpers/indexCheck.js"; import { EJSON } from "bson"; +import { ErrorCodes, MongoDBError } from "../../../common/errors.js"; export const AggregateArgs = { pipeline: z.array(z.object({}).passthrough()).describe("An array of aggregation stages to execute"), @@ -26,6 +27,8 @@ export class AggregateTool extends MongoDBToolBase { }: ToolArgs): Promise { const provider = await this.ensureConnected(); + this.assertOnlyUsesPermittedStages(pipeline); + // Check if aggregate operation uses an index if enabled if (this.config.indexCheck) { await checkIndexUsage(provider, database, collection, "aggregate", async () => { @@ -44,4 +47,19 @@ export class AggregateTool extends MongoDBToolBase { ), }; } + + private assertOnlyUsesPermittedStages(pipeline: Record[]): void { + if (!this.config.readOnly) { + 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." + ); + } + } + } } diff --git a/tests/integration/tools/mongodb/read/aggregate.test.ts b/tests/integration/tools/mongodb/read/aggregate.test.ts index fbe72ae80..643c5ef37 100644 --- a/tests/integration/tools/mongodb/read/aggregate.test.ts +++ b/tests/integration/tools/mongodb/read/aggregate.test.ts @@ -95,6 +95,40 @@ describeWithMongoDB("aggregate tool", (integration) => { ); }); + it("can not run $out stages in readOnly mode", async () => { + await integration.connectMcpClient(); + integration.mcpServer().userConfig.readOnly = true; + 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: In readOnly mode you can not run pipelines with $out or $merge stages." + ); + }); + + it("can not run $merge stages in readOnly mode", async () => { + await integration.connectMcpClient(); + integration.mcpServer().userConfig.readOnly = true; + 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: In readOnly mode you can not run pipelines with $out or $merge stages." + ); + }); + validateAutoConnectBehavior(integration, "aggregate", () => { return { args: {