Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/common/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ErrorCode extends ErrorCodes = ErrorCodes> extends Error {
Expand Down
19 changes: 19 additions & 0 deletions src/tools/mongodb/read/aggregate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import { formatUntrustedData } from "../../tool.js";
import { checkIndexUsage } from "../../../helpers/indexCheck.js";
import { EJSON } from "bson";
import { ErrorCodes, MongoDBError } from "../../../common/errors.js";
import { UserConfig } from "../../../common/config.js";

Check failure on line 9 in src/tools/mongodb/read/aggregate.ts

View workflow job for this annotation

GitHub Actions / check-style

'UserConfig' is defined but never used

Check failure on line 9 in src/tools/mongodb/read/aggregate.ts

View workflow job for this annotation

GitHub Actions / check-style

Value imports from config.ts are not allowed. Use dependency injection instead. Only type imports are permitted

export const AggregateArgs = {
pipeline: z.array(z.object({}).passthrough()).describe("An array of aggregation stages to execute"),
Expand All @@ -26,6 +28,8 @@
}: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {
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 () => {
Expand All @@ -44,4 +48,19 @@
),
};
}

private assertOnlyUsesPermittedStages(pipeline: Record<string, unknown>[]): 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."
);
}
}
}
}
34 changes: 34 additions & 0 deletions tests/integration/tools/mongodb/read/aggregate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
Loading