Skip to content

Commit 9751ff1

Browse files
committed
fix: Do not allow $out and $merge in readOnly mode
1 parent 6bfaad4 commit 9751ff1

File tree

3 files changed

+54
-0
lines changed

3 files changed

+54
-0
lines changed

src/common/errors.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export enum ErrorCodes {
22
NotConnectedToMongoDB = 1_000_000,
33
MisconfiguredConnectionString = 1_000_001,
44
ForbiddenCollscan = 1_000_002,
5+
ForbiddenWriteOperation = 1_000_003,
56
}
67

78
export class MongoDBError<ErrorCode extends ErrorCodes = ErrorCodes> extends Error {

src/tools/mongodb/read/aggregate.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import type { ToolArgs, OperationType } from "../../tool.js";
55
import { formatUntrustedData } from "../../tool.js";
66
import { checkIndexUsage } from "../../../helpers/indexCheck.js";
77
import { EJSON } from "bson";
8+
import { ErrorCodes, MongoDBError } from "../../../common/errors.js";
9+
import { UserConfig } from "../../../common/config.js";
810

911
export const AggregateArgs = {
1012
pipeline: z.array(z.object({}).passthrough()).describe("An array of aggregation stages to execute"),
@@ -26,6 +28,8 @@ export class AggregateTool extends MongoDBToolBase {
2628
}: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {
2729
const provider = await this.ensureConnected();
2830

31+
this.assertOnlyUsesPermittedStages(pipeline);
32+
2933
// Check if aggregate operation uses an index if enabled
3034
if (this.config.indexCheck) {
3135
await checkIndexUsage(provider, database, collection, "aggregate", async () => {
@@ -44,4 +48,19 @@ export class AggregateTool extends MongoDBToolBase {
4448
),
4549
};
4650
}
51+
52+
private assertOnlyUsesPermittedStages(pipeline: Record<string, unknown>[]): void {
53+
if (!this.config.readOnly) {
54+
return;
55+
}
56+
57+
for (const stage of pipeline) {
58+
if (stage.$out || stage.$merge) {
59+
throw new MongoDBError(
60+
ErrorCodes.ForbiddenWriteOperation,
61+
"In readOnly mode you can not run pipelines with $out or $merge stages."
62+
);
63+
}
64+
}
65+
}
4766
}

tests/integration/tools/mongodb/read/aggregate.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,40 @@ describeWithMongoDB("aggregate tool", (integration) => {
9595
);
9696
});
9797

98+
it("can not run $out stages in readOnly mode", async () => {
99+
await integration.connectMcpClient();
100+
integration.mcpServer().userConfig.readOnly = true;
101+
const response = await integration.mcpClient().callTool({
102+
name: "aggregate",
103+
arguments: {
104+
database: integration.randomDbName(),
105+
collection: "people",
106+
pipeline: [{ $out: "outpeople" }],
107+
},
108+
});
109+
const content = getResponseContent(response);
110+
expect(content).toEqual(
111+
"Error running aggregate: In readOnly mode you can not run pipelines with $out or $merge stages."
112+
);
113+
});
114+
115+
it("can not run $merge stages in readOnly mode", async () => {
116+
await integration.connectMcpClient();
117+
integration.mcpServer().userConfig.readOnly = true;
118+
const response = await integration.mcpClient().callTool({
119+
name: "aggregate",
120+
arguments: {
121+
database: integration.randomDbName(),
122+
collection: "people",
123+
pipeline: [{ $merge: "outpeople" }],
124+
},
125+
});
126+
const content = getResponseContent(response);
127+
expect(content).toEqual(
128+
"Error running aggregate: In readOnly mode you can not run pipelines with $out or $merge stages."
129+
);
130+
});
131+
98132
validateAutoConnectBehavior(integration, "aggregate", () => {
99133
return {
100134
args: {

0 commit comments

Comments
 (0)