mcp-mongo-server 3.0.2 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +37 -4
  2. package/build/index.js +2038 -1524
  3. package/package.json +3 -1
package/build/index.js CHANGED
@@ -7,334 +7,1313 @@ import { createMcpHandler } from "@modelcontextprotocol/server";
7
7
  import { serveStdio } from "@modelcontextprotocol/server/stdio";
8
8
  import { createMcpExpressApp } from "@modelcontextprotocol/express";
9
9
 
10
- // src/mongo.ts
10
+ // src/connections.ts
11
+ import { randomUUID } from "crypto";
12
+ import { ProtocolError, ProtocolErrorCode } from "@modelcontextprotocol/server";
11
13
  import { MongoClient } from "mongodb";
12
- async function connectToMongoDB(url, readOnly) {
13
- try {
14
- const client = new MongoClient(url);
15
- await client.connect();
16
- const db = client.db();
17
- console.warn(`Connected to MongoDB database: ${db.databaseName}`);
14
+ function isRecord(value) {
15
+ return !!value && typeof value === "object" && !Array.isArray(value);
16
+ }
17
+ var Connections = class {
18
+ cursorScope = randomUUID();
19
+ named;
20
+ entries = /* @__PURE__ */ new Map();
21
+ closing;
22
+ constructor(legacyUri, configuration, readOnly) {
23
+ this.named = configuration !== void 0;
24
+ let config = {};
25
+ if (configuration !== void 0) {
26
+ try {
27
+ config = JSON.parse(configuration);
28
+ } catch {
29
+ throw new Error("MCP_MONGODB_CONNECTIONS must be valid JSON");
30
+ }
31
+ }
32
+ if (!isRecord(config))
33
+ throw new Error("MCP_MONGODB_CONNECTIONS must be an object");
34
+ if (legacyUri && Object.hasOwn(config, "default")) {
35
+ throw new Error("The legacy URI and named default connection conflict");
36
+ }
37
+ const entries = legacyUri ? { ...config, default: { uri: legacyUri } } : config;
38
+ if (!Object.hasOwn(entries, "default"))
39
+ throw new Error("A default MongoDB connection is required");
40
+ for (const [id, entry] of Object.entries(entries)) {
41
+ if (!/^[A-Za-z0-9_-]+$/.test(id))
42
+ throw new Error("Invalid connection ID");
43
+ if (!isRecord(entry) || Object.keys(entry).some(
44
+ (key) => !["uri", "databases", "readOnly"].includes(key)
45
+ )) {
46
+ throw new Error(`Invalid configuration for connection '${id}'`);
47
+ }
48
+ const { uri, databases, readOnly: localReadOnly } = entry;
49
+ if (typeof uri !== "string" || !/^mongodb(?:\+srv)?:\/\//.test(uri)) {
50
+ throw new Error(`Invalid MongoDB URI for connection '${id}'`);
51
+ }
52
+ if (localReadOnly !== void 0 && typeof localReadOnly !== "boolean") {
53
+ throw new Error(`Invalid readOnly setting for connection '${id}'`);
54
+ }
55
+ let client;
56
+ const query = uri.indexOf("?");
57
+ const uriOptions = new URLSearchParams(
58
+ query < 0 ? "" : uri.slice(query + 1)
59
+ );
60
+ const hasSelectionTimeout = [...uriOptions.keys()].some(
61
+ (key) => key.toLowerCase() === "serverselectiontimeoutms"
62
+ );
63
+ const selectionTimeout = this.named && !hasSelectionTimeout ? 5e3 : void 0;
64
+ try {
65
+ client = new MongoClient(uri, {
66
+ serverSelectionTimeoutMS: selectionTimeout
67
+ });
68
+ } catch {
69
+ throw new Error(`Invalid MongoDB URI for connection '${id}'`);
70
+ }
71
+ const defaultDatabase = client.db().databaseName;
72
+ if (databases !== void 0 && (!Array.isArray(databases) || !databases.includes(defaultDatabase) || databases.some(
73
+ (name) => typeof name !== "string" || !name.trim() || /[/\\. "$]/.test(name) || name.includes("\0")
74
+ ))) {
75
+ throw new Error(`Invalid database allowlist for connection '${id}'`);
76
+ }
77
+ this.entries.set(id, {
78
+ connectionId: id,
79
+ uri,
80
+ selectionTimeout,
81
+ client,
82
+ defaultDatabase,
83
+ databases: databases === void 0 ? [defaultDatabase] : [...new Set(databases)].sort(),
84
+ readOnly: readOnly || localReadOnly === true
85
+ });
86
+ }
87
+ }
88
+ connection(id = "default") {
89
+ const entry = typeof id === "string" ? this.entries.get(id) : void 0;
90
+ if (!entry) throw new Error("Unknown or invalid MongoDB connection ID");
91
+ return entry;
92
+ }
93
+ list() {
94
+ return [...this.entries.keys()].sort().map((id) => {
95
+ const { connectionId, defaultDatabase, readOnly } = this.connection(id);
96
+ return { connectionId, defaultDatabase, readOnly };
97
+ });
98
+ }
99
+ allowedDatabases(id) {
100
+ return this.connection(id).databases;
101
+ }
102
+ async resolve({ connectionId, database } = {}) {
103
+ const entry = this.connection(connectionId);
104
+ const name = database === void 0 ? entry.defaultDatabase : database;
105
+ if (typeof name !== "string" || !entry.databases.includes(name)) {
106
+ throw new Error(
107
+ `Database is invalid or not allowed on connection '${entry.connectionId}'`
108
+ );
109
+ }
110
+ if (this.closing) throw new Error("MongoDB connections are shutting down");
111
+ if (!entry.pending) {
112
+ const client2 = entry.client ?? new MongoClient(entry.uri, {
113
+ serverSelectionTimeoutMS: entry.selectionTimeout
114
+ });
115
+ entry.client = client2;
116
+ entry.pending = client2.connect().catch(async () => {
117
+ await client2.close().catch(() => void 0);
118
+ entry.client = void 0;
119
+ entry.pending = void 0;
120
+ throw new Error(
121
+ `Failed to connect to MongoDB connection '${entry.connectionId}'`
122
+ );
123
+ });
124
+ }
125
+ const client = await entry.pending;
18
126
  return {
19
127
  client,
20
- db,
21
- isConnected: true,
22
- isReadOnlyMode: readOnly
23
- };
24
- } catch (error) {
25
- console.error("Failed to connect to MongoDB:", error);
26
- return {
27
- client: null,
28
- db: null,
29
- isConnected: false,
30
- isReadOnlyMode: readOnly
128
+ db: client.db(name),
129
+ isReadOnlyMode: entry.readOnly,
130
+ connectionId: entry.connectionId
31
131
  };
32
132
  }
133
+ async listDatabases(connectionId) {
134
+ const { db } = await this.resolve({ connectionId });
135
+ const allowed = this.allowedDatabases(connectionId);
136
+ const { databases } = await db.admin().listDatabases({
137
+ nameOnly: true,
138
+ authorizedDatabases: true,
139
+ filter: { name: { $in: [...allowed] } }
140
+ });
141
+ return databases.map(({ name }) => name).filter((name) => allowed.includes(name)).sort();
142
+ }
143
+ async close() {
144
+ this.closing ??= Promise.allSettled(
145
+ [...this.entries.values()].map(async ({ client, pending }) => {
146
+ await pending?.catch(() => void 0);
147
+ await client?.close();
148
+ })
149
+ ).then(() => void 0);
150
+ await this.closing;
151
+ }
152
+ };
153
+ function resourceSelectors(uri) {
154
+ let url;
155
+ try {
156
+ url = new URL(uri);
157
+ if (url.protocol !== "mongodb:") throw new Error("Invalid scheme");
158
+ decodeURIComponent(url.search);
159
+ } catch {
160
+ throw new ProtocolError(
161
+ ProtocolErrorCode.InvalidParams,
162
+ "Invalid MongoDB resource URI"
163
+ );
164
+ }
165
+ const selectors = {};
166
+ for (const key of ["connectionId", "database"]) {
167
+ const values = url.searchParams.getAll(key);
168
+ if (values.length > 1) {
169
+ throw new ProtocolError(
170
+ ProtocolErrorCode.InvalidParams,
171
+ `Duplicate resource selector '${key}'`
172
+ );
173
+ }
174
+ if (values.length === 1) selectors[key] = values[0];
175
+ }
176
+ return selectors;
177
+ }
178
+
179
+ // src/query-limits.ts
180
+ function readQueryLimits(env) {
181
+ function positiveInteger(key) {
182
+ const raw = env[key];
183
+ if (raw === void 0) return void 0;
184
+ const value = Number(raw);
185
+ if (!/^\d+$/.test(raw) || !Number.isSafeInteger(value) || value <= 0) {
186
+ throw new Error(`${key} must be a positive integer`);
187
+ }
188
+ return value;
189
+ }
190
+ const bytes = positiveInteger("MCP_MONGODB_MAX_RESULT_BYTES");
191
+ if (bytes === 1) {
192
+ throw new Error("MCP_MONGODB_MAX_RESULT_BYTES must be at least 2 bytes");
193
+ }
194
+ return {
195
+ documents: positiveInteger("MCP_MONGODB_MAX_RESULT_DOCUMENTS"),
196
+ bytes,
197
+ timeoutMS: positiveInteger("MCP_MONGODB_QUERY_TIMEOUT_MS")
198
+ };
199
+ }
200
+ async function readResultPage(cursor, limits, pageSize = Infinity, signal) {
201
+ const results = [];
202
+ const documentCap = limits.documents ?? Infinity;
203
+ const maxRows = Math.min(documentCap, pageSize);
204
+ let bytes = 2;
205
+ try {
206
+ while (true) {
207
+ signal?.throwIfAborted();
208
+ const row = await cursor.next();
209
+ signal?.throwIfAborted();
210
+ if (row === null) return { results, hasMore: false };
211
+ if (results.length === maxRows) {
212
+ return {
213
+ results,
214
+ hasMore: true,
215
+ ...documentCap < pageSize ? { truncated: "documents" } : {}
216
+ };
217
+ }
218
+ if (limits.bytes !== void 0) {
219
+ bytes += Buffer.byteLength(JSON.stringify(row), "utf8") + (results.length ? 1 : 0);
220
+ if (bytes > limits.bytes) {
221
+ if (!results.length) {
222
+ throw new Error(
223
+ "First result exceeds the byte limit. Use a narrower projection or pipeline."
224
+ );
225
+ }
226
+ return { results, hasMore: true, truncated: "bytes" };
227
+ }
228
+ }
229
+ results.push(row);
230
+ }
231
+ } finally {
232
+ await cursor.close();
233
+ }
33
234
  }
34
235
 
35
236
  // src/server.ts
36
- import { Server } from "@modelcontextprotocol/server";
237
+ import {
238
+ ProtocolError as ProtocolError4,
239
+ ProtocolErrorCode as ProtocolErrorCode4,
240
+ Server
241
+ } from "@modelcontextprotocol/server";
37
242
 
38
243
  // src/schemas/call.ts
39
- import { ProtocolError, ProtocolErrorCode } from "@modelcontextprotocol/server";
244
+ import { ProtocolError as ProtocolError3, ProtocolErrorCode as ProtocolErrorCode3 } from "@modelcontextprotocol/server";
245
+ import { ObjectId as ObjectId2 } from "mongodb";
246
+
247
+ // src/schemas/resource.ts
248
+ import {
249
+ ProtocolError as ProtocolError2,
250
+ ProtocolErrorCode as ProtocolErrorCode2
251
+ } from "@modelcontextprotocol/server";
40
252
  import { ObjectId } from "mongodb";
41
- var COLLECTION_OPERATIONS = [
42
- "query",
43
- "aggregate",
44
- "update",
45
- "insert",
46
- "createIndex",
47
- "count"
48
- ];
49
- var WRITE_OPERATIONS = ["update", "insert", "createIndex"];
50
- var MAX_OBJECT_DEPTH = 100;
51
- var WRITE_AGG_STAGES = /* @__PURE__ */ new Set(["$out", "$merge"]);
52
- var SERVER_JS_AGG_OPERATORS = /* @__PURE__ */ new Set([
53
- "$function",
54
- "$accumulator",
55
- "$where"
56
- ]);
57
- var READONLY_FORBIDDEN_AGG_OPERATORS = /* @__PURE__ */ new Set([
58
- ...WRITE_AGG_STAGES,
59
- ...SERVER_JS_AGG_OPERATORS
60
- ]);
61
- function findAggOperator(value, operators, depth = 0) {
62
- if (depth > MAX_OBJECT_DEPTH) {
63
- throw new Error(
64
- `Object nesting exceeds the maximum depth of ${MAX_OBJECT_DEPTH}`
253
+
254
+ // src/utils/pagination.ts
255
+ var DEFAULT_PAGE_SIZE = 20;
256
+ function encodeCursor(offset) {
257
+ const data = { offset };
258
+ return Buffer.from(JSON.stringify(data)).toString("base64");
259
+ }
260
+ function decodeCursor(cursor) {
261
+ if (!cursor) return 0;
262
+ try {
263
+ const data = JSON.parse(
264
+ Buffer.from(cursor, "base64").toString("utf-8")
65
265
  );
266
+ return typeof data.offset === "number" && data.offset >= 0 ? data.offset : 0;
267
+ } catch {
268
+ return 0;
66
269
  }
270
+ }
271
+ function paginate(items, cursor, pageSize = DEFAULT_PAGE_SIZE) {
272
+ const offset = decodeCursor(cursor);
273
+ const page = items.slice(offset, offset + pageSize);
274
+ const nextOffset = offset + pageSize;
275
+ const nextCursor = nextOffset < items.length ? encodeCursor(nextOffset) : void 0;
276
+ return { items: page, nextCursor };
277
+ }
278
+
279
+ // src/schemas/resource.ts
280
+ function detectMongoType(value) {
281
+ if (value === null) return "null";
282
+ if (value === void 0) return "undefined";
283
+ if (value instanceof ObjectId) return "ObjectId";
284
+ if (value instanceof Date) return "Date";
67
285
  if (Array.isArray(value)) {
68
- for (const item of value) {
69
- const found = findAggOperator(item, operators, depth + 1);
70
- if (found) return found;
286
+ if (value.length === 0) return "Array";
287
+ const elementTypes = new Set(value.map((item) => detectMongoType(item)));
288
+ if (elementTypes.size === 1) {
289
+ return `Array<${Array.from(elementTypes)[0]}>`;
71
290
  }
72
- return null;
291
+ return "Array<mixed>";
73
292
  }
74
- if (value && typeof value === "object") {
75
- for (const [key, nested] of Object.entries(value)) {
76
- if (operators.has(key)) {
77
- return key;
293
+ if (typeof value === "object") {
294
+ return "Document";
295
+ }
296
+ return typeof value;
297
+ }
298
+ function inferSchemaFromSamples(documents) {
299
+ if (!documents || documents.length === 0) {
300
+ return { fields: [] };
301
+ }
302
+ const fieldMap = /* @__PURE__ */ new Map();
303
+ for (const doc of documents) {
304
+ for (const [key, value] of Object.entries(doc)) {
305
+ if (!fieldMap.has(key)) {
306
+ fieldMap.set(key, {
307
+ name: key,
308
+ types: /* @__PURE__ */ new Set([detectMongoType(value)]),
309
+ nullable: false,
310
+ // Store sample values for complex types
311
+ samples: [value]
312
+ });
313
+ } else {
314
+ const fieldInfo = fieldMap.get(key);
315
+ fieldInfo.types.add(detectMongoType(value));
316
+ if (fieldInfo.samples.length < 3 && !fieldInfo.samples.some(
317
+ (sample) => JSON.stringify(sample) === JSON.stringify(value)
318
+ )) {
319
+ fieldInfo.samples.push(value);
320
+ }
78
321
  }
79
- const found = findAggOperator(nested, operators, depth + 1);
80
- if (found) return found;
81
322
  }
82
323
  }
83
- return null;
84
- }
85
- function assertNoServerJs(value) {
86
- const operator = findAggOperator(value, SERVER_JS_AGG_OPERATORS);
87
- if (operator) {
88
- throw new Error(
89
- `Server-side JavaScript operator '${operator}' is disabled by the current server policy.`
90
- );
324
+ for (const doc of documents) {
325
+ for (const [key] of fieldMap.entries()) {
326
+ if (!(key in doc)) {
327
+ const fieldInfo = fieldMap.get(key);
328
+ fieldInfo.nullable = true;
329
+ }
330
+ }
91
331
  }
92
- }
93
- function findCrossDbTarget(pipeline, connectedDb) {
94
- const foreignDb = (target) => {
95
- if (target && typeof target === "object" && "db" in target) {
96
- const db = target.db;
97
- if (typeof db === "string" && db && db !== connectedDb) return db;
332
+ for (const [key, fieldInfo] of fieldMap.entries()) {
333
+ if (fieldInfo.types.has("Document")) {
334
+ const nestedDocs = documents.filter(
335
+ (doc) => doc[key] && typeof doc[key] === "object" && !Array.isArray(doc[key])
336
+ ).map((doc) => doc[key]);
337
+ if (nestedDocs.length > 0) {
338
+ fieldInfo.nestedSchema = inferSchemaFromSamples(nestedDocs);
339
+ }
98
340
  }
99
- return null;
100
- };
101
- for (const stage of pipeline) {
102
- if (!stage || typeof stage !== "object") continue;
103
- const s = stage;
104
- const out = foreignDb(s.$out);
105
- if (out) return out;
106
- const merge = s.$merge;
107
- const mergeInto = foreignDb(merge?.into ?? s.$merge);
108
- if (mergeInto) return mergeInto;
109
- const lookup = s.$lookup;
110
- const lookupFrom = foreignDb(lookup?.from);
111
- if (lookupFrom) return lookupFrom;
112
341
  }
113
- return null;
342
+ const fields = Array.from(fieldMap.values()).map((fieldInfo) => {
343
+ const result = {
344
+ name: fieldInfo.name,
345
+ types: Array.from(fieldInfo.types),
346
+ nullable: fieldInfo.nullable,
347
+ prevalence: `${Math.round(
348
+ documents.filter((doc) => fieldInfo.name in doc).length / documents.length * 100
349
+ )}%`,
350
+ examples: []
351
+ };
352
+ if (fieldInfo.nestedSchema) {
353
+ result.nestedSchema = fieldInfo.nestedSchema;
354
+ }
355
+ const sampleValues = fieldInfo.samples.map((sample) => {
356
+ if (sample instanceof ObjectId) return sample.toString();
357
+ if (sample instanceof Date) return sample.toISOString();
358
+ if (typeof sample === "object") {
359
+ return Array.isArray(sample) ? "[...]" : "{...}";
360
+ }
361
+ return sample;
362
+ });
363
+ result.examples = sampleValues;
364
+ return result;
365
+ });
366
+ return { fields };
114
367
  }
115
- async function handleCallToolRequest({
368
+ async function handleReadResourceRequest({
116
369
  request,
117
370
  client,
118
371
  db,
119
372
  isReadOnlyMode,
120
- allowCrossDb = false,
121
- allowServerJs = false,
122
- signal
373
+ signal,
374
+ sendProgress
123
375
  }) {
124
- const { name, arguments: args = {} } = request.params;
125
- const operation = name;
126
- const objectIdMode = args.objectIdMode || "auto";
127
- const filteredArgs = {};
128
- for (const [key, value] of Object.entries(args)) {
129
- if (key !== "objectIdMode") {
130
- filteredArgs[key] = value;
131
- }
132
- }
133
- if (args.sort) {
134
- args.sort = parseSort(args.sort);
135
- }
136
- Object.assign(args, filteredArgs);
137
- validateOperation(operation);
376
+ let collectionName;
138
377
  try {
139
- checkReadOnlyMode(operation, isReadOnlyMode);
140
- let collection = null;
141
- if (COLLECTION_OPERATIONS.includes(operation)) {
142
- const collectionName = args.collection;
143
- if (!collectionName) {
144
- throw new Error(
145
- `Collection name is required for '${operation}' operation`
146
- );
147
- }
148
- collection = db.collection(collectionName);
149
- validateCollection(collection);
378
+ const url = new URL(request.params.uri);
379
+ if (url.protocol !== "mongodb:") {
380
+ throw new ProtocolError2(
381
+ ProtocolErrorCode2.InvalidParams,
382
+ `Unsupported resource URI scheme '${url.protocol}': expected 'mongodb:'`
383
+ );
150
384
  }
151
- signal?.throwIfAborted();
152
- return await executeOperation(
153
- operation,
154
- collection,
155
- db,
156
- isReadOnlyMode,
157
- allowCrossDb,
158
- allowServerJs,
159
- args,
160
- objectIdMode,
161
- signal
162
- );
385
+ collectionName = decodeURIComponent(url.pathname.replace(/^\//, ""));
163
386
  } catch (error) {
164
- if (error instanceof ProtocolError || signal?.aborted) {
165
- throw error;
166
- }
167
- return toolExecutionError(
168
- error instanceof Error ? error.message : "Unknown error"
387
+ if (error instanceof ProtocolError2) throw error;
388
+ throw new ProtocolError2(
389
+ ProtocolErrorCode2.InvalidParams,
390
+ `Invalid resource URI: ${request.params.uri}`
169
391
  );
170
392
  }
171
- }
172
- async function executeOperation(operation, collection, db, isReadOnlyMode, allowCrossDb, allowServerJs, args, objectIdMode, signal) {
173
- switch (operation) {
174
- case "query":
175
- return handleQuery(
176
- collection,
177
- args,
178
- objectIdMode,
179
- allowServerJs && !isReadOnlyMode,
180
- signal
181
- );
182
- case "aggregate":
183
- return handleAggregate(
184
- collection,
185
- args,
186
- objectIdMode,
187
- isReadOnlyMode,
188
- allowCrossDb,
189
- allowServerJs,
190
- signal
191
- );
192
- case "update":
193
- return handleUpdate(collection, args, objectIdMode, allowServerJs, signal);
194
- case "serverInfo":
195
- return handleServerInfo(db, isReadOnlyMode, args, signal);
196
- case "insert":
197
- return handleInsert(collection, args, objectIdMode, signal);
198
- case "createIndex":
199
- return handleCreateIndex(collection, args, objectIdMode, signal);
200
- case "count":
201
- return handleCount(
202
- collection,
203
- args,
204
- objectIdMode,
205
- allowServerJs && !isReadOnlyMode,
206
- signal
207
- );
208
- case "listCollections":
209
- return handleListCollections(db, args, objectIdMode, signal);
210
- case "convertTime":
211
- return handleConvertTime(args);
212
- default:
213
- throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Unknown tool: ${operation}`);
214
- }
215
- }
216
- function validateOperation(operation) {
217
- const validOperations = [
218
- "query",
219
- "aggregate",
220
- "update",
221
- "serverInfo",
222
- "insert",
223
- "createIndex",
224
- "count",
225
- "listCollections",
226
- "convertTime"
227
- ];
228
- if (!validOperations.includes(operation)) {
229
- throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Unknown tool: ${operation}`);
230
- }
231
- }
232
- function validateCollection(collection) {
233
- if (!collection.collectionName) {
234
- throw new Error("Collection name cannot be empty");
235
- }
236
- if (collection.collectionName.toLowerCase().startsWith("system.")) {
237
- throw new Error("Access to system collections is not allowed");
238
- }
239
- }
240
- function checkReadOnlyMode(operation, isReadOnlyMode) {
241
- if (isReadOnlyMode && WRITE_OPERATIONS.includes(operation)) {
242
- throw new Error(
243
- `ReadonlyError: Operation '${operation}' is not allowed in read-only mode`
393
+ if (!collectionName) {
394
+ throw new ProtocolError2(
395
+ ProtocolErrorCode2.InvalidParams,
396
+ "Resource URI is missing a collection name"
244
397
  );
245
398
  }
246
- }
247
- function parseSort(sort) {
248
- if (!sort) return null;
249
- if (typeof sort !== "object" || sort === null || Array.isArray(sort)) {
250
- return null;
399
+ if (collectionName.toLowerCase().startsWith("system.")) {
400
+ throw new ProtocolError2(
401
+ ProtocolErrorCode2.InvalidParams,
402
+ "Access to system collections is not allowed"
403
+ );
251
404
  }
252
- const validSort = {};
253
- for (const [key, value] of Object.entries(sort)) {
254
- if (typeof value === "number" && (value === 1 || value === -1)) {
255
- validSort[key] = value;
405
+ try {
406
+ const schema = await buildCollectionSchema(
407
+ db,
408
+ db.collection(collectionName),
409
+ signal,
410
+ sendProgress
411
+ );
412
+ return {
413
+ contents: [
414
+ {
415
+ uri: request.params.uri,
416
+ mimeType: "application/json",
417
+ text: JSON.stringify(schema, null, 2)
418
+ }
419
+ ]
420
+ };
421
+ } catch (error) {
422
+ signal?.throwIfAborted();
423
+ if (error instanceof Error) {
424
+ throw new Error(
425
+ `Failed to read collection ${collectionName}: ${error.message}`
426
+ );
256
427
  }
428
+ throw new Error(
429
+ `Failed to read collection ${collectionName}: Unknown error`
430
+ );
257
431
  }
258
- return Object.keys(validSort).length > 0 ? validSort : null;
259
432
  }
260
- function parseFilter(filter, objectIdMode = "auto") {
261
- if (!filter) {
262
- return {};
433
+ async function buildCollectionSchema(db, collection, signal, sendProgress) {
434
+ const collectionName = collection.collectionName;
435
+ const sampleSize = 100;
436
+ let sampleDocuments = [];
437
+ await sendProgress?.(1, 4, "Sampling documents");
438
+ try {
439
+ sampleDocuments = await collection.aggregate([{ $sample: { size: sampleSize } }], { signal }).toArray();
440
+ } catch (sampleError) {
441
+ signal?.throwIfAborted();
442
+ console.warn(
443
+ `$sample aggregation failed for ${collectionName}, falling back to sequential scan: ${sampleError}`
444
+ );
445
+ signal?.throwIfAborted();
446
+ sampleDocuments = await collection.find({}, { signal }).limit(sampleSize).toArray();
263
447
  }
264
- if (typeof filter === "string") {
448
+ await sendProgress?.(2, 4, "Getting indexes");
449
+ signal?.throwIfAborted();
450
+ const indexOptions = {
451
+ signal
452
+ };
453
+ const indexes = await collection.indexes(indexOptions);
454
+ const inferredSchema = inferSchemaFromSamples(sampleDocuments);
455
+ await sendProgress?.(3, 4, "Counting documents");
456
+ let documentCount = null;
457
+ try {
458
+ signal?.throwIfAborted();
459
+ documentCount = await collection.countDocuments(
460
+ {},
461
+ { timeoutMS: 5e3, signal }
462
+ );
463
+ } catch (countError) {
464
+ signal?.throwIfAborted();
465
+ console.warn(
466
+ `Count operation failed or timed out for ${collectionName}: ${countError}`
467
+ );
265
468
  try {
266
- return processObjectIdInFilter(JSON.parse(filter), objectIdMode);
469
+ signal?.throwIfAborted();
470
+ const stats = await db.command(
471
+ { collStats: collectionName },
472
+ { timeoutMS: 5e3, signal }
473
+ );
474
+ documentCount = stats.count;
267
475
  } catch {
268
- throw new Error("Invalid filter format: must be a valid JSON object");
476
+ signal?.throwIfAborted();
477
+ documentCount = "unknown (count operation timed out)";
269
478
  }
270
479
  }
271
- if (typeof filter === "object" && filter !== null && !Array.isArray(filter)) {
272
- return processObjectIdInFilter(
273
- filter,
274
- objectIdMode
275
- );
276
- }
277
- throw new Error("Query filter must be a plain object or ObjectId");
278
- }
279
- function isObjectIdField(fieldName) {
280
- const lowerFieldName = fieldName.toLowerCase();
281
- return lowerFieldName === "_id" || lowerFieldName === "id" || lowerFieldName.endsWith("id") || lowerFieldName.endsWith("_id");
480
+ await sendProgress?.(4, 4, "Done");
481
+ return {
482
+ type: "collection",
483
+ name: collectionName,
484
+ fields: inferredSchema.fields,
485
+ indexes: indexes.map((idx) => ({
486
+ name: idx.name,
487
+ keys: idx.key
488
+ })),
489
+ documentCount,
490
+ sampleSize: sampleDocuments.length,
491
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
492
+ };
282
493
  }
283
- function processObjectIdInFilter(filter, objectIdMode = "auto", depth = 0) {
284
- if (depth > MAX_OBJECT_DEPTH) {
285
- throw new Error(
286
- `Object nesting exceeds the maximum depth of ${MAX_OBJECT_DEPTH}`
287
- );
288
- }
289
- if (objectIdMode === "none") {
290
- const result2 = {};
291
- for (const [key, value] of Object.entries(filter)) {
292
- if (typeof value === "string" && isISODateString(value)) {
293
- result2[key] = new Date(value);
294
- } else if (typeof value === "string" && value.startsWith("ISODate(") && value.endsWith(")")) {
295
- const dateString = value.substring(8, value.length - 2);
296
- if (isISODateString(dateString)) {
297
- result2[key] = new Date(dateString);
298
- } else {
299
- result2[key] = value;
300
- }
301
- } else if (typeof value === "object" && value !== null) {
302
- if (Array.isArray(value)) {
303
- result2[key] = value.map((item) => {
304
- if (typeof item === "string" && isISODateString(item)) {
305
- return new Date(item);
306
- } else if (typeof item === "string" && item.startsWith("ISODate(") && item.endsWith(")")) {
307
- const dateString = item.substring(8, item.length - 2);
308
- return isISODateString(dateString) ? new Date(dateString) : item;
309
- }
310
- return item;
311
- });
312
- } else {
313
- result2[key] = processObjectIdInFilter(
314
- value,
315
- "none",
316
- depth + 1
317
- );
318
- }
319
- } else {
320
- result2[key] = value;
321
- }
494
+ async function handleListResourcesRequest({
495
+ request,
496
+ client,
497
+ db,
498
+ isReadOnlyMode,
499
+ signal
500
+ }) {
501
+ try {
502
+ signal?.throwIfAborted();
503
+ const collections = await db.listCollections().toArray();
504
+ const allResources = collections.map((collection) => ({
505
+ uri: `mongodb:///${encodeURIComponent(collection.name)}`,
506
+ mimeType: "application/json",
507
+ name: collection.name,
508
+ description: `MongoDB collection: ${collection.name}`
509
+ }));
510
+ const cursor = request.params?.cursor;
511
+ const { items, nextCursor } = paginate(allResources, cursor);
512
+ return {
513
+ resources: items,
514
+ ...nextCursor ? { nextCursor } : {}
515
+ };
516
+ } catch (error) {
517
+ if (error instanceof Error) {
518
+ throw new Error(`Failed to list collections: ${error.message}`);
322
519
  }
323
- return result2;
520
+ throw new Error("Failed to list collections: Unknown error");
324
521
  }
325
- const result = {};
326
- for (const [key, value] of Object.entries(filter)) {
327
- if (typeof value === "string" && isObjectIdString(value)) {
328
- if (objectIdMode === "force" || objectIdMode === "auto" && isObjectIdField(key)) {
329
- result[key] = new ObjectId(value);
330
- } else {
331
- result[key] = value;
332
- }
333
- } else if (typeof value === "string" && isISODateString(value)) {
334
- result[key] = new Date(value);
335
- } else if (typeof value === "string" && value.startsWith("ISODate(") && value.endsWith(")")) {
336
- const dateString = value.substring(8, value.length - 2);
337
- if (isISODateString(dateString)) {
522
+ }
523
+
524
+ // src/schemas/tools.ts
525
+ var WRITE_OPERATIONS = /* @__PURE__ */ new Set(["update", "insert", "createIndex"]);
526
+ async function handleListToolsRequest({
527
+ request,
528
+ client,
529
+ db,
530
+ isReadOnlyMode,
531
+ signal,
532
+ connectionSelection = false,
533
+ hasWritableConnection = !isReadOnlyMode,
534
+ queryLimits = {}
535
+ }) {
536
+ const result = {
537
+ tools: [
538
+ {
539
+ name: "query",
540
+ description: "Find documents in one collection using a filter, projection, or sort. Use aggregate for grouping or joins. Request an execution plan to inspect the query.",
541
+ inputSchema: {
542
+ type: "object",
543
+ properties: {
544
+ collection: {
545
+ type: "string",
546
+ description: "Name of the collection to query"
547
+ },
548
+ filter: {
549
+ type: "object",
550
+ description: "MongoDB query filter. Supports date strings in ISO format ('2025-01-01T00:00:00Z') and ISODate('2025-01-01T00:00:00Z') notation"
551
+ },
552
+ projection: {
553
+ type: "object",
554
+ description: "Fields to include/exclude"
555
+ },
556
+ limit: {
557
+ type: "number",
558
+ description: "Requested page size. Configured server caps may reduce the number returned.",
559
+ default: 10
560
+ },
561
+ includeTotal: {
562
+ type: "boolean",
563
+ description: "Include the exact match count. Set false to skip counting.",
564
+ default: true
565
+ },
566
+ skip: {
567
+ type: "number",
568
+ description: "Number of documents to skip before returning results",
569
+ default: 0
570
+ },
571
+ sort: {
572
+ type: "object",
573
+ description: "Sort order as a field-to-direction map, e.g. { orderDate: -1 } for descending or { name: 1 } for ascending"
574
+ },
575
+ explain: {
576
+ type: "string",
577
+ description: "Optional: Get query execution information",
578
+ enum: ["queryPlanner", "executionStats", "allPlansExecution"]
579
+ },
580
+ objectIdMode: {
581
+ type: "string",
582
+ description: "Control how 24-character hex strings are handled",
583
+ enum: ["auto", "none", "force"],
584
+ default: "auto"
585
+ }
586
+ },
587
+ required: ["collection"]
588
+ }
589
+ },
590
+ {
591
+ name: "aggregate",
592
+ description: "Run a MongoDB aggregation pipeline for grouping, joins, or multi-stage transformations. Request an execution plan to inspect the pipeline.",
593
+ inputSchema: {
594
+ type: "object",
595
+ properties: {
596
+ collection: {
597
+ type: "string",
598
+ description: "Name of the collection to aggregate"
599
+ },
600
+ pipeline: {
601
+ type: "array",
602
+ description: "Aggregation pipeline stages. Supports date strings in ISO format ('2025-01-01T00:00:00Z') and ISODate('2025-01-01T00:00:00Z') notation",
603
+ items: {
604
+ type: "object"
605
+ }
606
+ },
607
+ explain: {
608
+ type: "string",
609
+ description: "Optional: Get aggregation execution information (queryPlanner, executionStats, or allPlansExecution)",
610
+ enum: ["queryPlanner", "executionStats", "allPlansExecution"]
611
+ },
612
+ objectIdMode: {
613
+ type: "string",
614
+ description: "Control how 24-character hex strings are handled",
615
+ enum: ["auto", "none", "force"],
616
+ default: "auto"
617
+ }
618
+ },
619
+ required: ["collection", "pipeline"]
620
+ }
621
+ },
622
+ {
623
+ name: "update",
624
+ description: "Modify matching documents using operators such as $set, $unset, or $inc. Enable multi to update all matches or upsert to create a document when the filter matches none.",
625
+ inputSchema: {
626
+ type: "object",
627
+ properties: {
628
+ collection: {
629
+ type: "string",
630
+ description: "Name of the collection to update"
631
+ },
632
+ filter: {
633
+ type: "object",
634
+ description: "Filter to select documents to update. Supports date strings in ISO format ('2025-01-01T00:00:00Z') and ISODate('2025-01-01T00:00:00Z') notation"
635
+ },
636
+ update: {
637
+ type: "object",
638
+ description: "Update operations to apply ($set, $unset, $inc, etc.)"
639
+ },
640
+ upsert: {
641
+ type: "boolean",
642
+ description: "Create a new document if no documents match the filter"
643
+ },
644
+ multi: {
645
+ type: "boolean",
646
+ description: "Update multiple documents that match the filter"
647
+ },
648
+ objectIdMode: {
649
+ type: "string",
650
+ description: "Control how 24-character hex strings are handled",
651
+ enum: ["auto", "none", "force"],
652
+ default: "auto"
653
+ }
654
+ },
655
+ required: ["collection", "filter", "update"]
656
+ }
657
+ },
658
+ {
659
+ name: "serverInfo",
660
+ description: "Inspect MongoDB server metadata such as version and storage engine. Enable includeDebugInfo for additional diagnostics.",
661
+ inputSchema: {
662
+ type: "object",
663
+ properties: {
664
+ includeDebugInfo: {
665
+ type: "boolean",
666
+ description: "Include additional debug information about the server"
667
+ }
668
+ }
669
+ }
670
+ },
671
+ {
672
+ name: "insert",
673
+ description: "Insert new documents into a MongoDB collection. Use update to modify existing records.",
674
+ inputSchema: {
675
+ type: "object",
676
+ properties: {
677
+ collection: {
678
+ type: "string",
679
+ description: "Name of the collection to insert into"
680
+ },
681
+ documents: {
682
+ type: "array",
683
+ description: "Array of documents to insert",
684
+ items: { type: "object" }
685
+ },
686
+ ordered: {
687
+ type: "boolean",
688
+ description: "If true, perform ordered insert. If false, insert unordered"
689
+ },
690
+ writeConcern: {
691
+ type: "object",
692
+ description: "Write concern for the insert operation"
693
+ },
694
+ bypassDocumentValidation: {
695
+ type: "boolean",
696
+ description: "Allow insert to bypass schema validation"
697
+ },
698
+ objectIdMode: {
699
+ type: "string",
700
+ description: "Control how 24-character hex strings are handled",
701
+ enum: ["auto", "none", "force"],
702
+ default: "auto"
703
+ }
704
+ },
705
+ required: ["collection", "documents"]
706
+ }
707
+ },
708
+ {
709
+ name: "createIndex",
710
+ description: "Create indexes to improve query performance, enforce uniqueness, or expire documents with a TTL index.",
711
+ inputSchema: {
712
+ type: "object",
713
+ properties: {
714
+ collection: {
715
+ type: "string",
716
+ description: "Name of the collection to create indexes on"
717
+ },
718
+ indexes: {
719
+ type: "array",
720
+ description: "Array of index specifications",
721
+ items: {
722
+ type: "object",
723
+ properties: {
724
+ key: {
725
+ type: "object",
726
+ description: "Index key pattern, e.g. { field: 1 }"
727
+ },
728
+ name: {
729
+ type: "string",
730
+ description: "Optional: Name of the index"
731
+ },
732
+ unique: {
733
+ type: "boolean",
734
+ description: "Optional: Creates a unique index"
735
+ },
736
+ sparse: {
737
+ type: "boolean",
738
+ description: "Optional: Creates a sparse index"
739
+ },
740
+ background: {
741
+ type: "boolean",
742
+ description: "Optional: Builds index in background"
743
+ },
744
+ expireAfterSeconds: {
745
+ type: "number",
746
+ description: "TTL in seconds for documents"
747
+ },
748
+ partialFilterExpression: {
749
+ type: "object",
750
+ description: "Filter expression for partial indexes"
751
+ }
752
+ },
753
+ required: ["key"]
754
+ }
755
+ },
756
+ writeConcern: {
757
+ type: "object",
758
+ description: "Write concern for index creation"
759
+ },
760
+ commitQuorum: {
761
+ type: ["string", "number"],
762
+ description: "Replica-set commit quorum as a member count or a mode such as majority"
763
+ },
764
+ objectIdMode: {
765
+ type: "string",
766
+ description: "Control how 24-character hex strings are handled",
767
+ enum: ["auto", "none", "force"],
768
+ default: "auto"
769
+ }
770
+ },
771
+ required: ["collection", "indexes"]
772
+ }
773
+ },
774
+ {
775
+ name: "count",
776
+ description: "Count documents matching a filter in one collection. Use aggregate for counts grouped by field.",
777
+ inputSchema: {
778
+ type: "object",
779
+ properties: {
780
+ collection: { type: "string", description: "Collection name" },
781
+ query: { type: "object", description: "Query filter to count" },
782
+ limit: { type: "integer", description: "Max documents to count" },
783
+ skip: {
784
+ type: "integer",
785
+ description: "Docs to skip before counting"
786
+ },
787
+ hint: { type: "object", description: "Index hint" },
788
+ readConcern: { type: "object", description: "Read concern option" },
789
+ maxTimeMS: { type: "integer", description: "Max execution time" },
790
+ collation: {
791
+ type: "object",
792
+ description: "Collation rules for comparison"
793
+ },
794
+ objectIdMode: {
795
+ type: "string",
796
+ description: "Control how 24-character hex strings are handled",
797
+ enum: ["auto", "none", "force"],
798
+ default: "auto"
799
+ }
800
+ },
801
+ required: ["collection"]
802
+ }
803
+ },
804
+ {
805
+ name: "listCollections",
806
+ description: "List collections in the connected database. Filter by name or other collection properties and request names only for a compact result.",
807
+ inputSchema: {
808
+ type: "object",
809
+ properties: {
810
+ nameOnly: {
811
+ type: "boolean",
812
+ description: "If true, return only collection names"
813
+ },
814
+ filter: {
815
+ type: "object",
816
+ description: "Filter for collections"
817
+ },
818
+ skip: {
819
+ type: "number",
820
+ description: "Number of collections to skip before returning results",
821
+ default: 0
822
+ },
823
+ limit: {
824
+ type: "number",
825
+ description: "Maximum number of collections to return",
826
+ default: 20
827
+ },
828
+ objectIdMode: {
829
+ type: "string",
830
+ description: "Control how 24-character hex strings are handled",
831
+ enum: ["auto", "none", "force"],
832
+ default: "auto"
833
+ }
834
+ }
835
+ }
836
+ },
837
+ ...[
838
+ ["listIndexes", "List a collection's indexes, keys, and options."],
839
+ [
840
+ "collectionSchema",
841
+ "Infer a collection schema from sample documents and include its indexes."
842
+ ]
843
+ ].map(([name, description]) => ({
844
+ name,
845
+ description,
846
+ inputSchema: {
847
+ type: "object",
848
+ properties: {
849
+ collection: {
850
+ type: "string",
851
+ description: "Name of the collection to inspect"
852
+ }
853
+ },
854
+ required: ["collection"]
855
+ }
856
+ })),
857
+ {
858
+ name: "convertTime",
859
+ description: "Convert a Unix timestamp or date string to multiple formats (UTC ISO 8601, GMT, Unix seconds/milliseconds) and report the server's current time and timezone. Useful for building unambiguous date queries when the server and user are in different timezones. Omit 'input' to get the current time.",
860
+ inputSchema: {
861
+ type: "object",
862
+ properties: {
863
+ input: {
864
+ type: "string",
865
+ description: "A Unix timestamp (seconds or milliseconds, as a number or numeric string) or a date string (ISO 8601, with or without a timezone offset). Omit to use the current server time."
866
+ }
867
+ }
868
+ }
869
+ }
870
+ ]
871
+ };
872
+ const configuredLimits = [
873
+ queryLimits.documents !== void 0 && `${queryLimits.documents} documents`,
874
+ queryLimits.bytes !== void 0 && `${queryLimits.bytes} bytes in the results array`,
875
+ queryLimits.timeoutMS !== void 0 && `${queryLimits.timeoutMS} ms per MongoDB operation`
876
+ ].filter(Boolean);
877
+ result.tools = result.tools.filter(
878
+ ({ name }) => hasWritableConnection || !WRITE_OPERATIONS.has(name)
879
+ );
880
+ const { tools } = result;
881
+ for (const tool of tools) {
882
+ if (WRITE_OPERATIONS.has(tool.name)) {
883
+ tool.annotations = { ...tool.annotations, readOnlyHint: false };
884
+ if (connectionSelection) {
885
+ tool.description += " Select a writable connection with listConnections.";
886
+ }
887
+ }
888
+ if (tool.name === "aggregate" && !hasWritableConnection) {
889
+ tool.annotations = { ...tool.annotations, readOnlyHint: true };
890
+ }
891
+ if ((tool.name === "query" || tool.name === "aggregate") && configuredLimits.length) {
892
+ tool.description += ` Configured limits include ${configuredLimits.join(", ")}.`;
893
+ }
894
+ }
895
+ if (connectionSelection) {
896
+ const connectionId = {
897
+ type: "string",
898
+ description: "Configured connection ID. Defaults to default."
899
+ };
900
+ const database = {
901
+ type: "string",
902
+ description: "Allowed database name. Defaults to the database in the connection URI."
903
+ };
904
+ for (const tool of tools) {
905
+ if (tool.name !== "convertTime") {
906
+ tool.inputSchema.properties = {
907
+ ...tool.inputSchema.properties,
908
+ connectionId,
909
+ database
910
+ };
911
+ }
912
+ }
913
+ tools.push(
914
+ {
915
+ name: "listConnections",
916
+ description: "List configured connection IDs, default databases, and read-only policies.",
917
+ inputSchema: { type: "object", properties: {} }
918
+ },
919
+ {
920
+ name: "listDatabases",
921
+ description: "List existing authorized databases on a configured connection.",
922
+ inputSchema: { type: "object", properties: { connectionId } }
923
+ }
924
+ );
925
+ }
926
+ return result;
927
+ }
928
+
929
+ // src/schemas/call.ts
930
+ var COLLECTION_OPERATIONS = [
931
+ "query",
932
+ "aggregate",
933
+ "update",
934
+ "insert",
935
+ "createIndex",
936
+ "count",
937
+ "listIndexes",
938
+ "collectionSchema"
939
+ ];
940
+ var MAX_OBJECT_DEPTH = 100;
941
+ var WRITE_AGG_STAGES = /* @__PURE__ */ new Set(["$out", "$merge"]);
942
+ var SERVER_JS_AGG_OPERATORS = /* @__PURE__ */ new Set([
943
+ "$function",
944
+ "$accumulator",
945
+ "$where"
946
+ ]);
947
+ var READONLY_FORBIDDEN_AGG_OPERATORS = /* @__PURE__ */ new Set([
948
+ ...WRITE_AGG_STAGES,
949
+ ...SERVER_JS_AGG_OPERATORS
950
+ ]);
951
+ function findAggOperator(value, operators, depth = 0) {
952
+ if (depth > MAX_OBJECT_DEPTH) {
953
+ throw new Error(
954
+ `Object nesting exceeds the maximum depth of ${MAX_OBJECT_DEPTH}`
955
+ );
956
+ }
957
+ if (Array.isArray(value)) {
958
+ for (const item of value) {
959
+ const found = findAggOperator(item, operators, depth + 1);
960
+ if (found) return found;
961
+ }
962
+ return null;
963
+ }
964
+ if (value && typeof value === "object") {
965
+ for (const [key, nested] of Object.entries(value)) {
966
+ if (operators.has(key)) {
967
+ return key;
968
+ }
969
+ const found = findAggOperator(nested, operators, depth + 1);
970
+ if (found) return found;
971
+ }
972
+ }
973
+ return null;
974
+ }
975
+ function assertNoServerJs(value) {
976
+ const operator = findAggOperator(value, SERVER_JS_AGG_OPERATORS);
977
+ if (operator) {
978
+ throw new Error(
979
+ `Server-side JavaScript operator '${operator}' is disabled by the current server policy.`
980
+ );
981
+ }
982
+ }
983
+ function findCrossDbTarget(pipeline, connectedDb) {
984
+ const foreignDb = (target) => {
985
+ if (target && typeof target === "object" && "db" in target) {
986
+ const db = target.db;
987
+ if (typeof db === "string" && db && db !== connectedDb) return db;
988
+ }
989
+ return null;
990
+ };
991
+ for (const stage of pipeline) {
992
+ if (!stage || typeof stage !== "object") continue;
993
+ const s = stage;
994
+ const out = foreignDb(s.$out);
995
+ if (out) return out;
996
+ const merge = s.$merge;
997
+ const mergeInto = foreignDb(merge?.into ?? s.$merge);
998
+ if (mergeInto) return mergeInto;
999
+ const lookup = s.$lookup;
1000
+ const lookupFrom = foreignDb(lookup?.from);
1001
+ if (lookupFrom) return lookupFrom;
1002
+ }
1003
+ return null;
1004
+ }
1005
+ async function handleCallToolRequest({
1006
+ request,
1007
+ client,
1008
+ db,
1009
+ isReadOnlyMode,
1010
+ allowCrossDb = false,
1011
+ allowServerJs = false,
1012
+ connections: connections2,
1013
+ queryLimits = {},
1014
+ signal
1015
+ }) {
1016
+ const { name, arguments: args = {} } = request.params;
1017
+ const operation = name;
1018
+ const objectIdMode = args.objectIdMode || "auto";
1019
+ const filteredArgs = {};
1020
+ for (const [key, value] of Object.entries(args)) {
1021
+ if (key !== "objectIdMode") {
1022
+ filteredArgs[key] = value;
1023
+ }
1024
+ }
1025
+ if (args.sort) {
1026
+ args.sort = parseSort(args.sort);
1027
+ }
1028
+ Object.assign(args, filteredArgs);
1029
+ if (!connections2 || name !== "listConnections" && name !== "listDatabases") {
1030
+ validateOperation(operation);
1031
+ }
1032
+ try {
1033
+ signal?.throwIfAborted();
1034
+ if (connections2 && name === "listConnections") {
1035
+ return formatResponse({ connections: connections2.list() });
1036
+ }
1037
+ if (connections2 && name === "listDatabases") {
1038
+ return formatResponse({
1039
+ databases: await connections2.listDatabases(args.connectionId)
1040
+ });
1041
+ }
1042
+ if (connections2 && operation !== "convertTime") {
1043
+ const target = await connections2.resolve(args);
1044
+ db = target.db;
1045
+ isReadOnlyMode = target.isReadOnlyMode;
1046
+ if (connections2.named && operation === "aggregate" && Array.isArray(args.pipeline)) {
1047
+ const allowed = connections2.allowedDatabases(target.connectionId);
1048
+ checkNamedAggregationDatabases(
1049
+ args.pipeline,
1050
+ db.databaseName,
1051
+ allowCrossDb ? allowed : [db.databaseName]
1052
+ );
1053
+ }
1054
+ }
1055
+ checkReadOnlyMode(operation, isReadOnlyMode);
1056
+ let collection = null;
1057
+ if (COLLECTION_OPERATIONS.includes(operation)) {
1058
+ const collectionName = args.collection;
1059
+ if (!collectionName) {
1060
+ throw new Error(
1061
+ `Collection name is required for '${operation}' operation`
1062
+ );
1063
+ }
1064
+ collection = db.collection(collectionName);
1065
+ validateCollection(collection);
1066
+ }
1067
+ signal?.throwIfAborted();
1068
+ if (collection && (operation === "listIndexes" || operation === "collectionSchema")) {
1069
+ const target = `${db.databaseName}.${collection.collectionName}`;
1070
+ try {
1071
+ const options = { signal, timeoutMS: queryLimits.timeoutMS };
1072
+ return formatResponse(
1073
+ operation === "listIndexes" ? { indexes: await collection.indexes(options) } : await buildCollectionSchema(db, collection, signal)
1074
+ );
1075
+ } catch (error) {
1076
+ signal?.throwIfAborted();
1077
+ return handleError(error, operation, target);
1078
+ }
1079
+ }
1080
+ return await executeOperation(
1081
+ operation,
1082
+ collection,
1083
+ db,
1084
+ isReadOnlyMode,
1085
+ allowCrossDb,
1086
+ allowServerJs,
1087
+ args,
1088
+ objectIdMode,
1089
+ queryLimits,
1090
+ signal
1091
+ );
1092
+ } catch (error) {
1093
+ if (error instanceof ProtocolError3 || signal?.aborted) {
1094
+ throw error;
1095
+ }
1096
+ return toolExecutionError(
1097
+ error instanceof Error ? error.message : "Unknown error"
1098
+ );
1099
+ }
1100
+ }
1101
+ function checkNamedAggregationDatabases(pipeline, database, allowed, depth = 0) {
1102
+ if (depth > MAX_OBJECT_DEPTH)
1103
+ throw new Error("Aggregation pipeline nesting is too deep");
1104
+ for (const stage of pipeline) {
1105
+ if (!stage || typeof stage !== "object") continue;
1106
+ const { $out, $merge, $lookup, $unionWith, $facet } = stage;
1107
+ for (const target of [$out, $merge, $lookup, $unionWith]) {
1108
+ if (!target || typeof target !== "object") continue;
1109
+ const value = target;
1110
+ for (const reference of [value, value.into, value.from]) {
1111
+ if (!reference || typeof reference !== "object" || !("db" in reference))
1112
+ continue;
1113
+ if (typeof reference.db !== "string" || !allowed.includes(reference.db)) {
1114
+ throw new Error(
1115
+ `Aggregation references a database outside the allowed scope of '${database}'`
1116
+ );
1117
+ }
1118
+ }
1119
+ if (Array.isArray(value.pipeline))
1120
+ checkNamedAggregationDatabases(
1121
+ value.pipeline,
1122
+ database,
1123
+ allowed,
1124
+ depth + 1
1125
+ );
1126
+ }
1127
+ if ($facet && typeof $facet === "object") {
1128
+ for (const nested of Object.values($facet)) {
1129
+ if (Array.isArray(nested))
1130
+ checkNamedAggregationDatabases(nested, database, allowed, depth + 1);
1131
+ }
1132
+ }
1133
+ }
1134
+ }
1135
+ async function executeOperation(operation, collection, db, isReadOnlyMode, allowCrossDb, allowServerJs, args, objectIdMode, queryLimits, signal) {
1136
+ switch (operation) {
1137
+ case "query":
1138
+ return handleQuery(
1139
+ collection,
1140
+ args,
1141
+ objectIdMode,
1142
+ allowServerJs && !isReadOnlyMode,
1143
+ queryLimits,
1144
+ signal
1145
+ );
1146
+ case "aggregate":
1147
+ return handleAggregate(
1148
+ collection,
1149
+ args,
1150
+ objectIdMode,
1151
+ isReadOnlyMode,
1152
+ allowCrossDb,
1153
+ allowServerJs,
1154
+ queryLimits,
1155
+ signal
1156
+ );
1157
+ case "update":
1158
+ return handleUpdate(
1159
+ collection,
1160
+ args,
1161
+ objectIdMode,
1162
+ allowServerJs,
1163
+ signal
1164
+ );
1165
+ case "serverInfo":
1166
+ return handleServerInfo(db, isReadOnlyMode, args, signal);
1167
+ case "insert":
1168
+ return handleInsert(collection, args, objectIdMode, signal);
1169
+ case "createIndex":
1170
+ return handleCreateIndex(collection, args, objectIdMode, signal);
1171
+ case "count":
1172
+ return handleCount(
1173
+ collection,
1174
+ args,
1175
+ objectIdMode,
1176
+ allowServerJs && !isReadOnlyMode,
1177
+ signal
1178
+ );
1179
+ case "listCollections":
1180
+ return handleListCollections(db, args, objectIdMode, signal);
1181
+ case "convertTime":
1182
+ return handleConvertTime(args);
1183
+ default:
1184
+ throw new ProtocolError3(
1185
+ ProtocolErrorCode3.InvalidParams,
1186
+ `Unknown tool: ${operation}`
1187
+ );
1188
+ }
1189
+ }
1190
+ function validateOperation(operation) {
1191
+ const validOperations = [
1192
+ "query",
1193
+ "aggregate",
1194
+ "update",
1195
+ "serverInfo",
1196
+ "insert",
1197
+ "createIndex",
1198
+ "count",
1199
+ "listCollections",
1200
+ "listIndexes",
1201
+ "collectionSchema",
1202
+ "convertTime"
1203
+ ];
1204
+ if (!validOperations.includes(operation)) {
1205
+ throw new ProtocolError3(
1206
+ ProtocolErrorCode3.InvalidParams,
1207
+ `Unknown tool: ${operation}`
1208
+ );
1209
+ }
1210
+ }
1211
+ function validateCollection(collection) {
1212
+ if (!collection.collectionName) {
1213
+ throw new Error("Collection name cannot be empty");
1214
+ }
1215
+ if (collection.collectionName.toLowerCase().startsWith("system.")) {
1216
+ throw new Error("Access to system collections is not allowed");
1217
+ }
1218
+ }
1219
+ function checkReadOnlyMode(operation, isReadOnlyMode) {
1220
+ if (isReadOnlyMode && WRITE_OPERATIONS.has(operation)) {
1221
+ throw new Error(
1222
+ `ReadonlyError: Operation '${operation}' is not allowed in read-only mode`
1223
+ );
1224
+ }
1225
+ }
1226
+ function parseSort(sort) {
1227
+ if (!sort) return null;
1228
+ if (typeof sort !== "object" || sort === null || Array.isArray(sort)) {
1229
+ return null;
1230
+ }
1231
+ const validSort = {};
1232
+ for (const [key, value] of Object.entries(sort)) {
1233
+ if (typeof value === "number" && (value === 1 || value === -1)) {
1234
+ validSort[key] = value;
1235
+ }
1236
+ }
1237
+ return Object.keys(validSort).length > 0 ? validSort : null;
1238
+ }
1239
+ function parseFilter(filter, objectIdMode = "auto") {
1240
+ if (!filter) {
1241
+ return {};
1242
+ }
1243
+ if (typeof filter === "string") {
1244
+ try {
1245
+ return processObjectIdInFilter(JSON.parse(filter), objectIdMode);
1246
+ } catch {
1247
+ throw new Error("Invalid filter format: must be a valid JSON object");
1248
+ }
1249
+ }
1250
+ if (typeof filter === "object" && filter !== null && !Array.isArray(filter)) {
1251
+ return processObjectIdInFilter(
1252
+ filter,
1253
+ objectIdMode
1254
+ );
1255
+ }
1256
+ throw new Error("Query filter must be a plain object or ObjectId");
1257
+ }
1258
+ function isObjectIdField(fieldName) {
1259
+ const lowerFieldName = fieldName.toLowerCase();
1260
+ return lowerFieldName === "_id" || lowerFieldName === "id" || lowerFieldName.endsWith("id") || lowerFieldName.endsWith("_id");
1261
+ }
1262
+ function processObjectIdInFilter(filter, objectIdMode = "auto", depth = 0) {
1263
+ if (depth > MAX_OBJECT_DEPTH) {
1264
+ throw new Error(
1265
+ `Object nesting exceeds the maximum depth of ${MAX_OBJECT_DEPTH}`
1266
+ );
1267
+ }
1268
+ if (objectIdMode === "none") {
1269
+ const result2 = {};
1270
+ for (const [key, value] of Object.entries(filter)) {
1271
+ if (typeof value === "string" && isISODateString(value)) {
1272
+ result2[key] = new Date(value);
1273
+ } else if (typeof value === "string" && value.startsWith("ISODate(") && value.endsWith(")")) {
1274
+ const dateString = value.substring(8, value.length - 2);
1275
+ if (isISODateString(dateString)) {
1276
+ result2[key] = new Date(dateString);
1277
+ } else {
1278
+ result2[key] = value;
1279
+ }
1280
+ } else if (typeof value === "object" && value !== null) {
1281
+ if (Array.isArray(value)) {
1282
+ result2[key] = value.map((item) => {
1283
+ if (typeof item === "string" && isISODateString(item)) {
1284
+ return new Date(item);
1285
+ } else if (typeof item === "string" && item.startsWith("ISODate(") && item.endsWith(")")) {
1286
+ const dateString = item.substring(8, item.length - 2);
1287
+ return isISODateString(dateString) ? new Date(dateString) : item;
1288
+ }
1289
+ return item;
1290
+ });
1291
+ } else {
1292
+ result2[key] = processObjectIdInFilter(
1293
+ value,
1294
+ "none",
1295
+ depth + 1
1296
+ );
1297
+ }
1298
+ } else {
1299
+ result2[key] = value;
1300
+ }
1301
+ }
1302
+ return result2;
1303
+ }
1304
+ const result = {};
1305
+ for (const [key, value] of Object.entries(filter)) {
1306
+ if (typeof value === "string" && isObjectIdString(value)) {
1307
+ if (objectIdMode === "force" || objectIdMode === "auto" && isObjectIdField(key)) {
1308
+ result[key] = new ObjectId2(value);
1309
+ } else {
1310
+ result[key] = value;
1311
+ }
1312
+ } else if (typeof value === "string" && isISODateString(value)) {
1313
+ result[key] = new Date(value);
1314
+ } else if (typeof value === "string" && value.startsWith("ISODate(") && value.endsWith(")")) {
1315
+ const dateString = value.substring(8, value.length - 2);
1316
+ if (isISODateString(dateString)) {
338
1317
  result[key] = new Date(dateString);
339
1318
  } else {
340
1319
  result[key] = value;
@@ -343,7 +1322,7 @@ function processObjectIdInFilter(filter, objectIdMode = "auto", depth = 0) {
343
1322
  if (Array.isArray(value)) {
344
1323
  result[key] = value.map((item) => {
345
1324
  if (typeof item === "string" && isObjectIdString(item) && (objectIdMode === "force" || objectIdMode === "auto" && isObjectIdField(key))) {
346
- return new ObjectId(item);
1325
+ return new ObjectId2(item);
347
1326
  } else if (typeof item === "string" && isISODateString(item)) {
348
1327
  return new Date(item);
349
1328
  } else if (typeof item === "string" && item.startsWith("ISODate(") && item.endsWith(")")) {
@@ -361,1294 +1340,760 @@ function processObjectIdInFilter(filter, objectIdMode = "auto", depth = 0) {
361
1340
  }
362
1341
  } else {
363
1342
  result[key] = value;
364
- }
365
- }
366
- return result;
367
- }
368
- function isObjectIdString(str) {
369
- return /^[0-9a-fA-F]{24}$/.test(str);
370
- }
371
- function isISODateString(str) {
372
- return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,3})?(Z|[+-]\d{2}:\d{2})$/.test(
373
- str
374
- );
375
- }
376
- function formatResponse(data) {
377
- return {
378
- content: [
379
- {
380
- type: "text",
381
- text: JSON.stringify(data, null, 2)
382
- }
383
- ]
384
- };
385
- }
386
- function toolExecutionError(message) {
387
- return {
388
- content: [{ type: "text", text: message }],
389
- isError: true
390
- };
391
- }
392
- function handleError(error, operation, collectionName) {
393
- if (error instanceof Error && error.name === "AbortError") {
394
- throw error;
395
- }
396
- const context = collectionName ? `collection ${collectionName}` : "operation";
397
- const message = error instanceof Error ? error.message : "Unknown error";
398
- return toolExecutionError(`Failed to ${operation} ${context}: ${message}`);
399
- }
400
- async function handleQuery(collection, args, objectIdMode = "auto", allowServerJs = false, signal) {
401
- if (!collection) {
402
- throw new Error("Collection is required for query operation");
403
- }
404
- const { filter, projection, explain, sort } = args;
405
- const limit = args.limit || 10;
406
- const skip = args.skip || 0;
407
- const queryFilter = parseFilter(filter, objectIdMode);
408
- if (!allowServerJs) {
409
- assertNoServerJs(queryFilter);
410
- }
411
- try {
412
- if (explain) {
413
- signal?.throwIfAborted();
414
- const explainResult = await collection.find(queryFilter, {
415
- projection,
416
- limit,
417
- skip,
418
- sort
419
- }).explain(explain);
420
- return formatResponse(explainResult);
421
- }
422
- signal?.throwIfAborted();
423
- const [total, results] = await Promise.all([
424
- collection.countDocuments(queryFilter),
425
- collection.find(queryFilter, {
426
- projection,
427
- limit,
428
- skip,
429
- sort
430
- }).toArray()
431
- ]);
432
- return formatResponse({
433
- results,
434
- metadata: {
435
- total,
436
- returned: results.length,
437
- skip,
438
- limit,
439
- hasMore: skip + results.length < total
440
- }
441
- });
442
- } catch (error) {
443
- return handleError(error, "query", collection.collectionName);
444
- }
445
- }
446
- async function handleAggregate(collection, args, objectIdMode = "auto", isReadOnlyMode = false, allowCrossDb = false, allowServerJs = false, signal) {
447
- if (!collection) {
448
- throw new Error("Collection is required for aggregate operation");
449
- }
450
- const { pipeline, explain } = args;
451
- if (!Array.isArray(pipeline)) {
452
- throw new Error("Pipeline must be an array");
453
- }
454
- if (isReadOnlyMode) {
455
- const forbidden = findAggOperator(
456
- pipeline,
457
- READONLY_FORBIDDEN_AGG_OPERATORS
458
- );
459
- if (forbidden) {
460
- throw new Error(
461
- `ReadonlyError: Aggregation operator '${forbidden}' is not allowed in read-only mode`
462
- );
463
- }
464
- }
465
- if (!allowServerJs) {
466
- assertNoServerJs(pipeline);
467
- }
468
- if (!allowCrossDb) {
469
- const foreignDb = findCrossDbTarget(pipeline, collection.dbName);
470
- if (foreignDb) {
471
- throw new Error(
472
- `Cross-database access to '${foreignDb}' is not allowed (connected database is '${collection.dbName}'). Enable it with --allow-cross-db or MCP_MONGODB_ALLOW_CROSS_DB=true.`
473
- );
474
- }
475
- }
476
- const processedPipeline = pipeline.map((stage) => {
477
- if (typeof stage === "object" && stage !== null) {
478
- return processObjectIdInFilter(
479
- stage,
480
- objectIdMode
481
- );
482
- }
483
- return stage;
484
- });
485
- try {
486
- if (explain) {
487
- signal?.throwIfAborted();
488
- const explainResult = await collection.aggregate(processedPipeline, {
489
- explain: {
490
- verbosity: explain
491
- }
492
- }).toArray();
493
- return formatResponse(explainResult);
494
- }
495
- signal?.throwIfAborted();
496
- const results = await collection.aggregate(processedPipeline).toArray();
497
- return formatResponse({
498
- results,
499
- metadata: {
500
- returned: results.length
501
- }
502
- });
503
- } catch (error) {
504
- return handleError(error, "aggregate", collection.collectionName);
505
- }
506
- }
507
- async function handleUpdate(collection, args, objectIdMode = "auto", allowServerJs = false, signal) {
508
- if (!collection) {
509
- throw new Error("Collection is required for update operation");
510
- }
511
- const { filter, update, upsert, multi } = args;
512
- const queryFilter = parseFilter(filter, objectIdMode);
513
- if (!allowServerJs) {
514
- assertNoServerJs(queryFilter);
515
- }
516
- let processedUpdate = update;
517
- if (update && typeof update === "object" && !Array.isArray(update)) {
518
- processedUpdate = processObjectIdInFilter(
519
- update,
520
- objectIdMode
521
- );
522
- }
523
- if (!processedUpdate || typeof processedUpdate !== "object" || Array.isArray(processedUpdate)) {
524
- throw new Error("Update must be a valid MongoDB update document");
525
- }
526
- const validUpdateOperators = [
527
- "$set",
528
- "$unset",
529
- "$inc",
530
- "$push",
531
- "$pull",
532
- "$addToSet",
533
- "$pop",
534
- "$rename",
535
- "$mul"
536
- ];
537
- const hasValidOperator = Object.keys(processedUpdate).some(
538
- (key) => validUpdateOperators.includes(key)
539
- );
540
- if (!hasValidOperator) {
541
- throw new Error(
542
- "Update must include at least one valid update operator ($set, $unset, etc.)"
543
- );
544
- }
545
- try {
546
- const options = {
547
- upsert: !!upsert,
548
- multi: !!multi
549
- };
550
- const updateMethod = options.multi ? "updateMany" : "updateOne";
551
- signal?.throwIfAborted();
552
- const result = await collection[updateMethod](
553
- queryFilter,
554
- processedUpdate,
555
- options
556
- );
557
- return formatResponse({
558
- matchedCount: result.matchedCount,
559
- modifiedCount: result.modifiedCount,
560
- upsertedCount: result.upsertedCount,
561
- upsertedId: result.upsertedId
562
- });
563
- } catch (error) {
564
- return handleError(error, "update", collection.collectionName);
565
- }
566
- }
567
- async function handleServerInfo(db, isReadOnlyMode, args, signal) {
568
- const { includeDebugInfo } = args;
569
- try {
570
- signal?.throwIfAborted();
571
- const buildInfo = await db.command({ buildInfo: 1 });
572
- const serverInfo = {
573
- version: buildInfo.version,
574
- gitVersion: buildInfo.gitVersion,
575
- storageEngines: buildInfo.storageEngines,
576
- maxBsonObjectSize: buildInfo.maxBsonObjectSize,
577
- bits: buildInfo.bits,
578
- ok: buildInfo.ok,
579
- connectionInfo: {
580
- readOnlyMode: isReadOnlyMode
581
- }
582
- };
583
- if (includeDebugInfo) {
584
- signal?.throwIfAborted();
585
- const serverStatus = await db.command({ serverStatus: 1 });
586
- serverInfo.modules = buildInfo.modules;
587
- serverInfo.allocator = buildInfo.allocator;
588
- serverInfo.javascriptEngine = buildInfo.javascriptEngine;
589
- serverInfo.sysInfo = buildInfo.sysInfo;
590
- serverInfo.debug = buildInfo.debug;
591
- serverInfo.openssl = buildInfo.openssl;
592
- serverInfo.buildEnvironment = buildInfo.buildEnvironment;
593
- serverInfo.status = {
594
- host: serverStatus.host,
595
- version: serverStatus.version,
596
- process: serverStatus.process,
597
- pid: serverStatus.pid,
598
- uptime: serverStatus.uptime,
599
- uptimeMillis: serverStatus.uptimeMillis,
600
- uptimeEstimate: serverStatus.uptimeEstimate,
601
- localTime: serverStatus.localTime,
602
- connections: serverStatus.connections,
603
- network: serverStatus.network,
604
- memory: serverStatus.mem,
605
- storageEngine: serverStatus.storageEngine,
606
- security: serverStatus.security
607
- };
608
- }
609
- return formatResponse(serverInfo);
610
- } catch (error) {
611
- return handleError(error, "get server information");
1343
+ }
612
1344
  }
1345
+ return result;
613
1346
  }
614
- async function handleInsert(collection, args, objectIdMode = "auto", signal) {
615
- if (!collection) {
616
- throw new Error("Collection is required for insert operation");
617
- }
618
- const { documents, ordered, writeConcern, bypassDocumentValidation } = args;
619
- if (!Array.isArray(documents)) {
620
- throw new Error("Documents must be an array");
1347
+ function isObjectIdString(str) {
1348
+ return /^[0-9a-fA-F]{24}$/.test(str);
1349
+ }
1350
+ function isISODateString(str) {
1351
+ return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,3})?(Z|[+-]\d{2}:\d{2})$/.test(
1352
+ str
1353
+ );
1354
+ }
1355
+ function formatResponse(data) {
1356
+ return {
1357
+ content: [
1358
+ {
1359
+ type: "text",
1360
+ text: JSON.stringify(data, null, 2)
1361
+ }
1362
+ ]
1363
+ };
1364
+ }
1365
+ function toolExecutionError(message) {
1366
+ return {
1367
+ content: [{ type: "text", text: message }],
1368
+ isError: true
1369
+ };
1370
+ }
1371
+ function handleError(error, operation, collectionName) {
1372
+ if (error instanceof Error && error.name === "AbortError") {
1373
+ throw error;
621
1374
  }
622
- if (documents.length === 0) {
623
- throw new Error("Documents array cannot be empty");
1375
+ const context = collectionName ? `collection ${collectionName}` : "operation";
1376
+ const message = error instanceof Error ? error.message : "Unknown error";
1377
+ return toolExecutionError(`Failed to ${operation} ${context}: ${message}`);
1378
+ }
1379
+ async function handleQuery(collection, args, objectIdMode = "auto", allowServerJs = false, limits = {}, signal) {
1380
+ if (!collection) {
1381
+ throw new Error("Collection is required for query operation");
624
1382
  }
625
- if (!documents.every(
626
- (doc) => doc && typeof doc === "object" && !Array.isArray(doc)
627
- )) {
628
- throw new Error("Each document must be a valid MongoDB document object");
1383
+ const { filter, projection, explain, sort, includeTotal = true } = args;
1384
+ if (typeof includeTotal !== "boolean")
1385
+ throw new Error("includeTotal must be a boolean");
1386
+ const limit = args.limit || 10;
1387
+ const skip = args.skip || 0;
1388
+ const queryFilter = parseFilter(filter, objectIdMode);
1389
+ if (!allowServerJs) {
1390
+ assertNoServerJs(queryFilter);
629
1391
  }
630
- const processedDocuments = documents.map(
631
- (doc) => processObjectIdInFilter(doc, objectIdMode)
632
- );
633
1392
  try {
634
- const options = {
635
- ordered: ordered !== false,
636
- // default to true if not specified
637
- writeConcern,
638
- bypassDocumentValidation
639
- };
1393
+ if (explain) {
1394
+ signal?.throwIfAborted();
1395
+ const explainResult = await collection.find(queryFilter, {
1396
+ projection,
1397
+ limit,
1398
+ skip,
1399
+ sort,
1400
+ signal,
1401
+ timeoutMS: limits.timeoutMS
1402
+ }).explain(explain);
1403
+ return formatResponse(explainResult);
1404
+ }
640
1405
  signal?.throwIfAborted();
641
- const result = await collection.insertMany(
642
- processedDocuments,
643
- options
1406
+ const bounded = limits.documents !== void 0 || limits.bytes !== void 0 || !includeTotal;
1407
+ const pageSize = Math.abs(limit);
1408
+ if (bounded && !Number.isSafeInteger(limit)) {
1409
+ throw new Error("limit must be an integer");
1410
+ }
1411
+ const pageRows = Math.min(pageSize, limits.documents ?? Infinity);
1412
+ const fetchLimit = bounded ? Math.sign(limit) * Math.min(pageRows + 1, Number.MAX_SAFE_INTEGER) : limit;
1413
+ const cancelPeer = new AbortController();
1414
+ const operationSignal = signal ? AbortSignal.any([signal, cancelPeer.signal]) : cancelPeer.signal;
1415
+ const options = { signal: operationSignal, timeoutMS: limits.timeoutMS };
1416
+ const cursor = collection.find(queryFilter, {
1417
+ projection,
1418
+ limit: fetchLimit,
1419
+ skip,
1420
+ sort,
1421
+ ...options
1422
+ });
1423
+ const read = bounded ? readResultPage(cursor, limits, pageSize, operationSignal) : cursor.toArray().then((results) => ({
1424
+ results,
1425
+ hasMore: false,
1426
+ truncated: void 0
1427
+ }));
1428
+ const count = includeTotal ? collection.countDocuments(queryFilter, options) : void 0;
1429
+ const [total, page] = await Promise.all([count, read]).catch(
1430
+ async (error) => {
1431
+ cancelPeer.abort(error);
1432
+ await Promise.allSettled([count, read]);
1433
+ throw error;
1434
+ }
644
1435
  );
645
1436
  return formatResponse({
646
- acknowledged: result.acknowledged,
647
- insertedCount: result.insertedCount,
648
- insertedIds: result.insertedIds
1437
+ results: page.results,
1438
+ metadata: {
1439
+ ...total !== void 0 ? { total } : {},
1440
+ returned: page.results.length,
1441
+ skip,
1442
+ limit,
1443
+ hasMore: total === void 0 ? page.hasMore : page.hasMore || skip + page.results.length < total,
1444
+ ...page.truncated ? { truncated: page.truncated } : {}
1445
+ }
649
1446
  });
650
1447
  } catch (error) {
651
- if (error instanceof Error && error.name === "BulkWriteError") {
652
- const bulkError = error;
653
- return formatResponse({
654
- error: "Bulk write error occurred",
655
- writeErrors: bulkError.writeErrors,
656
- insertedCount: bulkError.result?.nInserted || 0,
657
- failedCount: bulkError.result?.nFailedInserts || 0
658
- });
659
- }
660
- return handleError(error, "insert", collection.collectionName);
1448
+ signal?.throwIfAborted();
1449
+ return handleError(error, "query", collection.collectionName);
661
1450
  }
662
1451
  }
663
- async function handleCreateIndex(collection, args, objectIdMode = "auto", signal) {
1452
+ async function handleAggregate(collection, args, objectIdMode = "auto", isReadOnlyMode = false, allowCrossDb = false, allowServerJs = false, limits = {}, signal) {
664
1453
  if (!collection) {
665
- throw new Error("Collection is required for createIndex operation");
1454
+ throw new Error("Collection is required for aggregate operation");
666
1455
  }
667
- const { indexes, commitQuorum, writeConcern } = args;
668
- if (!Array.isArray(indexes) || indexes.length === 0) {
669
- throw new Error("Indexes must be a non-empty array");
1456
+ const { pipeline, explain } = args;
1457
+ if (!Array.isArray(pipeline)) {
1458
+ throw new Error("Pipeline must be an array");
670
1459
  }
671
- if (writeConcern && (typeof writeConcern !== "object" || Array.isArray(writeConcern))) {
672
- throw new Error(
673
- "Write concern must be a valid MongoDB write concern object"
1460
+ if (isReadOnlyMode) {
1461
+ const forbidden = findAggOperator(
1462
+ pipeline,
1463
+ READONLY_FORBIDDEN_AGG_OPERATORS
674
1464
  );
675
- }
676
- if (commitQuorum !== void 0 && typeof commitQuorum !== "string" && typeof commitQuorum !== "number") {
677
- throw new Error("Commit quorum must be a string or number");
678
- }
679
- const processedIndexes = indexes.map((index) => {
680
- if (index && typeof index === "object") {
681
- return processObjectIdInFilter(
682
- index,
683
- objectIdMode
1465
+ if (forbidden) {
1466
+ throw new Error(
1467
+ `ReadonlyError: Aggregation operator '${forbidden}' is not allowed in read-only mode`
684
1468
  );
685
1469
  }
686
- return index;
687
- });
688
- try {
689
- const indexOptions = {
690
- commitQuorum
691
- };
692
- signal?.throwIfAborted();
693
- const createdIndexes = await collection.createIndexes(
694
- processedIndexes,
695
- indexOptions
696
- );
697
- return formatResponse({
698
- acknowledged: true,
699
- createdIndexes,
700
- indexCount: createdIndexes.length
701
- });
702
- } catch (error) {
703
- return handleError(error, "create indexes", collection.collectionName);
704
- }
705
- }
706
- async function handleCount(collection, args, objectIdMode = "auto", allowServerJs = false, signal) {
707
- if (!collection) {
708
- throw new Error("Collection is required for count operation");
709
1470
  }
710
- const { query, limit, skip, hint, readConcern, maxTimeMS, collation } = args;
711
- const countQuery = parseFilter(query, objectIdMode);
712
1471
  if (!allowServerJs) {
713
- assertNoServerJs(countQuery);
1472
+ assertNoServerJs(pipeline);
714
1473
  }
715
- try {
716
- const options = {
717
- limit: typeof limit === "number" ? limit : void 0,
718
- skip: typeof skip === "number" ? skip : void 0,
719
- hint: typeof hint === "object" && hint !== null ? hint : void 0,
720
- readConcern: typeof readConcern === "object" && readConcern !== null ? readConcern : void 0,
721
- maxTimeMS: typeof maxTimeMS === "number" ? maxTimeMS : void 0,
722
- collation: typeof collation === "object" && collation !== null ? collation : void 0
723
- };
724
- for (const key of Object.keys(options)) {
725
- if (options[key] === void 0) {
726
- delete options[key];
727
- }
1474
+ if (!allowCrossDb) {
1475
+ const foreignDb = findCrossDbTarget(pipeline, collection.dbName);
1476
+ if (foreignDb) {
1477
+ throw new Error(
1478
+ `Cross-database access to '${foreignDb}' is not allowed (connected database is '${collection.dbName}'). Enable it with --allow-cross-db or MCP_MONGODB_ALLOW_CROSS_DB=true.`
1479
+ );
728
1480
  }
729
- signal?.throwIfAborted();
730
- const count = await collection.countDocuments(countQuery, options);
731
- return formatResponse({
732
- count,
733
- ok: 1
734
- });
735
- } catch (error) {
736
- return handleError(error, "count documents", collection.collectionName);
737
1481
  }
738
- }
739
- function handleConvertTime(args) {
740
- const { input } = args;
741
- let date;
742
- if (input === void 0 || input === null || input === "") {
743
- date = /* @__PURE__ */ new Date();
744
- } else if (typeof input === "number") {
745
- date = new Date(Math.abs(input) < 1e12 ? input * 1e3 : input);
746
- } else if (typeof input === "string") {
747
- const trimmed = input.trim();
748
- if (/^-?\d+$/.test(trimmed)) {
749
- const n = Number(trimmed);
750
- date = new Date(Math.abs(n) < 1e12 ? n * 1e3 : n);
751
- } else {
752
- date = new Date(trimmed);
1482
+ const processedPipeline = pipeline.map((stage) => {
1483
+ if (typeof stage === "object" && stage !== null) {
1484
+ return processObjectIdInFilter(
1485
+ stage,
1486
+ objectIdMode
1487
+ );
753
1488
  }
754
- } else {
755
- throw new Error(
756
- "convertTime 'input' must be a Unix timestamp (number) or a date string"
757
- );
758
- }
759
- if (Number.isNaN(date.getTime())) {
760
- throw new Error(
761
- `Could not interpret '${String(input)}' as a date or Unix timestamp`
762
- );
763
- }
764
- const offsetMinutes = -date.getTimezoneOffset();
765
- const sign = offsetMinutes >= 0 ? "+" : "-";
766
- const absMinutes = Math.abs(offsetMinutes);
767
- const offsetLabel = `${sign}${String(Math.floor(absMinutes / 60)).padStart(
768
- 2,
769
- "0"
770
- )}:${String(absMinutes % 60).padStart(2, "0")}`;
771
- return formatResponse({
772
- iso: date.toISOString(),
773
- utc: date.toUTCString(),
774
- unixSeconds: Math.floor(date.getTime() / 1e3),
775
- unixMillis: date.getTime(),
776
- serverLocal: date.toString(),
777
- serverTimeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
778
- serverUtcOffset: offsetLabel
1489
+ return stage;
779
1490
  });
780
- }
781
- async function handleListCollections(db, args, objectIdMode = "auto", signal) {
782
- const { nameOnly, filter } = args;
783
- const skip = args.skip || 0;
784
- const limit = args.limit || 20;
785
1491
  try {
786
- const processedFilter = parseFilter(filter, objectIdMode);
1492
+ if (explain) {
1493
+ signal?.throwIfAborted();
1494
+ const explainResult = await collection.aggregate(processedPipeline, {
1495
+ explain: {
1496
+ verbosity: explain
1497
+ },
1498
+ signal,
1499
+ timeoutMS: limits.timeoutMS
1500
+ }).toArray();
1501
+ return formatResponse(explainResult);
1502
+ }
787
1503
  signal?.throwIfAborted();
788
- const collections = await db.listCollections(processedFilter).toArray();
789
- const allResults = nameOnly ? collections.map((collection) => collection.name) : collections;
790
- const total = allResults.length;
791
- const paged = allResults.slice(skip, skip + limit);
792
- return formatResponse({
793
- results: paged,
794
- metadata: {
795
- total,
796
- returned: paged.length,
797
- skip,
798
- limit,
799
- hasMore: skip + paged.length < total
1504
+ const cursor = collection.aggregate(processedPipeline, {
1505
+ signal,
1506
+ timeoutMS: limits.timeoutMS
1507
+ });
1508
+ const { results, truncated } = limits.documents !== void 0 || limits.bytes !== void 0 ? await readResultPage(cursor, limits, Infinity, signal) : { results: await cursor.toArray(), truncated: void 0 };
1509
+ return formatResponse({
1510
+ results,
1511
+ metadata: {
1512
+ returned: results.length,
1513
+ ...truncated ? { truncated } : {}
800
1514
  }
801
1515
  });
802
1516
  } catch (error) {
803
- return handleError(error, "list collections");
804
- }
805
- }
806
-
807
- // src/utils/pagination.ts
808
- var DEFAULT_PAGE_SIZE = 20;
809
- function encodeCursor(offset) {
810
- const data = { offset };
811
- return Buffer.from(JSON.stringify(data)).toString("base64");
812
- }
813
- function decodeCursor(cursor) {
814
- if (!cursor) return 0;
815
- try {
816
- const data = JSON.parse(
817
- Buffer.from(cursor, "base64").toString("utf-8")
818
- );
819
- return typeof data.offset === "number" && data.offset >= 0 ? data.offset : 0;
820
- } catch {
821
- return 0;
1517
+ signal?.throwIfAborted();
1518
+ return handleError(error, "aggregate", collection.collectionName);
822
1519
  }
823
1520
  }
824
- function paginate(items, cursor, pageSize = DEFAULT_PAGE_SIZE) {
825
- const offset = decodeCursor(cursor);
826
- const page = items.slice(offset, offset + pageSize);
827
- const nextOffset = offset + pageSize;
828
- const nextCursor = nextOffset < items.length ? encodeCursor(nextOffset) : void 0;
829
- return { items: page, nextCursor };
830
- }
831
-
832
- // src/schemas/completion.ts
833
- async function handleCompletionRequest({
834
- request,
835
- client,
836
- db,
837
- isReadOnlyMode,
838
- signal
839
- }) {
840
- const { ref, argument } = request.params;
841
- if (ref.type === "ref/prompt") {
842
- return handlePromptCompletion(
843
- client,
844
- db,
845
- isReadOnlyMode,
846
- ref.name,
847
- argument,
848
- signal
849
- );
850
- }
851
- if (ref.type === "ref/resource") {
852
- return handleResourceCompletion(
853
- client,
854
- db,
855
- isReadOnlyMode,
856
- ref.uri,
857
- argument,
858
- signal
859
- );
1521
+ async function handleUpdate(collection, args, objectIdMode = "auto", allowServerJs = false, signal) {
1522
+ if (!collection) {
1523
+ throw new Error("Collection is required for update operation");
860
1524
  }
861
- return emptyCompletionResult();
862
- }
863
- async function handlePromptCompletion(client, db, isReadOnlyMode, promptName, argument, signal) {
864
- if (!promptName) {
865
- return emptyCompletionResult();
1525
+ const { filter, update, upsert, multi } = args;
1526
+ const queryFilter = parseFilter(filter, objectIdMode);
1527
+ if (!allowServerJs) {
1528
+ assertNoServerJs(queryFilter);
866
1529
  }
867
- if (argument.name === "collection") {
868
- return await completeCollectionNames(
869
- argument.value,
870
- db,
871
- isReadOnlyMode,
872
- signal
1530
+ let processedUpdate = update;
1531
+ if (update && typeof update === "object" && !Array.isArray(update)) {
1532
+ processedUpdate = processObjectIdInFilter(
1533
+ update,
1534
+ objectIdMode
873
1535
  );
874
1536
  }
875
- return emptyCompletionResult();
876
- }
877
- async function handleResourceCompletion(client, db, isReadOnlyMode, promptName, argument, signal) {
878
- if (!promptName) {
879
- return emptyCompletionResult();
1537
+ if (!processedUpdate || typeof processedUpdate !== "object" || Array.isArray(processedUpdate)) {
1538
+ throw new Error("Update must be a valid MongoDB update document");
880
1539
  }
881
- if (argument.name === "collection") {
882
- return await completeCollectionNames(
883
- argument.value,
884
- db,
885
- isReadOnlyMode,
886
- signal
1540
+ const validUpdateOperators = [
1541
+ "$set",
1542
+ "$unset",
1543
+ "$inc",
1544
+ "$push",
1545
+ "$pull",
1546
+ "$addToSet",
1547
+ "$pop",
1548
+ "$rename",
1549
+ "$mul"
1550
+ ];
1551
+ const hasValidOperator = Object.keys(processedUpdate).some(
1552
+ (key) => validUpdateOperators.includes(key)
1553
+ );
1554
+ if (!hasValidOperator) {
1555
+ throw new Error(
1556
+ "Update must include at least one valid update operator ($set, $unset, etc.)"
887
1557
  );
888
1558
  }
889
- return emptyCompletionResult();
890
- }
891
- async function completeCollectionNames(partialValue, db, isReadOnlyMode, signal) {
892
1559
  try {
893
- console.warn(
894
- `Completing collection names with partial value: ${partialValue}`
895
- );
1560
+ const options = {
1561
+ upsert: !!upsert,
1562
+ multi: !!multi
1563
+ };
1564
+ const updateMethod = options.multi ? "updateMany" : "updateOne";
896
1565
  signal?.throwIfAborted();
897
- const collections = await db.listCollections().toArray();
898
- const matchingCollections = collections.map(
899
- (c) => c.name
900
- ).filter(
901
- (name) => (
902
- // Filter out system collections
903
- !name.startsWith("system.") && // Match partial value
904
- name.toLowerCase().includes(partialValue.toLowerCase())
905
- )
906
- ).sort();
907
- console.warn(`Found ${matchingCollections.length} matching collections`);
908
- const PAGE_SIZE = 100;
909
- const { items: limitedResults, nextCursor } = paginate(
910
- matchingCollections,
911
- void 0,
912
- PAGE_SIZE
1566
+ const result = await collection[updateMethod](
1567
+ queryFilter,
1568
+ processedUpdate,
1569
+ options
913
1570
  );
914
- return {
915
- completion: {
916
- values: limitedResults,
917
- total: matchingCollections.length,
918
- hasMore: !!nextCursor
919
- }
920
- };
1571
+ return formatResponse({
1572
+ matchedCount: result.matchedCount,
1573
+ modifiedCount: result.modifiedCount,
1574
+ upsertedCount: result.upsertedCount,
1575
+ upsertedId: result.upsertedId
1576
+ });
921
1577
  } catch (error) {
922
- console.error("Error completing collection names:", error);
923
- return emptyCompletionResult();
1578
+ return handleError(error, "update", collection.collectionName);
924
1579
  }
925
1580
  }
926
- function emptyCompletionResult() {
927
- return {
928
- completion: {
929
- values: [],
930
- total: 0,
931
- hasMore: false
932
- }
933
- };
934
- }
935
-
936
- // src/schemas/ping.ts
937
- async function handlePingRequest({
938
- request,
939
- client,
940
- db,
941
- isReadOnlyMode,
942
- signal
943
- }) {
1581
+ async function handleServerInfo(db, isReadOnlyMode, args, signal) {
1582
+ const { includeDebugInfo } = args;
944
1583
  try {
945
- if (!client) {
946
- throw new Error("MongoDB connection is not available");
947
- }
948
1584
  signal?.throwIfAborted();
949
- const pong = await db.command({ ping: 1 });
950
- if (pong.ok !== 1) {
951
- throw new Error(`MongoDB ping failed: ${pong.errmsg}`);
1585
+ const buildInfo = await db.command({ buildInfo: 1 });
1586
+ const serverInfo = {
1587
+ version: buildInfo.version,
1588
+ gitVersion: buildInfo.gitVersion,
1589
+ storageEngines: buildInfo.storageEngines,
1590
+ maxBsonObjectSize: buildInfo.maxBsonObjectSize,
1591
+ bits: buildInfo.bits,
1592
+ ok: buildInfo.ok,
1593
+ connectionInfo: {
1594
+ readOnlyMode: isReadOnlyMode
1595
+ }
1596
+ };
1597
+ if (includeDebugInfo) {
1598
+ signal?.throwIfAborted();
1599
+ const serverStatus = await db.command({ serverStatus: 1 });
1600
+ serverInfo.modules = buildInfo.modules;
1601
+ serverInfo.allocator = buildInfo.allocator;
1602
+ serverInfo.javascriptEngine = buildInfo.javascriptEngine;
1603
+ serverInfo.sysInfo = buildInfo.sysInfo;
1604
+ serverInfo.debug = buildInfo.debug;
1605
+ serverInfo.openssl = buildInfo.openssl;
1606
+ serverInfo.buildEnvironment = buildInfo.buildEnvironment;
1607
+ serverInfo.status = {
1608
+ host: serverStatus.host,
1609
+ version: serverStatus.version,
1610
+ process: serverStatus.process,
1611
+ pid: serverStatus.pid,
1612
+ uptime: serverStatus.uptime,
1613
+ uptimeMillis: serverStatus.uptimeMillis,
1614
+ uptimeEstimate: serverStatus.uptimeEstimate,
1615
+ localTime: serverStatus.localTime,
1616
+ connections: serverStatus.connections,
1617
+ network: serverStatus.network,
1618
+ memory: serverStatus.mem,
1619
+ storageEngine: serverStatus.storageEngine,
1620
+ security: serverStatus.security
1621
+ };
952
1622
  }
953
- return {};
1623
+ return formatResponse(serverInfo);
954
1624
  } catch (error) {
955
- if (error instanceof Error) {
956
- throw new Error(`MongoDB ping failed: ${error.message}`);
957
- }
958
- throw new Error("MongoDB ping failed: Unknown error");
1625
+ return handleError(error, "get server information");
959
1626
  }
960
1627
  }
961
-
962
- // src/schemas/prompts.ts
963
- async function handleListPromptsRequest({
964
- request,
965
- client,
966
- db,
967
- isReadOnlyMode,
968
- signal
969
- }) {
970
- return {
971
- prompts: [
972
- {
973
- name: "analyze_collection",
974
- description: "Analyze a MongoDB collection structure and contents",
975
- arguments: [
976
- {
977
- name: "collection",
978
- description: "Name of the collection to analyze",
979
- required: true
980
- }
981
- ]
982
- }
983
- ]
984
- };
985
- }
986
- async function handleGetPromptRequest({
987
- request,
988
- client,
989
- db,
990
- isReadOnlyMode,
991
- signal,
992
- sendProgress
993
- }) {
994
- const { name, arguments: args = {} } = request.params;
995
- if (name !== "analyze_collection") {
996
- throw new Error("Unknown prompt");
1628
+ async function handleInsert(collection, args, objectIdMode = "auto", signal) {
1629
+ if (!collection) {
1630
+ throw new Error("Collection is required for insert operation");
997
1631
  }
998
- const collectionName = args.collection;
999
- if (!collectionName) {
1000
- throw new Error("Collection name is required");
1632
+ const { documents, ordered, writeConcern, bypassDocumentValidation } = args;
1633
+ if (!Array.isArray(documents)) {
1634
+ throw new Error("Documents must be an array");
1635
+ }
1636
+ if (documents.length === 0) {
1637
+ throw new Error("Documents array cannot be empty");
1638
+ }
1639
+ if (!documents.every(
1640
+ (doc) => doc && typeof doc === "object" && !Array.isArray(doc)
1641
+ )) {
1642
+ throw new Error("Each document must be a valid MongoDB document object");
1001
1643
  }
1644
+ const processedDocuments = documents.map(
1645
+ (doc) => processObjectIdInFilter(doc, objectIdMode)
1646
+ );
1002
1647
  try {
1003
- const collection = db.collection(collectionName);
1004
- if (collection.collectionName.startsWith("system.")) {
1005
- throw new Error("Access to system collections is not allowed");
1006
- }
1007
- await sendProgress?.(1, 3, "Getting schema");
1008
- signal?.throwIfAborted();
1009
- const schemaSample = await collection.findOne({});
1010
- await sendProgress?.(2, 3, "Getting stats");
1011
- signal?.throwIfAborted();
1012
- const stats = await collection.aggregate([{ $collStats: { count: {} } }]).toArray();
1013
- await sendProgress?.(3, 3, "Fetching samples");
1014
- signal?.throwIfAborted();
1015
- const sampleDocs = await collection.find({}).limit(5).toArray();
1016
- const documentCount = stats[0]?.count ?? "unknown";
1017
- return {
1018
- messages: [
1019
- {
1020
- role: "user",
1021
- content: {
1022
- type: "text",
1023
- text: `Please analyze the following MongoDB collection:
1024
- Collection: ${collectionName}
1025
-
1026
- Schema:
1027
- ${JSON.stringify(schemaSample, null, 2)}
1028
-
1029
- Stats:
1030
- Document count: ${documentCount}
1031
-
1032
- Sample documents:
1033
- ${JSON.stringify(sampleDocs, null, 2)}`
1034
- }
1035
- },
1036
- {
1037
- role: "user",
1038
- content: {
1039
- type: "text",
1040
- text: "Provide insights about the collection's structure, data types, and basic statistics."
1041
- }
1042
- }
1043
- ]
1648
+ const options = {
1649
+ ordered: ordered !== false,
1650
+ // default to true if not specified
1651
+ writeConcern,
1652
+ bypassDocumentValidation
1044
1653
  };
1654
+ signal?.throwIfAborted();
1655
+ const result = await collection.insertMany(
1656
+ processedDocuments,
1657
+ options
1658
+ );
1659
+ return formatResponse({
1660
+ acknowledged: result.acknowledged,
1661
+ insertedCount: result.insertedCount,
1662
+ insertedIds: result.insertedIds
1663
+ });
1045
1664
  } catch (error) {
1046
- const msg = error instanceof Error ? error.message : "Unknown error";
1047
- throw new Error(`Failed to analyze collection ${collectionName}: ${msg}`);
1048
- }
1049
- }
1050
-
1051
- // src/schemas/resource.ts
1052
- import {
1053
- ProtocolError as ProtocolError2,
1054
- ProtocolErrorCode as ProtocolErrorCode2
1055
- } from "@modelcontextprotocol/server";
1056
- import { ObjectId as ObjectId2 } from "mongodb";
1057
- function detectMongoType(value) {
1058
- if (value === null) return "null";
1059
- if (value === void 0) return "undefined";
1060
- if (value instanceof ObjectId2) return "ObjectId";
1061
- if (value instanceof Date) return "Date";
1062
- if (Array.isArray(value)) {
1063
- if (value.length === 0) return "Array";
1064
- const elementTypes = new Set(value.map((item) => detectMongoType(item)));
1065
- if (elementTypes.size === 1) {
1066
- return `Array<${Array.from(elementTypes)[0]}>`;
1665
+ if (error instanceof Error && error.name === "BulkWriteError") {
1666
+ const bulkError = error;
1667
+ return formatResponse({
1668
+ error: "Bulk write error occurred",
1669
+ writeErrors: bulkError.writeErrors,
1670
+ insertedCount: bulkError.result?.nInserted || 0,
1671
+ failedCount: bulkError.result?.nFailedInserts || 0
1672
+ });
1067
1673
  }
1068
- return "Array<mixed>";
1069
- }
1070
- if (typeof value === "object") {
1071
- return "Document";
1674
+ return handleError(error, "insert", collection.collectionName);
1072
1675
  }
1073
- return typeof value;
1074
1676
  }
1075
- function inferSchemaFromSamples(documents) {
1076
- if (!documents || documents.length === 0) {
1077
- return { fields: [] };
1677
+ async function handleCreateIndex(collection, args, objectIdMode = "auto", signal) {
1678
+ if (!collection) {
1679
+ throw new Error("Collection is required for createIndex operation");
1078
1680
  }
1079
- const fieldMap = /* @__PURE__ */ new Map();
1080
- for (const doc of documents) {
1081
- for (const [key, value] of Object.entries(doc)) {
1082
- if (!fieldMap.has(key)) {
1083
- fieldMap.set(key, {
1084
- name: key,
1085
- types: /* @__PURE__ */ new Set([detectMongoType(value)]),
1086
- nullable: false,
1087
- // Store sample values for complex types
1088
- samples: [value]
1089
- });
1090
- } else {
1091
- const fieldInfo = fieldMap.get(key);
1092
- fieldInfo.types.add(detectMongoType(value));
1093
- if (fieldInfo.samples.length < 3 && !fieldInfo.samples.some(
1094
- (sample) => JSON.stringify(sample) === JSON.stringify(value)
1095
- )) {
1096
- fieldInfo.samples.push(value);
1097
- }
1098
- }
1099
- }
1681
+ const { indexes, commitQuorum, writeConcern } = args;
1682
+ if (!Array.isArray(indexes) || indexes.length === 0) {
1683
+ throw new Error("Indexes must be a non-empty array");
1100
1684
  }
1101
- for (const doc of documents) {
1102
- for (const [key] of fieldMap.entries()) {
1103
- if (!(key in doc)) {
1104
- const fieldInfo = fieldMap.get(key);
1105
- fieldInfo.nullable = true;
1106
- }
1107
- }
1685
+ if (writeConcern && (typeof writeConcern !== "object" || Array.isArray(writeConcern))) {
1686
+ throw new Error(
1687
+ "Write concern must be a valid MongoDB write concern object"
1688
+ );
1108
1689
  }
1109
- for (const [key, fieldInfo] of fieldMap.entries()) {
1110
- if (fieldInfo.types.has("Document")) {
1111
- const nestedDocs = documents.filter(
1112
- (doc) => doc[key] && typeof doc[key] === "object" && !Array.isArray(doc[key])
1113
- ).map((doc) => doc[key]);
1114
- if (nestedDocs.length > 0) {
1115
- fieldInfo.nestedSchema = inferSchemaFromSamples(nestedDocs);
1116
- }
1117
- }
1690
+ if (commitQuorum !== void 0 && typeof commitQuorum !== "string" && typeof commitQuorum !== "number") {
1691
+ throw new Error("Commit quorum must be a string or number");
1118
1692
  }
1119
- const fields = Array.from(fieldMap.values()).map((fieldInfo) => {
1120
- const result = {
1121
- name: fieldInfo.name,
1122
- types: Array.from(fieldInfo.types),
1123
- nullable: fieldInfo.nullable,
1124
- prevalence: `${Math.round(
1125
- documents.filter((doc) => fieldInfo.name in doc).length / documents.length * 100
1126
- )}%`,
1127
- examples: []
1128
- };
1129
- if (fieldInfo.nestedSchema) {
1130
- result.nestedSchema = fieldInfo.nestedSchema;
1693
+ const processedIndexes = indexes.map((index) => {
1694
+ if (index && typeof index === "object") {
1695
+ return processObjectIdInFilter(
1696
+ index,
1697
+ objectIdMode
1698
+ );
1131
1699
  }
1132
- const sampleValues = fieldInfo.samples.map((sample) => {
1133
- if (sample instanceof ObjectId2) return sample.toString();
1134
- if (sample instanceof Date) return sample.toISOString();
1135
- if (typeof sample === "object") {
1136
- return Array.isArray(sample) ? "[...]" : "{...}";
1137
- }
1138
- return sample;
1139
- });
1140
- result.examples = sampleValues;
1141
- return result;
1700
+ return index;
1142
1701
  });
1143
- return { fields };
1702
+ try {
1703
+ const indexOptions = {
1704
+ commitQuorum
1705
+ };
1706
+ signal?.throwIfAborted();
1707
+ const createdIndexes = await collection.createIndexes(
1708
+ processedIndexes,
1709
+ indexOptions
1710
+ );
1711
+ return formatResponse({
1712
+ acknowledged: true,
1713
+ createdIndexes,
1714
+ indexCount: createdIndexes.length
1715
+ });
1716
+ } catch (error) {
1717
+ return handleError(error, "create indexes", collection.collectionName);
1718
+ }
1144
1719
  }
1145
- async function handleReadResourceRequest({
1146
- request,
1147
- client,
1148
- db,
1149
- isReadOnlyMode,
1150
- signal,
1151
- sendProgress
1152
- }) {
1153
- let collectionName;
1720
+ async function handleCount(collection, args, objectIdMode = "auto", allowServerJs = false, signal) {
1721
+ if (!collection) {
1722
+ throw new Error("Collection is required for count operation");
1723
+ }
1724
+ const { query, limit, skip, hint, readConcern, maxTimeMS, collation } = args;
1725
+ const countQuery = parseFilter(query, objectIdMode);
1726
+ if (!allowServerJs) {
1727
+ assertNoServerJs(countQuery);
1728
+ }
1154
1729
  try {
1155
- const url = new URL(request.params.uri);
1156
- if (url.protocol !== "mongodb:") {
1157
- throw new ProtocolError2(
1158
- ProtocolErrorCode2.InvalidParams,
1159
- `Unsupported resource URI scheme '${url.protocol}': expected 'mongodb:'`
1160
- );
1730
+ const options = {
1731
+ limit: typeof limit === "number" ? limit : void 0,
1732
+ skip: typeof skip === "number" ? skip : void 0,
1733
+ hint: typeof hint === "object" && hint !== null ? hint : void 0,
1734
+ readConcern: typeof readConcern === "object" && readConcern !== null ? readConcern : void 0,
1735
+ maxTimeMS: typeof maxTimeMS === "number" ? maxTimeMS : void 0,
1736
+ collation: typeof collation === "object" && collation !== null ? collation : void 0
1737
+ };
1738
+ for (const key of Object.keys(options)) {
1739
+ if (options[key] === void 0) {
1740
+ delete options[key];
1741
+ }
1161
1742
  }
1162
- collectionName = decodeURIComponent(url.pathname.replace(/^\//, ""));
1743
+ signal?.throwIfAborted();
1744
+ const count = await collection.countDocuments(countQuery, options);
1745
+ return formatResponse({
1746
+ count,
1747
+ ok: 1
1748
+ });
1163
1749
  } catch (error) {
1164
- if (error instanceof ProtocolError2) throw error;
1165
- throw new ProtocolError2(
1166
- ProtocolErrorCode2.InvalidParams,
1167
- `Invalid resource URI: ${request.params.uri}`
1168
- );
1750
+ return handleError(error, "count documents", collection.collectionName);
1169
1751
  }
1170
- if (!collectionName) {
1171
- throw new ProtocolError2(
1172
- ProtocolErrorCode2.InvalidParams,
1173
- "Resource URI is missing a collection name"
1752
+ }
1753
+ function handleConvertTime(args) {
1754
+ const { input } = args;
1755
+ let date;
1756
+ if (input === void 0 || input === null || input === "") {
1757
+ date = /* @__PURE__ */ new Date();
1758
+ } else if (typeof input === "number") {
1759
+ date = new Date(Math.abs(input) < 1e12 ? input * 1e3 : input);
1760
+ } else if (typeof input === "string") {
1761
+ const trimmed = input.trim();
1762
+ if (/^-?\d+$/.test(trimmed)) {
1763
+ const n = Number(trimmed);
1764
+ date = new Date(Math.abs(n) < 1e12 ? n * 1e3 : n);
1765
+ } else {
1766
+ date = new Date(trimmed);
1767
+ }
1768
+ } else {
1769
+ throw new Error(
1770
+ "convertTime 'input' must be a Unix timestamp (number) or a date string"
1174
1771
  );
1175
1772
  }
1176
- if (collectionName.startsWith("system.")) {
1177
- throw new ProtocolError2(
1178
- ProtocolErrorCode2.InvalidParams,
1179
- "Access to system collections is not allowed"
1773
+ if (Number.isNaN(date.getTime())) {
1774
+ throw new Error(
1775
+ `Could not interpret '${String(input)}' as a date or Unix timestamp`
1180
1776
  );
1181
1777
  }
1778
+ const offsetMinutes = -date.getTimezoneOffset();
1779
+ const sign = offsetMinutes >= 0 ? "+" : "-";
1780
+ const absMinutes = Math.abs(offsetMinutes);
1781
+ const offsetLabel = `${sign}${String(Math.floor(absMinutes / 60)).padStart(
1782
+ 2,
1783
+ "0"
1784
+ )}:${String(absMinutes % 60).padStart(2, "0")}`;
1785
+ return formatResponse({
1786
+ iso: date.toISOString(),
1787
+ utc: date.toUTCString(),
1788
+ unixSeconds: Math.floor(date.getTime() / 1e3),
1789
+ unixMillis: date.getTime(),
1790
+ serverLocal: date.toString(),
1791
+ serverTimeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
1792
+ serverUtcOffset: offsetLabel
1793
+ });
1794
+ }
1795
+ async function handleListCollections(db, args, objectIdMode = "auto", signal) {
1796
+ const { nameOnly, filter } = args;
1797
+ const skip = args.skip || 0;
1798
+ const limit = args.limit || 20;
1182
1799
  try {
1183
- const collection = db.collection(collectionName);
1184
- const sampleSize = 100;
1185
- let sampleDocuments = [];
1186
- await sendProgress?.(1, 4, "Sampling documents");
1187
- try {
1188
- signal?.throwIfAborted();
1189
- sampleDocuments = await collection.aggregate([{ $sample: { size: sampleSize } }]).toArray();
1190
- } catch (sampleError) {
1191
- console.warn(
1192
- `$sample aggregation failed for ${collectionName}, falling back to sequential scan: ${sampleError}`
1193
- );
1194
- signal?.throwIfAborted();
1195
- sampleDocuments = await collection.find({}).limit(sampleSize).toArray();
1196
- }
1197
- await sendProgress?.(2, 4, "Getting indexes");
1800
+ const processedFilter = parseFilter(filter, objectIdMode);
1198
1801
  signal?.throwIfAborted();
1199
- const indexes = await collection.indexes();
1200
- const inferredSchema = inferSchemaFromSamples(sampleDocuments);
1201
- await sendProgress?.(3, 4, "Counting documents");
1202
- let documentCount = null;
1203
- try {
1204
- signal?.throwIfAborted();
1205
- documentCount = await Promise.race([
1206
- collection.countDocuments(),
1207
- new Promise(
1208
- (_, reject) => setTimeout(
1209
- () => reject(new Error("Count operation timed out")),
1210
- 5e3
1211
- )
1212
- )
1213
- ]);
1214
- } catch (countError) {
1215
- console.warn(
1216
- `Count operation failed or timed out for ${collectionName}: ${countError}`
1217
- );
1218
- try {
1219
- signal?.throwIfAborted();
1220
- const stats = await db.command({ collStats: collectionName });
1221
- documentCount = stats.count;
1222
- } catch {
1223
- documentCount = "unknown (count operation timed out)";
1802
+ const collections = await db.listCollections(processedFilter).toArray();
1803
+ const allResults = nameOnly ? collections.map((collection) => collection.name) : collections;
1804
+ const total = allResults.length;
1805
+ const paged = allResults.slice(skip, skip + limit);
1806
+ return formatResponse({
1807
+ results: paged,
1808
+ metadata: {
1809
+ total,
1810
+ returned: paged.length,
1811
+ skip,
1812
+ limit,
1813
+ hasMore: skip + paged.length < total
1224
1814
  }
1225
- }
1226
- await sendProgress?.(4, 4, "Done");
1227
- const schema = {
1228
- type: "collection",
1229
- name: collectionName,
1230
- fields: inferredSchema.fields,
1231
- indexes: indexes.map((idx) => ({
1232
- name: idx.name,
1233
- keys: idx.key
1234
- })),
1235
- documentCount,
1236
- sampleSize: sampleDocuments.length,
1237
- lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
1238
- };
1239
- return {
1240
- contents: [
1241
- {
1242
- uri: request.params.uri,
1243
- mimeType: "application/json",
1244
- text: JSON.stringify(schema, null, 2)
1245
- }
1246
- ]
1247
- };
1815
+ });
1248
1816
  } catch (error) {
1249
- if (error instanceof Error) {
1250
- throw new Error(
1251
- `Failed to read collection ${collectionName}: ${error.message}`
1252
- );
1253
- }
1254
- throw new Error(
1255
- `Failed to read collection ${collectionName}: Unknown error`
1256
- );
1817
+ return handleError(error, "list collections");
1257
1818
  }
1258
1819
  }
1259
- async function handleListResourcesRequest({
1820
+
1821
+ // src/schemas/completion.ts
1822
+ async function handleCompletionRequest({
1260
1823
  request,
1261
1824
  client,
1262
1825
  db,
1263
1826
  isReadOnlyMode,
1264
1827
  signal
1265
1828
  }) {
1829
+ const { ref, argument } = request.params;
1830
+ if (ref.type === "ref/prompt") {
1831
+ return handlePromptCompletion(
1832
+ client,
1833
+ db,
1834
+ isReadOnlyMode,
1835
+ ref.name,
1836
+ argument,
1837
+ signal
1838
+ );
1839
+ }
1840
+ if (ref.type === "ref/resource") {
1841
+ return handleResourceCompletion(
1842
+ client,
1843
+ db,
1844
+ isReadOnlyMode,
1845
+ ref.uri,
1846
+ argument,
1847
+ signal
1848
+ );
1849
+ }
1850
+ return emptyCompletionResult();
1851
+ }
1852
+ async function handlePromptCompletion(client, db, isReadOnlyMode, promptName, argument, signal) {
1853
+ if (!promptName) {
1854
+ return emptyCompletionResult();
1855
+ }
1856
+ if (argument.name === "collection") {
1857
+ return await completeCollectionNames(
1858
+ argument.value,
1859
+ db,
1860
+ isReadOnlyMode,
1861
+ signal
1862
+ );
1863
+ }
1864
+ return emptyCompletionResult();
1865
+ }
1866
+ async function handleResourceCompletion(client, db, isReadOnlyMode, promptName, argument, signal) {
1867
+ if (!promptName) {
1868
+ return emptyCompletionResult();
1869
+ }
1870
+ if (argument.name === "collection") {
1871
+ return await completeCollectionNames(
1872
+ argument.value,
1873
+ db,
1874
+ isReadOnlyMode,
1875
+ signal
1876
+ );
1877
+ }
1878
+ return emptyCompletionResult();
1879
+ }
1880
+ async function completeCollectionNames(partialValue, db, isReadOnlyMode, signal) {
1266
1881
  try {
1882
+ console.warn(
1883
+ `Completing collection names with partial value: ${partialValue}`
1884
+ );
1267
1885
  signal?.throwIfAborted();
1268
1886
  const collections = await db.listCollections().toArray();
1269
- const allResources = collections.map((collection) => ({
1270
- uri: `mongodb:///${encodeURIComponent(collection.name)}`,
1271
- mimeType: "application/json",
1272
- name: collection.name,
1273
- description: `MongoDB collection: ${collection.name}`
1274
- }));
1275
- const cursor = request.params?.cursor;
1276
- const { items, nextCursor } = paginate(allResources, cursor);
1887
+ const matchingCollections = collections.map(
1888
+ (c) => c.name
1889
+ ).filter(
1890
+ (name) => (
1891
+ // Filter out system collections
1892
+ !name.startsWith("system.") && // Match partial value
1893
+ name.toLowerCase().includes(partialValue.toLowerCase())
1894
+ )
1895
+ ).sort();
1896
+ console.warn(`Found ${matchingCollections.length} matching collections`);
1897
+ const PAGE_SIZE = 100;
1898
+ const { items: limitedResults, nextCursor } = paginate(
1899
+ matchingCollections,
1900
+ void 0,
1901
+ PAGE_SIZE
1902
+ );
1277
1903
  return {
1278
- resources: items,
1279
- ...nextCursor ? { nextCursor } : {}
1904
+ completion: {
1905
+ values: limitedResults,
1906
+ total: matchingCollections.length,
1907
+ hasMore: !!nextCursor
1908
+ }
1280
1909
  };
1281
1910
  } catch (error) {
1282
- if (error instanceof Error) {
1283
- throw new Error(`Failed to list collections: ${error.message}`);
1284
- }
1285
- throw new Error("Failed to list collections: Unknown error");
1911
+ console.error("Error completing collection names:", error);
1912
+ return emptyCompletionResult();
1286
1913
  }
1287
1914
  }
1915
+ function emptyCompletionResult() {
1916
+ return {
1917
+ completion: {
1918
+ values: [],
1919
+ total: 0,
1920
+ hasMore: false
1921
+ }
1922
+ };
1923
+ }
1288
1924
 
1289
- // src/schemas/templates.ts
1290
- async function handleListResourceTemplatesRequest({
1925
+ // src/schemas/ping.ts
1926
+ async function handlePingRequest({
1291
1927
  request,
1292
1928
  client,
1293
1929
  db,
1294
1930
  isReadOnlyMode,
1295
- signal
1296
- }) {
1297
- return {
1298
- resourceTemplates: [
1299
- {
1300
- name: "mongodb_query",
1301
- description: "Template for constructing MongoDB queries",
1302
- uriTemplate: "mongodb:///{collection}",
1303
- text: `To query MongoDB collections, you can use these operators:
1304
-
1305
- Filter operators:
1306
- - $eq: Matches values equal to a specified value
1307
- - $gt/$gte: Matches values greater than (or equal to) a specified value
1308
- - $lt/$lte: Matches values less than (or equal to) a specified value
1309
- - $in: Matches any of the values in an array
1310
- - $nin: Matches none of the values in an array
1311
- - $ne: Matches values not equal to a specified value
1312
- - $exists: Matches documents that have the specified field
1313
-
1314
- Example queries:
1315
- 1. Find documents where age > 21:
1316
- { "age": { "$gt": 21 } }
1317
-
1318
- 2. Find documents with specific status:
1319
- { "status": { "$in": ["active", "pending"] } }
1320
-
1321
- 3. Find documents with existing email:
1322
- { "email": { "$exists": true } }
1323
-
1324
- 4. Find documents with dates:
1325
- // Using ISO date string format
1326
- { "createdAt": { "$gt": "2023-01-01T00:00:00Z" } }
1327
- // Using ISODate syntax
1328
- { "createdAt": { "$gt": ISODate("2023-01-01T00:00:00Z") } }
1329
-
1330
- Use these patterns to construct MongoDB queries.`
1331
- }
1332
- ]
1333
- };
1931
+ signal
1932
+ }) {
1933
+ try {
1934
+ if (!client) {
1935
+ throw new Error("MongoDB connection is not available");
1936
+ }
1937
+ signal?.throwIfAborted();
1938
+ const pong = await db.command({ ping: 1 });
1939
+ if (pong.ok !== 1) {
1940
+ throw new Error(`MongoDB ping failed: ${pong.errmsg}`);
1941
+ }
1942
+ return {};
1943
+ } catch (error) {
1944
+ if (error instanceof Error) {
1945
+ throw new Error(`MongoDB ping failed: ${error.message}`);
1946
+ }
1947
+ throw new Error("MongoDB ping failed: Unknown error");
1948
+ }
1334
1949
  }
1335
1950
 
1336
- // src/schemas/tools.ts
1337
- async function handleListToolsRequest({
1951
+ // src/schemas/prompts.ts
1952
+ async function handleListPromptsRequest({
1338
1953
  request,
1339
1954
  client,
1340
1955
  db,
1341
1956
  isReadOnlyMode,
1342
- signal
1957
+ signal,
1958
+ connectionSelection = false
1343
1959
  }) {
1344
- return {
1345
- tools: [
1346
- {
1347
- name: "query",
1348
- description: "Find documents in one collection using a filter, projection, or sort. Use aggregate for grouping or joins. Request an execution plan to inspect the query.",
1349
- inputSchema: {
1350
- type: "object",
1351
- properties: {
1352
- collection: {
1353
- type: "string",
1354
- description: "Name of the collection to query"
1355
- },
1356
- filter: {
1357
- type: "object",
1358
- description: "MongoDB query filter. Supports date strings in ISO format ('2025-01-01T00:00:00Z') and ISODate('2025-01-01T00:00:00Z') notation"
1359
- },
1360
- projection: {
1361
- type: "object",
1362
- description: "Fields to include/exclude"
1363
- },
1364
- limit: {
1365
- type: "number",
1366
- description: "Maximum number of documents to return",
1367
- default: 10
1368
- },
1369
- skip: {
1370
- type: "number",
1371
- description: "Number of documents to skip before returning results",
1372
- default: 0
1373
- },
1374
- sort: {
1375
- type: "object",
1376
- description: "Sort order as a field-to-direction map, e.g. { orderDate: -1 } for descending or { name: 1 } for ascending"
1377
- },
1378
- explain: {
1379
- type: "string",
1380
- description: "Optional: Get query execution information",
1381
- enum: ["queryPlanner", "executionStats", "allPlansExecution"]
1382
- },
1383
- objectIdMode: {
1384
- type: "string",
1385
- description: "Control how 24-character hex strings are handled",
1386
- enum: ["auto", "none", "force"],
1387
- default: "auto"
1388
- }
1389
- },
1390
- required: ["collection"]
1391
- }
1392
- },
1393
- {
1394
- name: "aggregate",
1395
- description: "Run a MongoDB aggregation pipeline for grouping, joins, or multi-stage transformations. Request an execution plan to inspect the pipeline.",
1396
- inputSchema: {
1397
- type: "object",
1398
- properties: {
1399
- collection: {
1400
- type: "string",
1401
- description: "Name of the collection to aggregate"
1402
- },
1403
- pipeline: {
1404
- type: "array",
1405
- description: "Aggregation pipeline stages. Supports date strings in ISO format ('2025-01-01T00:00:00Z') and ISODate('2025-01-01T00:00:00Z') notation",
1406
- items: {
1407
- type: "object"
1408
- }
1409
- },
1410
- explain: {
1411
- type: "string",
1412
- description: "Optional: Get aggregation execution information (queryPlanner, executionStats, or allPlansExecution)",
1413
- enum: ["queryPlanner", "executionStats", "allPlansExecution"]
1414
- },
1415
- objectIdMode: {
1416
- type: "string",
1417
- description: "Control how 24-character hex strings are handled",
1418
- enum: ["auto", "none", "force"],
1419
- default: "auto"
1420
- }
1421
- },
1422
- required: ["collection", "pipeline"]
1423
- }
1424
- },
1425
- {
1426
- name: "update",
1427
- description: "Modify matching documents using operators such as $set, $unset, or $inc. Enable multi to update all matches or upsert to create a document when the filter matches none.",
1428
- inputSchema: {
1429
- type: "object",
1430
- properties: {
1431
- collection: {
1432
- type: "string",
1433
- description: "Name of the collection to update"
1434
- },
1435
- filter: {
1436
- type: "object",
1437
- description: "Filter to select documents to update. Supports date strings in ISO format ('2025-01-01T00:00:00Z') and ISODate('2025-01-01T00:00:00Z') notation"
1438
- },
1439
- update: {
1440
- type: "object",
1441
- description: "Update operations to apply ($set, $unset, $inc, etc.)"
1442
- },
1443
- upsert: {
1444
- type: "boolean",
1445
- description: "Create a new document if no documents match the filter"
1446
- },
1447
- multi: {
1448
- type: "boolean",
1449
- description: "Update multiple documents that match the filter"
1450
- },
1451
- objectIdMode: {
1452
- type: "string",
1453
- description: "Control how 24-character hex strings are handled",
1454
- enum: ["auto", "none", "force"],
1455
- default: "auto"
1456
- }
1457
- },
1458
- required: ["collection", "filter", "update"]
1459
- }
1460
- },
1461
- {
1462
- name: "serverInfo",
1463
- description: "Inspect MongoDB server metadata such as version and storage engine. Enable includeDebugInfo for additional diagnostics.",
1464
- inputSchema: {
1465
- type: "object",
1466
- properties: {
1467
- includeDebugInfo: {
1468
- type: "boolean",
1469
- description: "Include additional debug information about the server"
1470
- }
1471
- }
1472
- }
1473
- },
1474
- {
1475
- name: "insert",
1476
- description: "Insert new documents into a MongoDB collection. Use update to modify existing records.",
1477
- inputSchema: {
1478
- type: "object",
1479
- properties: {
1480
- collection: {
1481
- type: "string",
1482
- description: "Name of the collection to insert into"
1483
- },
1484
- documents: {
1485
- type: "array",
1486
- description: "Array of documents to insert",
1487
- items: { type: "object" }
1488
- },
1489
- ordered: {
1490
- type: "boolean",
1491
- description: "If true, perform ordered insert. If false, insert unordered"
1492
- },
1493
- writeConcern: {
1494
- type: "object",
1495
- description: "Write concern for the insert operation"
1496
- },
1497
- bypassDocumentValidation: {
1498
- type: "boolean",
1499
- description: "Allow insert to bypass schema validation"
1500
- },
1501
- objectIdMode: {
1502
- type: "string",
1503
- description: "Control how 24-character hex strings are handled",
1504
- enum: ["auto", "none", "force"],
1505
- default: "auto"
1506
- }
1507
- },
1508
- required: ["collection", "documents"]
1509
- }
1510
- },
1511
- {
1512
- name: "createIndex",
1513
- description: "Create indexes to improve query performance, enforce uniqueness, or expire documents with a TTL index.",
1514
- inputSchema: {
1515
- type: "object",
1516
- properties: {
1517
- collection: {
1518
- type: "string",
1519
- description: "Name of the collection to create indexes on"
1520
- },
1521
- indexes: {
1522
- type: "array",
1523
- description: "Array of index specifications",
1524
- items: {
1525
- type: "object",
1526
- properties: {
1527
- key: {
1528
- type: "object",
1529
- description: "Index key pattern, e.g. { field: 1 }"
1530
- },
1531
- name: {
1532
- type: "string",
1533
- description: "Optional: Name of the index"
1534
- },
1535
- unique: {
1536
- type: "boolean",
1537
- description: "Optional: Creates a unique index"
1538
- },
1539
- sparse: {
1540
- type: "boolean",
1541
- description: "Optional: Creates a sparse index"
1542
- },
1543
- background: {
1544
- type: "boolean",
1545
- description: "Optional: Builds index in background"
1546
- },
1547
- expireAfterSeconds: {
1548
- type: "number",
1549
- description: "TTL in seconds for documents"
1550
- },
1551
- partialFilterExpression: {
1552
- type: "object",
1553
- description: "Filter expression for partial indexes"
1554
- }
1555
- },
1556
- required: ["key"]
1557
- }
1558
- },
1559
- writeConcern: {
1560
- type: "object",
1561
- description: "Write concern for index creation"
1562
- },
1563
- commitQuorum: {
1564
- type: ["string", "number"],
1565
- description: "Replica-set commit quorum as a member count or a mode such as majority"
1566
- },
1567
- objectIdMode: {
1568
- type: "string",
1569
- description: "Control how 24-character hex strings are handled",
1570
- enum: ["auto", "none", "force"],
1571
- default: "auto"
1572
- }
1573
- },
1574
- required: ["collection", "indexes"]
1575
- }
1576
- },
1960
+ return {
1961
+ prompts: [
1577
1962
  {
1578
- name: "count",
1579
- description: "Count documents matching a filter in one collection. Use aggregate for counts grouped by field.",
1580
- inputSchema: {
1581
- type: "object",
1582
- properties: {
1583
- collection: { type: "string", description: "Collection name" },
1584
- query: { type: "object", description: "Query filter to count" },
1585
- limit: { type: "integer", description: "Max documents to count" },
1586
- skip: {
1587
- type: "integer",
1588
- description: "Docs to skip before counting"
1589
- },
1590
- hint: { type: "object", description: "Index hint" },
1591
- readConcern: { type: "object", description: "Read concern option" },
1592
- maxTimeMS: { type: "integer", description: "Max execution time" },
1593
- collation: {
1594
- type: "object",
1595
- description: "Collation rules for comparison"
1596
- },
1597
- objectIdMode: {
1598
- type: "string",
1599
- description: "Control how 24-character hex strings are handled",
1600
- enum: ["auto", "none", "force"],
1601
- default: "auto"
1602
- }
1963
+ name: "analyze_collection",
1964
+ description: "Analyze a MongoDB collection structure and contents",
1965
+ arguments: [
1966
+ {
1967
+ name: "collection",
1968
+ description: "Name of the collection to analyze",
1969
+ required: true
1603
1970
  },
1604
- required: ["collection"]
1605
- }
1606
- },
1607
- {
1608
- name: "listCollections",
1609
- description: "List collections in the connected database. Filter by name or other collection properties and request names only for a compact result.",
1610
- inputSchema: {
1611
- type: "object",
1612
- properties: {
1613
- nameOnly: {
1614
- type: "boolean",
1615
- description: "If true, return only collection names"
1616
- },
1617
- filter: {
1618
- type: "object",
1619
- description: "Filter for collections"
1620
- },
1621
- skip: {
1622
- type: "number",
1623
- description: "Number of collections to skip before returning results",
1624
- default: 0
1625
- },
1626
- limit: {
1627
- type: "number",
1628
- description: "Maximum number of collections to return",
1629
- default: 20
1971
+ ...connectionSelection ? [
1972
+ {
1973
+ name: "connectionId",
1974
+ description: "Configured MongoDB connection ID"
1630
1975
  },
1631
- objectIdMode: {
1632
- type: "string",
1633
- description: "Control how 24-character hex strings are handled",
1634
- enum: ["auto", "none", "force"],
1635
- default: "auto"
1976
+ {
1977
+ name: "database",
1978
+ description: "Allowed MongoDB database name"
1636
1979
  }
1980
+ ] : []
1981
+ ]
1982
+ }
1983
+ ]
1984
+ };
1985
+ }
1986
+ async function handleGetPromptRequest({
1987
+ request,
1988
+ client,
1989
+ db,
1990
+ isReadOnlyMode,
1991
+ signal,
1992
+ sendProgress,
1993
+ connectionId,
1994
+ showTarget = false
1995
+ }) {
1996
+ const { name, arguments: args = {} } = request.params;
1997
+ if (name !== "analyze_collection") {
1998
+ throw new Error("Unknown prompt");
1999
+ }
2000
+ const collectionName = args.collection;
2001
+ if (!collectionName) {
2002
+ throw new Error("Collection name is required");
2003
+ }
2004
+ try {
2005
+ const collection = db.collection(collectionName);
2006
+ if (collection.collectionName.startsWith("system.")) {
2007
+ throw new Error("Access to system collections is not allowed");
2008
+ }
2009
+ await sendProgress?.(1, 3, "Getting schema");
2010
+ signal?.throwIfAborted();
2011
+ const schemaSample = await collection.findOne({});
2012
+ await sendProgress?.(2, 3, "Getting stats");
2013
+ signal?.throwIfAborted();
2014
+ const stats = await collection.aggregate([{ $collStats: { count: {} } }]).toArray();
2015
+ await sendProgress?.(3, 3, "Fetching samples");
2016
+ signal?.throwIfAborted();
2017
+ const sampleDocs = await collection.find({}).limit(5).toArray();
2018
+ const documentCount = stats[0]?.count ?? "unknown";
2019
+ return {
2020
+ messages: [
2021
+ {
2022
+ role: "user",
2023
+ content: {
2024
+ type: "text",
2025
+ text: `Please analyze the following MongoDB collection:
2026
+ Collection: ${collectionName}${showTarget ? `
2027
+ Connection: ${connectionId}
2028
+ Database: ${db.databaseName}` : ""}
2029
+
2030
+ Schema:
2031
+ ${JSON.stringify(schemaSample, null, 2)}
2032
+
2033
+ Stats:
2034
+ Document count: ${documentCount}
2035
+
2036
+ Sample documents:
2037
+ ${JSON.stringify(sampleDocs, null, 2)}`
1637
2038
  }
1638
- }
1639
- },
1640
- {
1641
- name: "convertTime",
1642
- description: "Convert a Unix timestamp or date string to multiple formats (UTC ISO 8601, GMT, Unix seconds/milliseconds) and report the server's current time and timezone. Useful for building unambiguous date queries when the server and user are in different timezones. Omit 'input' to get the current time.",
1643
- inputSchema: {
1644
- type: "object",
1645
- properties: {
1646
- input: {
1647
- type: "string",
1648
- description: "A Unix timestamp (seconds or milliseconds, as a number or numeric string) or a date string (ISO 8601, with or without a timezone offset). Omit to use the current server time."
1649
- }
2039
+ },
2040
+ {
2041
+ role: "user",
2042
+ content: {
2043
+ type: "text",
2044
+ text: "Provide insights about the collection's structure, data types, and basic statistics."
1650
2045
  }
1651
2046
  }
2047
+ ]
2048
+ };
2049
+ } catch (error) {
2050
+ const msg = error instanceof Error ? error.message : "Unknown error";
2051
+ throw new Error(`Failed to analyze collection ${collectionName}: ${msg}`);
2052
+ }
2053
+ }
2054
+
2055
+ // src/schemas/templates.ts
2056
+ async function handleListResourceTemplatesRequest({
2057
+ request,
2058
+ client,
2059
+ db,
2060
+ isReadOnlyMode,
2061
+ signal
2062
+ }) {
2063
+ return {
2064
+ resourceTemplates: [
2065
+ {
2066
+ name: "mongodb_query",
2067
+ description: "Template for constructing MongoDB queries",
2068
+ uriTemplate: "mongodb:///{collection}",
2069
+ text: `To query MongoDB collections, you can use these operators:
2070
+
2071
+ Filter operators:
2072
+ - $eq: Matches values equal to a specified value
2073
+ - $gt/$gte: Matches values greater than (or equal to) a specified value
2074
+ - $lt/$lte: Matches values less than (or equal to) a specified value
2075
+ - $in: Matches any of the values in an array
2076
+ - $nin: Matches none of the values in an array
2077
+ - $ne: Matches values not equal to a specified value
2078
+ - $exists: Matches documents that have the specified field
2079
+
2080
+ Example queries:
2081
+ 1. Find documents where age > 21:
2082
+ { "age": { "$gt": 21 } }
2083
+
2084
+ 2. Find documents with specific status:
2085
+ { "status": { "$in": ["active", "pending"] } }
2086
+
2087
+ 3. Find documents with existing email:
2088
+ { "email": { "$exists": true } }
2089
+
2090
+ 4. Find documents with dates:
2091
+ // Using ISO date string format
2092
+ { "createdAt": { "$gt": "2023-01-01T00:00:00Z" } }
2093
+ // Using ISODate syntax
2094
+ { "createdAt": { "$gt": ISODate("2023-01-01T00:00:00Z") } }
2095
+
2096
+ Use these patterns to construct MongoDB queries.`
1652
2097
  }
1653
2098
  ]
1654
2099
  };
@@ -1662,12 +2107,13 @@ function createSendProgress(progressToken, notify) {
1662
2107
  params: { progressToken, progress, total, message }
1663
2108
  });
1664
2109
  }
1665
- function createServer(client, db, isReadOnlyMode = false, allowCrossDb = false, allowServerJs = false, options = {}) {
2110
+ function createServer(client, db, isReadOnlyMode = false, allowCrossDb = false, allowServerJs = false, options = {}, connections2, queryLimits) {
2111
+ const defaultTarget = { client, db, isReadOnlyMode };
1666
2112
  const server = new Server(
1667
2113
  {
1668
2114
  name: "mongodb",
1669
2115
  title: "MongoDB MCP Server",
1670
- version: "3.0.2",
2116
+ version: "3.2.0",
1671
2117
  description: "MCP server for MongoDB: query, aggregate, and manage collections with read-only mode, progress notifications, and cancellation",
1672
2118
  websiteUrl: "https://github.com/kiliczsh/mcp-mongo-server",
1673
2119
  ...options
@@ -1692,23 +2138,27 @@ function createServer(client, db, isReadOnlyMode = false, allowCrossDb = false,
1692
2138
  signal: ctx.mcpReq.signal
1693
2139
  })
1694
2140
  );
1695
- server.setRequestHandler(
1696
- "resources/list",
1697
- (request, ctx) => handleListResourcesRequest({
2141
+ server.setRequestHandler("resources/list", (request, ctx) => {
2142
+ if (connections2?.named) {
2143
+ return listNamedResources(
2144
+ connections2,
2145
+ request.params?.cursor,
2146
+ ctx.mcpReq.signal
2147
+ );
2148
+ }
2149
+ return handleListResourcesRequest({
1698
2150
  request,
1699
2151
  client,
1700
2152
  db,
1701
2153
  isReadOnlyMode,
1702
2154
  signal: ctx.mcpReq.signal
1703
- })
1704
- );
2155
+ });
2156
+ });
1705
2157
  server.setRequestHandler(
1706
2158
  "resources/read",
1707
- (request, ctx) => handleReadResourceRequest({
2159
+ async (request, ctx) => handleReadResourceRequest({
1708
2160
  request,
1709
- client,
1710
- db,
1711
- isReadOnlyMode,
2161
+ ...connections2 ? await connections2.resolve(resourceSelectors(request.params.uri)) : defaultTarget,
1712
2162
  signal: ctx.mcpReq.signal,
1713
2163
  sendProgress: createSendProgress(
1714
2164
  request.params._meta?.progressToken,
@@ -1723,6 +2173,9 @@ function createServer(client, db, isReadOnlyMode = false, allowCrossDb = false,
1723
2173
  client,
1724
2174
  db,
1725
2175
  isReadOnlyMode,
2176
+ connectionSelection: !!connections2,
2177
+ hasWritableConnection: connections2?.list().some(({ readOnly }) => !readOnly),
2178
+ queryLimits,
1726
2179
  signal: ctx.mcpReq.signal
1727
2180
  })
1728
2181
  );
@@ -1735,6 +2188,8 @@ function createServer(client, db, isReadOnlyMode = false, allowCrossDb = false,
1735
2188
  isReadOnlyMode,
1736
2189
  allowCrossDb,
1737
2190
  allowServerJs,
2191
+ connections: connections2,
2192
+ queryLimits,
1738
2193
  signal: ctx.mcpReq.signal
1739
2194
  })
1740
2195
  );
@@ -1745,16 +2200,16 @@ function createServer(client, db, isReadOnlyMode = false, allowCrossDb = false,
1745
2200
  client,
1746
2201
  db,
1747
2202
  isReadOnlyMode,
2203
+ connectionSelection: !!connections2,
1748
2204
  signal: ctx.mcpReq.signal
1749
2205
  })
1750
2206
  );
1751
2207
  server.setRequestHandler(
1752
2208
  "prompts/get",
1753
- (request, ctx) => handleGetPromptRequest({
2209
+ async (request, ctx) => handleGetPromptRequest({
1754
2210
  request,
1755
- client,
1756
- db,
1757
- isReadOnlyMode,
2211
+ ...connections2 ? await connections2.resolve(request.params.arguments) : defaultTarget,
2212
+ showTarget: !!connections2?.named,
1758
2213
  signal: ctx.mcpReq.signal,
1759
2214
  sendProgress: createSendProgress(
1760
2215
  request.params._meta?.progressToken,
@@ -1762,31 +2217,94 @@ function createServer(client, db, isReadOnlyMode = false, allowCrossDb = false,
1762
2217
  )
1763
2218
  })
1764
2219
  );
1765
- server.setRequestHandler(
1766
- "resources/templates/list",
1767
- (request, ctx) => handleListResourceTemplatesRequest({
2220
+ server.setRequestHandler("resources/templates/list", async (request, ctx) => {
2221
+ const result = await handleListResourceTemplatesRequest({
1768
2222
  request,
1769
2223
  client,
1770
2224
  db,
1771
2225
  isReadOnlyMode,
1772
2226
  signal: ctx.mcpReq.signal
1773
- })
1774
- );
1775
- server.setRequestHandler(
1776
- "completion/complete",
1777
- (request, ctx) => handleCompletionRequest({
2227
+ });
2228
+ if (connections2?.named) {
2229
+ result.resourceTemplates.push({
2230
+ ...result.resourceTemplates[0],
2231
+ name: "mongodb_connection_collection",
2232
+ uriTemplate: "mongodb:///{collection}{?connectionId,database}"
2233
+ });
2234
+ }
2235
+ return result;
2236
+ });
2237
+ server.setRequestHandler("completion/complete", async (request, ctx) => {
2238
+ const { ref, argument, context } = request.params;
2239
+ const selectors = {
2240
+ ...context?.arguments,
2241
+ ...ref.type === "ref/resource" ? resourceSelectors(ref.uri) : {}
2242
+ };
2243
+ if (connections2 && (argument.name === "connectionId" || argument.name === "database")) {
2244
+ const candidates = argument.name === "connectionId" ? connections2.list().map(({ connectionId }) => connectionId) : await connections2.listDatabases(selectors.connectionId);
2245
+ const matches = candidates.filter(
2246
+ (value) => value.startsWith(argument.value)
2247
+ );
2248
+ return {
2249
+ completion: {
2250
+ values: matches.slice(0, 100),
2251
+ total: matches.length,
2252
+ hasMore: matches.length > 100
2253
+ }
2254
+ };
2255
+ }
2256
+ return handleCompletionRequest({
1778
2257
  request,
1779
- client,
1780
- db,
1781
- isReadOnlyMode,
2258
+ ...connections2 ? await connections2.resolve(selectors) : defaultTarget,
1782
2259
  signal: ctx.mcpReq.signal
1783
- })
1784
- );
2260
+ });
2261
+ });
1785
2262
  return server;
1786
2263
  }
2264
+ async function listNamedResources(connections2, cursor, signal) {
2265
+ const prefix = `${connections2.cursorScope}.`;
2266
+ const offsetCursor = cursor?.slice(prefix.length);
2267
+ const offset = decodeCursor(offsetCursor);
2268
+ if (cursor !== void 0 && (!cursor.startsWith(prefix) || !Number.isSafeInteger(offset) || encodeCursor(offset) !== offsetCursor)) {
2269
+ throw new ProtocolError4(
2270
+ ProtocolErrorCode4.InvalidParams,
2271
+ "Invalid resource cursor for the current connection configuration"
2272
+ );
2273
+ }
2274
+ const resources = [];
2275
+ for (const { connectionId, defaultDatabase } of connections2.list()) {
2276
+ try {
2277
+ signal?.throwIfAborted();
2278
+ for (const database of await connections2.listDatabases(connectionId)) {
2279
+ const { db } = await connections2.resolve({ connectionId, database });
2280
+ const collections = await db.listCollections({}, { nameOnly: true }).toArray();
2281
+ for (const name of collections.map(({ name: name2 }) => name2).sort()) {
2282
+ if (name.startsWith("system.")) continue;
2283
+ const query = connectionId === "default" && database === defaultDatabase ? "" : `?${new URLSearchParams({ connectionId, database })}`;
2284
+ resources.push({
2285
+ uri: `mongodb:///${encodeURIComponent(name)}${query}`,
2286
+ name,
2287
+ mimeType: "application/json",
2288
+ description: `MongoDB collection: ${connectionId}/${database}/${name}`
2289
+ });
2290
+ }
2291
+ }
2292
+ } catch (error) {
2293
+ if (signal?.aborted) throw error;
2294
+ throw new Error(
2295
+ `Failed to list resources for MongoDB connection '${connectionId}'`
2296
+ );
2297
+ }
2298
+ }
2299
+ const { items, nextCursor } = paginate(resources, offsetCursor);
2300
+ return {
2301
+ resources: items,
2302
+ ...nextCursor ? { nextCursor: prefix + nextCursor } : {}
2303
+ };
2304
+ }
1787
2305
 
1788
2306
  // src/index.ts
1789
- var mongoClient = null;
2307
+ var connections;
1790
2308
  async function main() {
1791
2309
  const args = process.argv.slice(2);
1792
2310
  let connectionUrl = "";
@@ -1835,34 +2353,14 @@ async function main() {
1835
2353
  if (!connectionUrl) {
1836
2354
  connectionUrl = process.env.MCP_MONGODB_URI || "";
1837
2355
  }
1838
- if (!connectionUrl) {
1839
- console.error(
1840
- "Please provide a MongoDB connection URL via command-line argument or MCP_MONGODB_URI environment variable"
1841
- );
1842
- console.error(
1843
- "Usage: command <mongodb-url> [--read-only|-r] [--transport stdio|http] [--port 3001]"
1844
- );
1845
- console.error(
1846
- " or: MCP_MONGODB_URI=<mongodb-url> [MCP_MONGODB_READONLY=true] command"
1847
- );
1848
- process.exit(1);
1849
- }
1850
- if (!connectionUrl.startsWith("mongodb://") && !connectionUrl.startsWith("mongodb+srv://")) {
1851
- console.error(
1852
- "Invalid MongoDB connection URL. URL must start with 'mongodb://' or 'mongodb+srv://'"
1853
- );
1854
- process.exit(1);
1855
- }
1856
2356
  try {
1857
- const { client, db, isConnected, isReadOnlyMode } = await connectToMongoDB(
2357
+ const queryLimits = readQueryLimits(process.env);
2358
+ connections = new Connections(
1858
2359
  connectionUrl,
2360
+ process.env.MCP_MONGODB_CONNECTIONS,
1859
2361
  readOnlyMode
1860
2362
  );
1861
- mongoClient = client;
1862
- if (!isConnected || !client || !db) {
1863
- console.error("Failed to connect to MongoDB");
1864
- process.exit(1);
1865
- }
2363
+ const { client, db, isReadOnlyMode } = await connections.resolve();
1866
2364
  if (transportMode === "http") {
1867
2365
  await startHttpServer(
1868
2366
  client,
@@ -1870,6 +2368,8 @@ async function main() {
1870
2368
  isReadOnlyMode,
1871
2369
  allowCrossDb,
1872
2370
  allowServerJs,
2371
+ connections,
2372
+ queryLimits,
1873
2373
  port,
1874
2374
  allowedOrigins,
1875
2375
  jsonLimit,
@@ -1881,20 +2381,29 @@ async function main() {
1881
2381
  db,
1882
2382
  isReadOnlyMode,
1883
2383
  allowCrossDb,
1884
- allowServerJs
2384
+ allowServerJs,
2385
+ connections,
2386
+ queryLimits
1885
2387
  );
1886
2388
  }
1887
2389
  } catch (error) {
1888
- console.error("Failed to connect to MongoDB:", error);
1889
- if (mongoClient) {
1890
- await mongoClient.close();
1891
- }
2390
+ console.error("Failed to start MongoDB MCP server:", error);
2391
+ await connections?.close();
1892
2392
  process.exit(1);
1893
2393
  }
1894
2394
  }
1895
- async function startStdioServer(client, db, isReadOnlyMode, allowCrossDb, allowServerJs) {
2395
+ async function startStdioServer(client, db, isReadOnlyMode, allowCrossDb, allowServerJs, connections2, queryLimits) {
1896
2396
  serveStdio(
1897
- () => createServer(client, db, isReadOnlyMode, allowCrossDb, allowServerJs)
2397
+ () => createServer(
2398
+ client,
2399
+ db,
2400
+ isReadOnlyMode,
2401
+ allowCrossDb,
2402
+ allowServerJs,
2403
+ {},
2404
+ connections2,
2405
+ queryLimits
2406
+ )
1898
2407
  );
1899
2408
  console.warn("Server connected successfully via stdio");
1900
2409
  }
@@ -1921,7 +2430,7 @@ function bearerTokenValid(authorization, expected) {
1921
2430
  const wanted = Buffer.from(expected);
1922
2431
  return provided.length === wanted.length && timingSafeEqual(provided, wanted);
1923
2432
  }
1924
- async function startHttpServer(client, db, isReadOnlyMode, allowCrossDb, allowServerJs, port, allowedOrigins, jsonLimit, authToken) {
2433
+ async function startHttpServer(client, db, isReadOnlyMode, allowCrossDb, allowServerJs, connections2, queryLimits, port, allowedOrigins, jsonLimit, authToken) {
1925
2434
  const app = createMcpExpressApp({ host: "0.0.0.0", jsonLimit });
1926
2435
  app.use((req, res, next) => {
1927
2436
  const start = Date.now();
@@ -1972,7 +2481,16 @@ async function startHttpServer(client, db, isReadOnlyMode, allowCrossDb, allowSe
1972
2481
  });
1973
2482
  }
1974
2483
  const mcpHandler = createMcpHandler(
1975
- (_ctx) => createServer(client, db, isReadOnlyMode, allowCrossDb, allowServerJs)
2484
+ (_ctx) => createServer(
2485
+ client,
2486
+ db,
2487
+ isReadOnlyMode,
2488
+ allowCrossDb,
2489
+ allowServerJs,
2490
+ {},
2491
+ connections2,
2492
+ queryLimits
2493
+ )
1976
2494
  );
1977
2495
  const nodeHandler = toNodeHandler(mcpHandler, {
1978
2496
  onerror: (error) => console.error("Error handling MCP request:", error)
@@ -1999,15 +2517,11 @@ async function startHttpServer(client, db, isReadOnlyMode, allowCrossDb, allowSe
1999
2517
  });
2000
2518
  }
2001
2519
  process.on("SIGINT", async () => {
2002
- if (mongoClient) {
2003
- await mongoClient.close();
2004
- }
2520
+ await connections?.close();
2005
2521
  process.exit(0);
2006
2522
  });
2007
2523
  process.on("SIGTERM", async () => {
2008
- if (mongoClient) {
2009
- await mongoClient.close();
2010
- }
2524
+ await connections?.close();
2011
2525
  process.exit(0);
2012
2526
  });
2013
2527
  main().catch((error) => {