Skip to content

Commit 14176ba

Browse files
authored
fix(readOnly): Do not allow $out and $merge in readOnly mode (#525)
1 parent 0912f39 commit 14176ba

File tree

3 files changed

+53
-0
lines changed

3 files changed

+53
-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: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ 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";
89

910
export const AggregateArgs = {
1011
pipeline: z.array(z.object({}).passthrough()).describe("An array of aggregation stages to execute"),
@@ -26,6 +27,8 @@ export class AggregateTool extends MongoDBToolBase {
2627
}: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {
2728
const provider = await this.ensureConnected();
2829

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

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)