Skip to content

Commit 9d9b9f8

Browse files
chore: PR feedback
1. implements disabling maxDocumentsPerQuery and maxBytesPerQuery 2. use correct numbers for bytes calculation
1 parent 937908b commit 9d9b9f8

File tree

7 files changed

+294
-172
lines changed

7 files changed

+294
-172
lines changed

src/common/config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ export const defaultUserConfig: UserConfig = {
183183
notificationTimeoutMs: 540000, // 9 minutes
184184
httpHeaders: {},
185185
maxDocumentsPerQuery: 10, // By default, we only fetch a maximum 10 documents per query / aggregation
186-
maxBytesPerQuery: 1 * 1000 * 1000, // By default, we only return ~1 mb of data per query / aggregation
186+
maxBytesPerQuery: 1 * 1024 * 1024, // By default, we only return ~1 mb of data per query / aggregation
187187
};
188188

189189
export const config = setupUserConfig({

src/helpers/iterateCursor.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ export async function iterateCursorUntilMaxBytes(
1212
cursor: FindCursor<unknown> | AggregationCursor<unknown>,
1313
maxBytesPerQuery: number
1414
): Promise<unknown[]> {
15+
// Setting configured limit to zero or negative is equivalent to disabling
16+
// the max bytes limit applied on tool responses.
17+
if (maxBytesPerQuery <= 0) {
18+
return await cursor.toArray();
19+
}
20+
1521
let biggestDocSizeSoFar = 0;
1622
let totalBytes = 0;
1723
const bufferedDocuments: unknown[] = [];

src/tools/mongodb/read/aggregate.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,11 @@ export class AggregateTool extends MongoDBToolBase {
4545
});
4646
}
4747

48-
const cappedResultsPipeline = [...pipeline, { $limit: this.config.maxDocumentsPerQuery }];
49-
aggregationCursor = provider
50-
.aggregate(database, collection, cappedResultsPipeline)
51-
.batchSize(this.config.maxDocumentsPerQuery);
48+
const cappedResultsPipeline = [...pipeline];
49+
if (this.config.maxDocumentsPerQuery > 0) {
50+
cappedResultsPipeline.push({ $limit: this.config.maxDocumentsPerQuery });
51+
}
52+
aggregationCursor = provider.aggregate(database, collection, cappedResultsPipeline);
5253

5354
const [totalDocuments, documents] = await Promise.all([
5455
this.countAggregationResultDocuments({ provider, database, collection, pipeline }),

src/tools/mongodb/read/find.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,12 @@ export class FindTool extends MongoDBToolBase {
6161
});
6262
}
6363

64-
const limitOnFindCursor = Math.min(limit ?? Number.POSITIVE_INFINITY, this.config.maxDocumentsPerQuery);
64+
const limitOnFindCursor = this.getLimitForFindCursor(limit);
65+
6566
findCursor = provider.find(database, collection, filter, {
6667
projection,
6768
limit: limitOnFindCursor,
6869
sort,
69-
batchSize: limitOnFindCursor,
7070
});
7171

7272
const [queryResultsCount, documents] = await Promise.all([
@@ -75,7 +75,8 @@ export class FindTool extends MongoDBToolBase {
7575
provider.countDocuments(database, collection, filter, {
7676
// We should be counting documents that the original
7777
// query would have yielded which is why we don't
78-
// use `limitOnFindCursor` calculated above.
78+
// use `limitOnFindCursor` calculated above, only
79+
// the limit provided to the tool.
7980
limit,
8081
maxTimeMS: QUERY_COUNT_MAX_TIME_MS_CAP,
8182
}),
@@ -104,4 +105,17 @@ Note to LLM: If entire query result is needed then use "export" tool to export t
104105
await findCursor?.close();
105106
}
106107
}
108+
109+
private getLimitForFindCursor(providedLimit: number | undefined): number | undefined {
110+
const configuredLimit = this.config.maxDocumentsPerQuery;
111+
// Setting configured limit to negative or zero is equivalent to
112+
// disabling the max limit applied on documents
113+
if (configuredLimit <= 0) {
114+
return providedLimit;
115+
}
116+
117+
return providedLimit === null || providedLimit === undefined
118+
? configuredLimit
119+
: Math.min(providedLimit, configuredLimit);
120+
}
107121
}

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

Lines changed: 131 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
getResponseContent,
66
defaultTestConfig,
77
} from "../../../helpers.js";
8-
import { describe, expect, it, vi } from "vitest";
8+
import { beforeEach, describe, expect, it, vi, afterEach } from "vitest";
99
import { describeWithMongoDB, getDocsFromUntrustedContent, validateAutoConnectBehavior } from "../mongodbHelpers.js";
1010
import * as constants from "../../../../../src/helpers/constants.js";
1111
import { freshInsertDocuments } from "./find.test.js";
@@ -142,90 +142,94 @@ describeWithMongoDB("aggregate tool", (integration) => {
142142
expectedResponse: "The aggregation resulted in 0 documents",
143143
};
144144
});
145+
146+
describe("when counting documents exceed the configured count maxTimeMS", () => {
147+
beforeEach(async () => {
148+
await freshInsertDocuments({
149+
collection: integration.mongoClient().db(integration.randomDbName()).collection("people"),
150+
count: 1000,
151+
documentMapper(index) {
152+
return { name: `Person ${index}`, age: index };
153+
},
154+
});
155+
});
156+
157+
afterEach(() => {
158+
vi.resetAllMocks();
159+
});
160+
161+
it("should abort discard count operation and respond with indeterminable count", async () => {
162+
vi.spyOn(constants, "AGG_COUNT_MAX_TIME_MS_CAP", "get").mockReturnValue(0.1);
163+
await integration.connectMcpClient();
164+
const response = await integration.mcpClient().callTool({
165+
name: "aggregate",
166+
arguments: {
167+
database: integration.randomDbName(),
168+
collection: "people",
169+
pipeline: [{ $match: { age: { $gte: 10 } } }, { $sort: { name: -1 } }],
170+
},
171+
});
172+
const content = getResponseContent(response);
173+
expect(content).toContain("The aggregation resulted in indeterminable number of documents");
174+
expect(content).toContain(`Returning 10 documents while respecting the applied limits.`);
175+
const docs = getDocsFromUntrustedContent(content);
176+
expect(docs[0]).toEqual(
177+
expect.objectContaining({
178+
_id: expect.any(Object) as object,
179+
name: "Person 999",
180+
age: 999,
181+
})
182+
);
183+
expect(docs[1]).toEqual(
184+
expect.objectContaining({
185+
_id: expect.any(Object) as object,
186+
name: "Person 998",
187+
age: 998,
188+
})
189+
);
190+
});
191+
});
145192
});
146193

147194
describeWithMongoDB(
148195
"aggregate tool with configured max documents per query",
149196
(integration) => {
150-
describe("when the aggregation results are larger than the configured limit", () => {
151-
it("should return documents limited to the configured limit", async () => {
152-
await freshInsertDocuments({
153-
collection: integration.mongoClient().db(integration.randomDbName()).collection("people"),
154-
count: 1000,
155-
documentMapper(index) {
156-
return { name: `Person ${index}`, age: index };
157-
},
158-
});
159-
await integration.connectMcpClient();
160-
const response = await integration.mcpClient().callTool({
161-
name: "aggregate",
162-
arguments: {
163-
database: integration.randomDbName(),
164-
collection: "people",
165-
pipeline: [{ $match: { age: { $gte: 10 } } }, { $sort: { name: -1 } }],
166-
},
167-
});
168-
169-
const content = getResponseContent(response);
170-
expect(content).toContain("The aggregation resulted in 990 documents");
171-
expect(content).toContain(`Returning 20 documents while respecting the applied limits.`);
172-
const docs = getDocsFromUntrustedContent(content);
173-
expect(docs[0]).toEqual(
174-
expect.objectContaining({
175-
_id: expect.any(Object) as object,
176-
name: "Person 999",
177-
age: 999,
178-
})
179-
);
180-
expect(docs[1]).toEqual(
181-
expect.objectContaining({
182-
_id: expect.any(Object) as object,
183-
name: "Person 998",
184-
age: 998,
185-
})
186-
);
197+
it("should return documents limited to the configured limit", async () => {
198+
await freshInsertDocuments({
199+
collection: integration.mongoClient().db(integration.randomDbName()).collection("people"),
200+
count: 1000,
201+
documentMapper(index) {
202+
return { name: `Person ${index}`, age: index };
203+
},
187204
});
188-
});
189-
190-
describe("when counting documents exceed the configured count maxTimeMS", () => {
191-
it("should abort discard count operation and respond with indeterminable count", async () => {
192-
vi.spyOn(constants, "AGG_COUNT_MAX_TIME_MS_CAP", "get").mockReturnValue(0.1);
193-
await freshInsertDocuments({
194-
collection: integration.mongoClient().db(integration.randomDbName()).collection("people"),
195-
count: 1000,
196-
documentMapper(index) {
197-
return { name: `Person ${index}`, age: index };
198-
},
199-
});
200-
await integration.connectMcpClient();
201-
const response = await integration.mcpClient().callTool({
202-
name: "aggregate",
203-
arguments: {
204-
database: integration.randomDbName(),
205-
collection: "people",
206-
pipeline: [{ $match: { age: { $gte: 10 } } }, { $sort: { name: -1 } }],
207-
},
208-
});
209-
const content = getResponseContent(response);
210-
expect(content).toContain("The aggregation resulted in indeterminable number of documents");
211-
expect(content).toContain(`Returning 20 documents while respecting the applied limits.`);
212-
const docs = getDocsFromUntrustedContent(content);
213-
expect(docs[0]).toEqual(
214-
expect.objectContaining({
215-
_id: expect.any(Object) as object,
216-
name: "Person 999",
217-
age: 999,
218-
})
219-
);
220-
expect(docs[1]).toEqual(
221-
expect.objectContaining({
222-
_id: expect.any(Object) as object,
223-
name: "Person 998",
224-
age: 998,
225-
})
226-
);
227-
vi.resetAllMocks();
205+
await integration.connectMcpClient();
206+
const response = await integration.mcpClient().callTool({
207+
name: "aggregate",
208+
arguments: {
209+
database: integration.randomDbName(),
210+
collection: "people",
211+
pipeline: [{ $match: { age: { $gte: 10 } } }, { $sort: { name: -1 } }],
212+
},
228213
});
214+
215+
const content = getResponseContent(response);
216+
expect(content).toContain("The aggregation resulted in 990 documents");
217+
expect(content).toContain(`Returning 20 documents while respecting the applied limits.`);
218+
const docs = getDocsFromUntrustedContent(content);
219+
expect(docs[0]).toEqual(
220+
expect.objectContaining({
221+
_id: expect.any(Object) as object,
222+
name: "Person 999",
223+
age: 999,
224+
})
225+
);
226+
expect(docs[1]).toEqual(
227+
expect.objectContaining({
228+
_id: expect.any(Object) as object,
229+
name: "Person 998",
230+
age: 998,
231+
})
232+
);
229233
});
230234
},
231235
() => ({ ...defaultTestConfig, maxDocumentsPerQuery: 20 })
@@ -234,30 +238,57 @@ describeWithMongoDB(
234238
describeWithMongoDB(
235239
"aggregate tool with configured max bytes per query",
236240
(integration) => {
237-
describe("when the provided maxBytesPerQuery is hit", () => {
238-
it("should return only the documents that could fit in maxBytesPerQuery limit", async () => {
239-
await freshInsertDocuments({
240-
collection: integration.mongoClient().db(integration.randomDbName()).collection("people"),
241-
count: 1000,
242-
documentMapper(index) {
243-
return { name: `Person ${index}`, age: index };
244-
},
245-
});
246-
await integration.connectMcpClient();
247-
const response = await integration.mcpClient().callTool({
248-
name: "aggregate",
249-
arguments: {
250-
database: integration.randomDbName(),
251-
collection: "people",
252-
pipeline: [{ $match: { age: { $gte: 10 } } }, { $sort: { name: -1 } }],
253-
},
254-
});
255-
256-
const content = getResponseContent(response);
257-
expect(content).toContain("The aggregation resulted in 990 documents");
258-
expect(content).toContain(`Returning 1 documents while respecting the applied limits.`);
241+
it("should return only the documents that could fit in maxBytesPerQuery limit", async () => {
242+
await freshInsertDocuments({
243+
collection: integration.mongoClient().db(integration.randomDbName()).collection("people"),
244+
count: 1000,
245+
documentMapper(index) {
246+
return { name: `Person ${index}`, age: index };
247+
},
248+
});
249+
await integration.connectMcpClient();
250+
const response = await integration.mcpClient().callTool({
251+
name: "aggregate",
252+
arguments: {
253+
database: integration.randomDbName(),
254+
collection: "people",
255+
pipeline: [{ $match: { age: { $gte: 10 } } }, { $sort: { name: -1 } }],
256+
},
259257
});
258+
259+
const content = getResponseContent(response);
260+
expect(content).toContain("The aggregation resulted in 990 documents");
261+
expect(content).toContain(`Returning 1 documents while respecting the applied limits.`);
260262
});
261263
},
262264
() => ({ ...defaultTestConfig, maxBytesPerQuery: 100 })
263265
);
266+
267+
describeWithMongoDB(
268+
"aggregate tool with disabled max documents and max bytes per query",
269+
(integration) => {
270+
it("should return all the documents", async () => {
271+
await freshInsertDocuments({
272+
collection: integration.mongoClient().db(integration.randomDbName()).collection("people"),
273+
count: 1000,
274+
documentMapper(index) {
275+
return { name: `Person ${index}`, age: index };
276+
},
277+
});
278+
await integration.connectMcpClient();
279+
const response = await integration.mcpClient().callTool({
280+
name: "aggregate",
281+
arguments: {
282+
database: integration.randomDbName(),
283+
collection: "people",
284+
pipeline: [{ $match: { age: { $gte: 10 } } }, { $sort: { name: -1 } }],
285+
},
286+
});
287+
288+
const content = getResponseContent(response);
289+
expect(content).toContain("The aggregation resulted in 990 documents");
290+
expect(content).toContain(`Returning 990 documents while respecting the applied limits.`);
291+
});
292+
},
293+
() => ({ ...defaultTestConfig, maxDocumentsPerQuery: -1, maxBytesPerQuery: -1 })
294+
);

0 commit comments

Comments
 (0)