Skip to content

Commit 03f3bc5

Browse files
committed
Merge branch 'main' of github.com:mongodb-js/mongodb-mcp-server into gagik/add-depcheck
2 parents beee492 + 163b333 commit 03f3bc5

29 files changed

+1362
-203
lines changed

README.md

Lines changed: 25 additions & 23 deletions
Large diffs are not rendered by default.

package-lock.json

Lines changed: 116 additions & 82 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "mongodb-mcp-server",
33
"description": "MongoDB Model Context Protocol Server",
4-
"version": "1.0.0",
4+
"version": "1.0.1",
55
"type": "module",
66
"exports": {
77
".": {

scripts/accuracy/runAccuracyTests.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export MDB_ACCURACY_RUN_ID=$(npx uuid v4)
1717
# specified in the command line. Such as:
1818
# npm run test:accuracy -- tests/accuracy/some-test.test.ts
1919
echo "Running accuracy tests with MDB_ACCURACY_RUN_ID '$MDB_ACCURACY_RUN_ID'"
20-
vitest --config vitest.config.ts --project=accuracy --coverage=false --run "$@"
20+
vitest --config vitest.config.ts --project=accuracy --coverage=false --no-file-parallelism --run "$@"
2121

2222
# Preserving the exit code from test run to correctly notify in the CI
2323
# environments when the tests fail.

src/common/config.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import levenshtein from "ts-levenshtein";
99

1010
// From: https://github.com/mongodb-js/mongosh/blob/main/packages/cli-repl/src/arg-parser.ts
1111
const OPTIONS = {
12+
number: ["maxDocumentsPerQuery", "maxBytesPerQuery"],
1213
string: [
1314
"apiBaseUrl",
1415
"apiClientId",
@@ -98,6 +99,7 @@ const OPTIONS = {
9899

99100
interface Options {
100101
string: string[];
102+
number: string[];
101103
boolean: string[];
102104
array: string[];
103105
alias: Record<string, string>;
@@ -106,6 +108,7 @@ interface Options {
106108

107109
export const ALL_CONFIG_KEYS = new Set(
108110
(OPTIONS.string as readonly string[])
111+
.concat(OPTIONS.number)
109112
.concat(OPTIONS.array)
110113
.concat(OPTIONS.boolean)
111114
.concat(Object.keys(OPTIONS.alias))
@@ -175,6 +178,8 @@ export interface UserConfig extends CliOptions {
175178
loggers: Array<"stderr" | "disk" | "mcp">;
176179
idleTimeoutMs: number;
177180
notificationTimeoutMs: number;
181+
maxDocumentsPerQuery: number;
182+
maxBytesPerQuery: number;
178183
atlasTemporaryDatabaseUserLifetimeMs: number;
179184
}
180185

@@ -202,6 +207,8 @@ export const defaultUserConfig: UserConfig = {
202207
idleTimeoutMs: 10 * 60 * 1000, // 10 minutes
203208
notificationTimeoutMs: 9 * 60 * 1000, // 9 minutes
204209
httpHeaders: {},
210+
maxDocumentsPerQuery: 100, // By default, we only fetch a maximum 100 documents per query / aggregation
211+
maxBytesPerQuery: 16 * 1024 * 1024, // By default, we only return ~16 mb of data per query / aggregation
205212
atlasTemporaryDatabaseUserLifetimeMs: 4 * 60 * 60 * 1000, // 4 hours
206213
};
207214

src/common/logger.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ export const LogId = {
4444
mongodbConnectFailure: mongoLogId(1_004_001),
4545
mongodbDisconnectFailure: mongoLogId(1_004_002),
4646
mongodbConnectTry: mongoLogId(1_004_003),
47+
mongodbCursorCloseError: mongoLogId(1_004_004),
4748

4849
toolUpdateFailure: mongoLogId(1_005_001),
4950
resourceUpdateFailure: mongoLogId(1_005_002),

src/common/packageInfo.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// This file was generated by scripts/updatePackageVersion.ts - Do not edit it manually.
22
export const packageInfo = {
3-
version: "1.0.0",
3+
version: "1.0.1",
44
mcpServerName: "MongoDB MCP Server",
55
};
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { calculateObjectSize } from "bson";
2+
import type { AggregationCursor, FindCursor } from "mongodb";
3+
4+
export function getResponseBytesLimit(
5+
toolResponseBytesLimit: number | undefined | null,
6+
configuredMaxBytesPerQuery: unknown
7+
): {
8+
cappedBy: "config.maxBytesPerQuery" | "tool.responseBytesLimit" | undefined;
9+
limit: number;
10+
} {
11+
const configuredLimit: number = parseInt(String(configuredMaxBytesPerQuery), 10);
12+
13+
// Setting configured maxBytesPerQuery to negative, zero or nullish is
14+
// equivalent to disabling the max limit applied on documents
15+
const configuredLimitIsNotApplicable = Number.isNaN(configuredLimit) || configuredLimit <= 0;
16+
17+
// It's possible to have tool parameter responseBytesLimit as null or
18+
// negative values in which case we consider that no limit is to be
19+
// applied from tool call perspective unless we have a maxBytesPerQuery
20+
// configured.
21+
const toolResponseLimitIsNotApplicable = typeof toolResponseBytesLimit !== "number" || toolResponseBytesLimit <= 0;
22+
23+
if (configuredLimitIsNotApplicable) {
24+
return {
25+
cappedBy: toolResponseLimitIsNotApplicable ? undefined : "tool.responseBytesLimit",
26+
limit: toolResponseLimitIsNotApplicable ? 0 : toolResponseBytesLimit,
27+
};
28+
}
29+
30+
if (toolResponseLimitIsNotApplicable) {
31+
return { cappedBy: "config.maxBytesPerQuery", limit: configuredLimit };
32+
}
33+
34+
return {
35+
cappedBy: configuredLimit < toolResponseBytesLimit ? "config.maxBytesPerQuery" : "tool.responseBytesLimit",
36+
limit: Math.min(toolResponseBytesLimit, configuredLimit),
37+
};
38+
}
39+
40+
/**
41+
* This function attempts to put a guard rail against accidental memory overflow
42+
* on the MCP server.
43+
*
44+
* The cursor is iterated until we can predict that fetching next doc won't
45+
* exceed the derived limit on number of bytes for the tool call. The derived
46+
* limit takes into account the limit provided from the Tool's interface and the
47+
* configured maxBytesPerQuery for the server.
48+
*/
49+
export async function collectCursorUntilMaxBytesLimit<T = unknown>({
50+
cursor,
51+
toolResponseBytesLimit,
52+
configuredMaxBytesPerQuery,
53+
abortSignal,
54+
}: {
55+
cursor: FindCursor<T> | AggregationCursor<T>;
56+
toolResponseBytesLimit: number | undefined | null;
57+
configuredMaxBytesPerQuery: unknown;
58+
abortSignal?: AbortSignal;
59+
}): Promise<{ cappedBy: "config.maxBytesPerQuery" | "tool.responseBytesLimit" | undefined; documents: T[] }> {
60+
const { limit: maxBytesPerQuery, cappedBy } = getResponseBytesLimit(
61+
toolResponseBytesLimit,
62+
configuredMaxBytesPerQuery
63+
);
64+
65+
// It's possible to have no limit on the cursor response by setting both the
66+
// config.maxBytesPerQuery and tool.responseBytesLimit to nullish or
67+
// negative values.
68+
if (maxBytesPerQuery <= 0) {
69+
return {
70+
cappedBy,
71+
documents: await cursor.toArray(),
72+
};
73+
}
74+
75+
let wasCapped: boolean = false;
76+
let totalBytes = 0;
77+
const bufferedDocuments: T[] = [];
78+
while (true) {
79+
if (abortSignal?.aborted) {
80+
break;
81+
}
82+
83+
// If the cursor is empty then there is nothing for us to do anymore.
84+
const nextDocument = await cursor.tryNext();
85+
if (!nextDocument) {
86+
break;
87+
}
88+
89+
const nextDocumentSize = calculateObjectSize(nextDocument);
90+
if (totalBytes + nextDocumentSize >= maxBytesPerQuery) {
91+
wasCapped = true;
92+
break;
93+
}
94+
95+
totalBytes += nextDocumentSize;
96+
bufferedDocuments.push(nextDocument);
97+
}
98+
99+
return {
100+
cappedBy: wasCapped ? cappedBy : undefined,
101+
documents: bufferedDocuments,
102+
};
103+
}

src/helpers/constants.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/**
2+
* A cap for the maxTimeMS used for FindCursor.countDocuments.
3+
*
4+
* The number is relatively smaller because we expect the count documents query
5+
* to be finished sooner if not by the time the batch of documents is retrieved
6+
* so that count documents query don't hold the final response back.
7+
*/
8+
export const QUERY_COUNT_MAX_TIME_MS_CAP: number = 10_000;
9+
10+
/**
11+
* A cap for the maxTimeMS used for counting resulting documents of an
12+
* aggregation.
13+
*/
14+
export const AGG_COUNT_MAX_TIME_MS_CAP: number = 60_000;
15+
16+
export const ONE_MB: number = 1 * 1024 * 1024;
17+
18+
/**
19+
* A map of applied limit on cursors to a text that is supposed to be sent as
20+
* response to LLM
21+
*/
22+
export const CURSOR_LIMITS_TO_LLM_TEXT = {
23+
"config.maxDocumentsPerQuery": "server's configured - maxDocumentsPerQuery",
24+
"config.maxBytesPerQuery": "server's configured - maxBytesPerQuery",
25+
"tool.responseBytesLimit": "tool's parameter - responseBytesLimit",
26+
} as const;

src/helpers/isObjectEmpty.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
type EmptyObject = { [x: string]: never } | null | undefined;
2+
3+
export function isObjectEmpty(value: object | null | undefined): value is EmptyObject {
4+
if (!value) {
5+
return true;
6+
}
7+
8+
for (const prop in value) {
9+
if (Object.prototype.hasOwnProperty.call(value, prop)) {
10+
return false;
11+
}
12+
}
13+
14+
return true;
15+
}

0 commit comments

Comments
 (0)