mcp-mongo-server 3.1.0 → 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.
- package/README.md +5 -1
- package/build/index.js +1764 -1579
- package/package.json +2 -2
package/build/index.js
CHANGED
|
@@ -176,6 +176,63 @@ function resourceSelectors(uri) {
|
|
|
176
176
|
return selectors;
|
|
177
177
|
}
|
|
178
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
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
179
236
|
// src/server.ts
|
|
180
237
|
import {
|
|
181
238
|
ProtocolError as ProtocolError4,
|
|
@@ -184,1742 +241,1862 @@ import {
|
|
|
184
241
|
} from "@modelcontextprotocol/server";
|
|
185
242
|
|
|
186
243
|
// src/schemas/call.ts
|
|
187
|
-
import { ProtocolError as
|
|
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";
|
|
188
252
|
import { ObjectId } from "mongodb";
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
"
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
"$function",
|
|
202
|
-
"$accumulator",
|
|
203
|
-
"$where"
|
|
204
|
-
]);
|
|
205
|
-
var READONLY_FORBIDDEN_AGG_OPERATORS = /* @__PURE__ */ new Set([
|
|
206
|
-
...WRITE_AGG_STAGES,
|
|
207
|
-
...SERVER_JS_AGG_OPERATORS
|
|
208
|
-
]);
|
|
209
|
-
function findAggOperator(value, operators, depth = 0) {
|
|
210
|
-
if (depth > MAX_OBJECT_DEPTH) {
|
|
211
|
-
throw new Error(
|
|
212
|
-
`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")
|
|
213
265
|
);
|
|
266
|
+
return typeof data.offset === "number" && data.offset >= 0 ? data.offset : 0;
|
|
267
|
+
} catch {
|
|
268
|
+
return 0;
|
|
214
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";
|
|
215
285
|
if (Array.isArray(value)) {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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]}>`;
|
|
219
290
|
}
|
|
220
|
-
return
|
|
291
|
+
return "Array<mixed>";
|
|
221
292
|
}
|
|
222
|
-
if (
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
+
}
|
|
226
321
|
}
|
|
227
|
-
const found = findAggOperator(nested, operators, depth + 1);
|
|
228
|
-
if (found) return found;
|
|
229
322
|
}
|
|
230
323
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
);
|
|
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
|
+
}
|
|
239
331
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
if (
|
|
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
|
+
}
|
|
246
340
|
}
|
|
247
|
-
return null;
|
|
248
|
-
};
|
|
249
|
-
for (const stage of pipeline) {
|
|
250
|
-
if (!stage || typeof stage !== "object") continue;
|
|
251
|
-
const s = stage;
|
|
252
|
-
const out = foreignDb(s.$out);
|
|
253
|
-
if (out) return out;
|
|
254
|
-
const merge = s.$merge;
|
|
255
|
-
const mergeInto = foreignDb(merge?.into ?? s.$merge);
|
|
256
|
-
if (mergeInto) return mergeInto;
|
|
257
|
-
const lookup = s.$lookup;
|
|
258
|
-
const lookupFrom = foreignDb(lookup?.from);
|
|
259
|
-
if (lookupFrom) return lookupFrom;
|
|
260
341
|
}
|
|
261
|
-
|
|
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 };
|
|
262
367
|
}
|
|
263
|
-
async function
|
|
368
|
+
async function handleReadResourceRequest({
|
|
264
369
|
request,
|
|
265
370
|
client,
|
|
266
371
|
db,
|
|
267
372
|
isReadOnlyMode,
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
connections: connections2,
|
|
271
|
-
signal
|
|
373
|
+
signal,
|
|
374
|
+
sendProgress
|
|
272
375
|
}) {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
376
|
+
let collectionName;
|
|
377
|
+
try {
|
|
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
|
+
);
|
|
280
384
|
}
|
|
385
|
+
collectionName = decodeURIComponent(url.pathname.replace(/^\//, ""));
|
|
386
|
+
} catch (error) {
|
|
387
|
+
if (error instanceof ProtocolError2) throw error;
|
|
388
|
+
throw new ProtocolError2(
|
|
389
|
+
ProtocolErrorCode2.InvalidParams,
|
|
390
|
+
`Invalid resource URI: ${request.params.uri}`
|
|
391
|
+
);
|
|
281
392
|
}
|
|
282
|
-
if (
|
|
283
|
-
|
|
393
|
+
if (!collectionName) {
|
|
394
|
+
throw new ProtocolError2(
|
|
395
|
+
ProtocolErrorCode2.InvalidParams,
|
|
396
|
+
"Resource URI is missing a collection name"
|
|
397
|
+
);
|
|
284
398
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
399
|
+
if (collectionName.toLowerCase().startsWith("system.")) {
|
|
400
|
+
throw new ProtocolError2(
|
|
401
|
+
ProtocolErrorCode2.InvalidParams,
|
|
402
|
+
"Access to system collections is not allowed"
|
|
403
|
+
);
|
|
288
404
|
}
|
|
289
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) {
|
|
290
422
|
signal?.throwIfAborted();
|
|
291
|
-
if (
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
return formatResponse({
|
|
296
|
-
databases: await connections2.listDatabases(args.connectionId)
|
|
297
|
-
});
|
|
423
|
+
if (error instanceof Error) {
|
|
424
|
+
throw new Error(
|
|
425
|
+
`Failed to read collection ${collectionName}: ${error.message}`
|
|
426
|
+
);
|
|
298
427
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
db = target.db;
|
|
302
|
-
isReadOnlyMode = target.isReadOnlyMode;
|
|
303
|
-
if (connections2.named && operation === "aggregate" && Array.isArray(args.pipeline)) {
|
|
304
|
-
const allowed = connections2.allowedDatabases(target.connectionId);
|
|
305
|
-
checkNamedAggregationDatabases(
|
|
306
|
-
args.pipeline,
|
|
307
|
-
db.databaseName,
|
|
308
|
-
allowCrossDb ? allowed : [db.databaseName]
|
|
309
|
-
);
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
checkReadOnlyMode(operation, isReadOnlyMode);
|
|
313
|
-
let collection = null;
|
|
314
|
-
if (COLLECTION_OPERATIONS.includes(operation)) {
|
|
315
|
-
const collectionName = args.collection;
|
|
316
|
-
if (!collectionName) {
|
|
317
|
-
throw new Error(
|
|
318
|
-
`Collection name is required for '${operation}' operation`
|
|
319
|
-
);
|
|
320
|
-
}
|
|
321
|
-
collection = db.collection(collectionName);
|
|
322
|
-
validateCollection(collection);
|
|
323
|
-
}
|
|
324
|
-
signal?.throwIfAborted();
|
|
325
|
-
return await executeOperation(
|
|
326
|
-
operation,
|
|
327
|
-
collection,
|
|
328
|
-
db,
|
|
329
|
-
isReadOnlyMode,
|
|
330
|
-
allowCrossDb,
|
|
331
|
-
allowServerJs,
|
|
332
|
-
args,
|
|
333
|
-
objectIdMode,
|
|
334
|
-
signal
|
|
335
|
-
);
|
|
336
|
-
} catch (error) {
|
|
337
|
-
if (error instanceof ProtocolError2 || signal?.aborted) {
|
|
338
|
-
throw error;
|
|
339
|
-
}
|
|
340
|
-
return toolExecutionError(
|
|
341
|
-
error instanceof Error ? error.message : "Unknown error"
|
|
428
|
+
throw new Error(
|
|
429
|
+
`Failed to read collection ${collectionName}: Unknown error`
|
|
342
430
|
);
|
|
343
431
|
}
|
|
344
432
|
}
|
|
345
|
-
function
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
continue;
|
|
357
|
-
if (typeof reference.db !== "string" || !allowed.includes(reference.db)) {
|
|
358
|
-
throw new Error(
|
|
359
|
-
`Aggregation references a database outside the allowed scope of '${database}'`
|
|
360
|
-
);
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
if (Array.isArray(value.pipeline))
|
|
364
|
-
checkNamedAggregationDatabases(
|
|
365
|
-
value.pipeline,
|
|
366
|
-
database,
|
|
367
|
-
allowed,
|
|
368
|
-
depth + 1
|
|
369
|
-
);
|
|
370
|
-
}
|
|
371
|
-
if ($facet && typeof $facet === "object") {
|
|
372
|
-
for (const nested of Object.values($facet)) {
|
|
373
|
-
if (Array.isArray(nested))
|
|
374
|
-
checkNamedAggregationDatabases(nested, database, allowed, depth + 1);
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
async function executeOperation(operation, collection, db, isReadOnlyMode, allowCrossDb, allowServerJs, args, objectIdMode, signal) {
|
|
380
|
-
switch (operation) {
|
|
381
|
-
case "query":
|
|
382
|
-
return handleQuery(
|
|
383
|
-
collection,
|
|
384
|
-
args,
|
|
385
|
-
objectIdMode,
|
|
386
|
-
allowServerJs && !isReadOnlyMode,
|
|
387
|
-
signal
|
|
388
|
-
);
|
|
389
|
-
case "aggregate":
|
|
390
|
-
return handleAggregate(
|
|
391
|
-
collection,
|
|
392
|
-
args,
|
|
393
|
-
objectIdMode,
|
|
394
|
-
isReadOnlyMode,
|
|
395
|
-
allowCrossDb,
|
|
396
|
-
allowServerJs,
|
|
397
|
-
signal
|
|
398
|
-
);
|
|
399
|
-
case "update":
|
|
400
|
-
return handleUpdate(
|
|
401
|
-
collection,
|
|
402
|
-
args,
|
|
403
|
-
objectIdMode,
|
|
404
|
-
allowServerJs,
|
|
405
|
-
signal
|
|
406
|
-
);
|
|
407
|
-
case "serverInfo":
|
|
408
|
-
return handleServerInfo(db, isReadOnlyMode, args, signal);
|
|
409
|
-
case "insert":
|
|
410
|
-
return handleInsert(collection, args, objectIdMode, signal);
|
|
411
|
-
case "createIndex":
|
|
412
|
-
return handleCreateIndex(collection, args, objectIdMode, signal);
|
|
413
|
-
case "count":
|
|
414
|
-
return handleCount(
|
|
415
|
-
collection,
|
|
416
|
-
args,
|
|
417
|
-
objectIdMode,
|
|
418
|
-
allowServerJs && !isReadOnlyMode,
|
|
419
|
-
signal
|
|
420
|
-
);
|
|
421
|
-
case "listCollections":
|
|
422
|
-
return handleListCollections(db, args, objectIdMode, signal);
|
|
423
|
-
case "convertTime":
|
|
424
|
-
return handleConvertTime(args);
|
|
425
|
-
default:
|
|
426
|
-
throw new ProtocolError2(
|
|
427
|
-
ProtocolErrorCode2.InvalidParams,
|
|
428
|
-
`Unknown tool: ${operation}`
|
|
429
|
-
);
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
function validateOperation(operation) {
|
|
433
|
-
const validOperations = [
|
|
434
|
-
"query",
|
|
435
|
-
"aggregate",
|
|
436
|
-
"update",
|
|
437
|
-
"serverInfo",
|
|
438
|
-
"insert",
|
|
439
|
-
"createIndex",
|
|
440
|
-
"count",
|
|
441
|
-
"listCollections",
|
|
442
|
-
"convertTime"
|
|
443
|
-
];
|
|
444
|
-
if (!validOperations.includes(operation)) {
|
|
445
|
-
throw new ProtocolError2(
|
|
446
|
-
ProtocolErrorCode2.InvalidParams,
|
|
447
|
-
`Unknown tool: ${operation}`
|
|
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}`
|
|
448
444
|
);
|
|
445
|
+
signal?.throwIfAborted();
|
|
446
|
+
sampleDocuments = await collection.find({}, { signal }).limit(sampleSize).toArray();
|
|
449
447
|
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
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}`
|
|
463
467
|
);
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
function parseSort(sort) {
|
|
467
|
-
if (!sort) return null;
|
|
468
|
-
if (typeof sort !== "object" || sort === null || Array.isArray(sort)) {
|
|
469
|
-
return null;
|
|
470
|
-
}
|
|
471
|
-
const validSort = {};
|
|
472
|
-
for (const [key, value] of Object.entries(sort)) {
|
|
473
|
-
if (typeof value === "number" && (value === 1 || value === -1)) {
|
|
474
|
-
validSort[key] = value;
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
return Object.keys(validSort).length > 0 ? validSort : null;
|
|
478
|
-
}
|
|
479
|
-
function parseFilter(filter, objectIdMode = "auto") {
|
|
480
|
-
if (!filter) {
|
|
481
|
-
return {};
|
|
482
|
-
}
|
|
483
|
-
if (typeof filter === "string") {
|
|
484
468
|
try {
|
|
485
|
-
|
|
469
|
+
signal?.throwIfAborted();
|
|
470
|
+
const stats = await db.command(
|
|
471
|
+
{ collStats: collectionName },
|
|
472
|
+
{ timeoutMS: 5e3, signal }
|
|
473
|
+
);
|
|
474
|
+
documentCount = stats.count;
|
|
486
475
|
} catch {
|
|
487
|
-
|
|
476
|
+
signal?.throwIfAborted();
|
|
477
|
+
documentCount = "unknown (count operation timed out)";
|
|
488
478
|
}
|
|
489
479
|
}
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
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
|
+
};
|
|
501
493
|
}
|
|
502
|
-
function
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
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}`);
|
|
519
|
+
}
|
|
520
|
+
throw new Error("Failed to list collections: Unknown error");
|
|
507
521
|
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
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"]
|
|
537
588
|
}
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
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
|
+
}
|
|
571
668
|
}
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
} else {
|
|
575
|
-
result[key] = processObjectIdInFilter(
|
|
576
|
-
value,
|
|
577
|
-
objectIdMode,
|
|
578
|
-
depth + 1
|
|
579
|
-
);
|
|
580
|
-
}
|
|
581
|
-
} else {
|
|
582
|
-
result[key] = value;
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
return result;
|
|
586
|
-
}
|
|
587
|
-
function isObjectIdString(str) {
|
|
588
|
-
return /^[0-9a-fA-F]{24}$/.test(str);
|
|
589
|
-
}
|
|
590
|
-
function isISODateString(str) {
|
|
591
|
-
return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,3})?(Z|[+-]\d{2}:\d{2})$/.test(
|
|
592
|
-
str
|
|
593
|
-
);
|
|
594
|
-
}
|
|
595
|
-
function formatResponse(data) {
|
|
596
|
-
return {
|
|
597
|
-
content: [
|
|
669
|
+
}
|
|
670
|
+
},
|
|
598
671
|
{
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
const explainResult = await collection.find(queryFilter, {
|
|
634
|
-
projection,
|
|
635
|
-
limit,
|
|
636
|
-
skip,
|
|
637
|
-
sort
|
|
638
|
-
}).explain(explain);
|
|
639
|
-
return formatResponse(explainResult);
|
|
640
|
-
}
|
|
641
|
-
signal?.throwIfAborted();
|
|
642
|
-
const [total, results] = await Promise.all([
|
|
643
|
-
collection.countDocuments(queryFilter),
|
|
644
|
-
collection.find(queryFilter, {
|
|
645
|
-
projection,
|
|
646
|
-
limit,
|
|
647
|
-
skip,
|
|
648
|
-
sort
|
|
649
|
-
}).toArray()
|
|
650
|
-
]);
|
|
651
|
-
return formatResponse({
|
|
652
|
-
results,
|
|
653
|
-
metadata: {
|
|
654
|
-
total,
|
|
655
|
-
returned: results.length,
|
|
656
|
-
skip,
|
|
657
|
-
limit,
|
|
658
|
-
hasMore: skip + results.length < total
|
|
659
|
-
}
|
|
660
|
-
});
|
|
661
|
-
} catch (error) {
|
|
662
|
-
return handleError(error, "query", collection.collectionName);
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
async function handleAggregate(collection, args, objectIdMode = "auto", isReadOnlyMode = false, allowCrossDb = false, allowServerJs = false, signal) {
|
|
666
|
-
if (!collection) {
|
|
667
|
-
throw new Error("Collection is required for aggregate operation");
|
|
668
|
-
}
|
|
669
|
-
const { pipeline, explain } = args;
|
|
670
|
-
if (!Array.isArray(pipeline)) {
|
|
671
|
-
throw new Error("Pipeline must be an array");
|
|
672
|
-
}
|
|
673
|
-
if (isReadOnlyMode) {
|
|
674
|
-
const forbidden = findAggOperator(
|
|
675
|
-
pipeline,
|
|
676
|
-
READONLY_FORBIDDEN_AGG_OPERATORS
|
|
677
|
-
);
|
|
678
|
-
if (forbidden) {
|
|
679
|
-
throw new Error(
|
|
680
|
-
`ReadonlyError: Aggregation operator '${forbidden}' is not allowed in read-only mode`
|
|
681
|
-
);
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
if (!allowServerJs) {
|
|
685
|
-
assertNoServerJs(pipeline);
|
|
686
|
-
}
|
|
687
|
-
if (!allowCrossDb) {
|
|
688
|
-
const foreignDb = findCrossDbTarget(pipeline, collection.dbName);
|
|
689
|
-
if (foreignDb) {
|
|
690
|
-
throw new Error(
|
|
691
|
-
`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.`
|
|
692
|
-
);
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
const processedPipeline = pipeline.map((stage) => {
|
|
696
|
-
if (typeof stage === "object" && stage !== null) {
|
|
697
|
-
return processObjectIdInFilter(
|
|
698
|
-
stage,
|
|
699
|
-
objectIdMode
|
|
700
|
-
);
|
|
701
|
-
}
|
|
702
|
-
return stage;
|
|
703
|
-
});
|
|
704
|
-
try {
|
|
705
|
-
if (explain) {
|
|
706
|
-
signal?.throwIfAborted();
|
|
707
|
-
const explainResult = await collection.aggregate(processedPipeline, {
|
|
708
|
-
explain: {
|
|
709
|
-
verbosity: explain
|
|
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"]
|
|
710
706
|
}
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
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)
|
|
758
879
|
);
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
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
|
+
}
|
|
763
894
|
}
|
|
764
|
-
|
|
765
|
-
const
|
|
766
|
-
|
|
767
|
-
|
|
895
|
+
if (connectionSelection) {
|
|
896
|
+
const connectionId = {
|
|
897
|
+
type: "string",
|
|
898
|
+
description: "Configured connection ID. Defaults to default."
|
|
768
899
|
};
|
|
769
|
-
const
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
queryFilter,
|
|
773
|
-
processedUpdate,
|
|
774
|
-
options
|
|
775
|
-
);
|
|
776
|
-
return formatResponse({
|
|
777
|
-
matchedCount: result.matchedCount,
|
|
778
|
-
modifiedCount: result.modifiedCount,
|
|
779
|
-
upsertedCount: result.upsertedCount,
|
|
780
|
-
upsertedId: result.upsertedId
|
|
781
|
-
});
|
|
782
|
-
} catch (error) {
|
|
783
|
-
return handleError(error, "update", collection.collectionName);
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
async function handleServerInfo(db, isReadOnlyMode, args, signal) {
|
|
787
|
-
const { includeDebugInfo } = args;
|
|
788
|
-
try {
|
|
789
|
-
signal?.throwIfAborted();
|
|
790
|
-
const buildInfo = await db.command({ buildInfo: 1 });
|
|
791
|
-
const serverInfo = {
|
|
792
|
-
version: buildInfo.version,
|
|
793
|
-
gitVersion: buildInfo.gitVersion,
|
|
794
|
-
storageEngines: buildInfo.storageEngines,
|
|
795
|
-
maxBsonObjectSize: buildInfo.maxBsonObjectSize,
|
|
796
|
-
bits: buildInfo.bits,
|
|
797
|
-
ok: buildInfo.ok,
|
|
798
|
-
connectionInfo: {
|
|
799
|
-
readOnlyMode: isReadOnlyMode
|
|
800
|
-
}
|
|
900
|
+
const database = {
|
|
901
|
+
type: "string",
|
|
902
|
+
description: "Allowed database name. Defaults to the database in the connection URI."
|
|
801
903
|
};
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
serverInfo.openssl = buildInfo.openssl;
|
|
811
|
-
serverInfo.buildEnvironment = buildInfo.buildEnvironment;
|
|
812
|
-
serverInfo.status = {
|
|
813
|
-
host: serverStatus.host,
|
|
814
|
-
version: serverStatus.version,
|
|
815
|
-
process: serverStatus.process,
|
|
816
|
-
pid: serverStatus.pid,
|
|
817
|
-
uptime: serverStatus.uptime,
|
|
818
|
-
uptimeMillis: serverStatus.uptimeMillis,
|
|
819
|
-
uptimeEstimate: serverStatus.uptimeEstimate,
|
|
820
|
-
localTime: serverStatus.localTime,
|
|
821
|
-
connections: serverStatus.connections,
|
|
822
|
-
network: serverStatus.network,
|
|
823
|
-
memory: serverStatus.mem,
|
|
824
|
-
storageEngine: serverStatus.storageEngine,
|
|
825
|
-
security: serverStatus.security
|
|
826
|
-
};
|
|
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
|
+
}
|
|
827
912
|
}
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
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
|
+
);
|
|
831
925
|
}
|
|
926
|
+
return result;
|
|
832
927
|
}
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
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
|
+
);
|
|
843
956
|
}
|
|
844
|
-
if (
|
|
845
|
-
(
|
|
846
|
-
|
|
847
|
-
|
|
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;
|
|
848
963
|
}
|
|
849
|
-
|
|
850
|
-
(
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
writeConcern,
|
|
857
|
-
bypassDocumentValidation
|
|
858
|
-
};
|
|
859
|
-
signal?.throwIfAborted();
|
|
860
|
-
const result = await collection.insertMany(
|
|
861
|
-
processedDocuments,
|
|
862
|
-
options
|
|
863
|
-
);
|
|
864
|
-
return formatResponse({
|
|
865
|
-
acknowledged: result.acknowledged,
|
|
866
|
-
insertedCount: result.insertedCount,
|
|
867
|
-
insertedIds: result.insertedIds
|
|
868
|
-
});
|
|
869
|
-
} catch (error) {
|
|
870
|
-
if (error instanceof Error && error.name === "BulkWriteError") {
|
|
871
|
-
const bulkError = error;
|
|
872
|
-
return formatResponse({
|
|
873
|
-
error: "Bulk write error occurred",
|
|
874
|
-
writeErrors: bulkError.writeErrors,
|
|
875
|
-
insertedCount: bulkError.result?.nInserted || 0,
|
|
876
|
-
failedCount: bulkError.result?.nFailedInserts || 0
|
|
877
|
-
});
|
|
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;
|
|
878
971
|
}
|
|
879
|
-
return handleError(error, "insert", collection.collectionName);
|
|
880
972
|
}
|
|
973
|
+
return null;
|
|
881
974
|
}
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
}
|
|
886
|
-
const { indexes, commitQuorum, writeConcern } = args;
|
|
887
|
-
if (!Array.isArray(indexes) || indexes.length === 0) {
|
|
888
|
-
throw new Error("Indexes must be a non-empty array");
|
|
889
|
-
}
|
|
890
|
-
if (writeConcern && (typeof writeConcern !== "object" || Array.isArray(writeConcern))) {
|
|
975
|
+
function assertNoServerJs(value) {
|
|
976
|
+
const operator = findAggOperator(value, SERVER_JS_AGG_OPERATORS);
|
|
977
|
+
if (operator) {
|
|
891
978
|
throw new Error(
|
|
892
|
-
|
|
979
|
+
`Server-side JavaScript operator '${operator}' is disabled by the current server policy.`
|
|
893
980
|
);
|
|
894
981
|
}
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
return
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
const
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
);
|
|
916
|
-
return formatResponse({
|
|
917
|
-
acknowledged: true,
|
|
918
|
-
createdIndexes,
|
|
919
|
-
indexCount: createdIndexes.length
|
|
920
|
-
});
|
|
921
|
-
} catch (error) {
|
|
922
|
-
return handleError(error, "create indexes", collection.collectionName);
|
|
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;
|
|
923
1002
|
}
|
|
1003
|
+
return null;
|
|
924
1004
|
}
|
|
925
|
-
async function
|
|
926
|
-
|
|
927
|
-
|
|
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
|
+
}
|
|
928
1024
|
}
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
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);
|
|
933
1031
|
}
|
|
934
1032
|
try {
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
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
|
+
);
|
|
946
1063
|
}
|
|
1064
|
+
collection = db.collection(collectionName);
|
|
1065
|
+
validateCollection(collection);
|
|
947
1066
|
}
|
|
948
1067
|
signal?.throwIfAborted();
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
let date;
|
|
961
|
-
if (input === void 0 || input === null || input === "") {
|
|
962
|
-
date = /* @__PURE__ */ new Date();
|
|
963
|
-
} else if (typeof input === "number") {
|
|
964
|
-
date = new Date(Math.abs(input) < 1e12 ? input * 1e3 : input);
|
|
965
|
-
} else if (typeof input === "string") {
|
|
966
|
-
const trimmed = input.trim();
|
|
967
|
-
if (/^-?\d+$/.test(trimmed)) {
|
|
968
|
-
const n = Number(trimmed);
|
|
969
|
-
date = new Date(Math.abs(n) < 1e12 ? n * 1e3 : n);
|
|
970
|
-
} else {
|
|
971
|
-
date = new Date(trimmed);
|
|
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
|
+
}
|
|
972
1079
|
}
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
1080
|
+
return await executeOperation(
|
|
1081
|
+
operation,
|
|
1082
|
+
collection,
|
|
1083
|
+
db,
|
|
1084
|
+
isReadOnlyMode,
|
|
1085
|
+
allowCrossDb,
|
|
1086
|
+
allowServerJs,
|
|
1087
|
+
args,
|
|
1088
|
+
objectIdMode,
|
|
1089
|
+
queryLimits,
|
|
1090
|
+
signal
|
|
976
1091
|
);
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
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"
|
|
981
1098
|
);
|
|
982
1099
|
}
|
|
983
|
-
const offsetMinutes = -date.getTimezoneOffset();
|
|
984
|
-
const sign = offsetMinutes >= 0 ? "+" : "-";
|
|
985
|
-
const absMinutes = Math.abs(offsetMinutes);
|
|
986
|
-
const offsetLabel = `${sign}${String(Math.floor(absMinutes / 60)).padStart(
|
|
987
|
-
2,
|
|
988
|
-
"0"
|
|
989
|
-
)}:${String(absMinutes % 60).padStart(2, "0")}`;
|
|
990
|
-
return formatResponse({
|
|
991
|
-
iso: date.toISOString(),
|
|
992
|
-
utc: date.toUTCString(),
|
|
993
|
-
unixSeconds: Math.floor(date.getTime() / 1e3),
|
|
994
|
-
unixMillis: date.getTime(),
|
|
995
|
-
serverLocal: date.toString(),
|
|
996
|
-
serverTimeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
997
|
-
serverUtcOffset: offsetLabel
|
|
998
|
-
});
|
|
999
1100
|
}
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
const
|
|
1004
|
-
|
|
1005
|
-
const
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
limit,
|
|
1018
|
-
hasMore: skip + paged.length < total
|
|
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
|
+
}
|
|
1019
1118
|
}
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
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
|
+
);
|
|
1023
1188
|
}
|
|
1024
1189
|
}
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
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}`
|
|
1037
1208
|
);
|
|
1038
|
-
return typeof data.offset === "number" && data.offset >= 0 ? data.offset : 0;
|
|
1039
|
-
} catch {
|
|
1040
|
-
return 0;
|
|
1041
1209
|
}
|
|
1042
1210
|
}
|
|
1043
|
-
function
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
const nextOffset = offset + pageSize;
|
|
1047
|
-
const nextCursor = nextOffset < items.length ? encodeCursor(nextOffset) : void 0;
|
|
1048
|
-
return { items: page, nextCursor };
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
// src/schemas/completion.ts
|
|
1052
|
-
async function handleCompletionRequest({
|
|
1053
|
-
request,
|
|
1054
|
-
client,
|
|
1055
|
-
db,
|
|
1056
|
-
isReadOnlyMode,
|
|
1057
|
-
signal
|
|
1058
|
-
}) {
|
|
1059
|
-
const { ref, argument } = request.params;
|
|
1060
|
-
if (ref.type === "ref/prompt") {
|
|
1061
|
-
return handlePromptCompletion(
|
|
1062
|
-
client,
|
|
1063
|
-
db,
|
|
1064
|
-
isReadOnlyMode,
|
|
1065
|
-
ref.name,
|
|
1066
|
-
argument,
|
|
1067
|
-
signal
|
|
1068
|
-
);
|
|
1211
|
+
function validateCollection(collection) {
|
|
1212
|
+
if (!collection.collectionName) {
|
|
1213
|
+
throw new Error("Collection name cannot be empty");
|
|
1069
1214
|
}
|
|
1070
|
-
if (
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
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`
|
|
1078
1223
|
);
|
|
1079
1224
|
}
|
|
1080
|
-
return emptyCompletionResult();
|
|
1081
1225
|
}
|
|
1082
|
-
|
|
1083
|
-
if (!
|
|
1084
|
-
|
|
1226
|
+
function parseSort(sort) {
|
|
1227
|
+
if (!sort) return null;
|
|
1228
|
+
if (typeof sort !== "object" || sort === null || Array.isArray(sort)) {
|
|
1229
|
+
return null;
|
|
1085
1230
|
}
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
signal
|
|
1092
|
-
);
|
|
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
|
+
}
|
|
1093
1236
|
}
|
|
1094
|
-
return
|
|
1237
|
+
return Object.keys(validSort).length > 0 ? validSort : null;
|
|
1095
1238
|
}
|
|
1096
|
-
|
|
1097
|
-
if (!
|
|
1098
|
-
return
|
|
1239
|
+
function parseFilter(filter, objectIdMode = "auto") {
|
|
1240
|
+
if (!filter) {
|
|
1241
|
+
return {};
|
|
1099
1242
|
}
|
|
1100
|
-
if (
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
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
|
|
1106
1254
|
);
|
|
1107
1255
|
}
|
|
1108
|
-
|
|
1256
|
+
throw new Error("Query filter must be a plain object or ObjectId");
|
|
1109
1257
|
}
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
(c) => c.name
|
|
1119
|
-
).filter(
|
|
1120
|
-
(name) => (
|
|
1121
|
-
// Filter out system collections
|
|
1122
|
-
!name.startsWith("system.") && // Match partial value
|
|
1123
|
-
name.toLowerCase().includes(partialValue.toLowerCase())
|
|
1124
|
-
)
|
|
1125
|
-
).sort();
|
|
1126
|
-
console.warn(`Found ${matchingCollections.length} matching collections`);
|
|
1127
|
-
const PAGE_SIZE = 100;
|
|
1128
|
-
const { items: limitedResults, nextCursor } = paginate(
|
|
1129
|
-
matchingCollections,
|
|
1130
|
-
void 0,
|
|
1131
|
-
PAGE_SIZE
|
|
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}`
|
|
1132
1266
|
);
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
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)) {
|
|
1317
|
+
result[key] = new Date(dateString);
|
|
1318
|
+
} else {
|
|
1319
|
+
result[key] = value;
|
|
1320
|
+
}
|
|
1321
|
+
} else if (typeof value === "object" && value !== null) {
|
|
1322
|
+
if (Array.isArray(value)) {
|
|
1323
|
+
result[key] = value.map((item) => {
|
|
1324
|
+
if (typeof item === "string" && isObjectIdString(item) && (objectIdMode === "force" || objectIdMode === "auto" && isObjectIdField(key))) {
|
|
1325
|
+
return new ObjectId2(item);
|
|
1326
|
+
} else if (typeof item === "string" && isISODateString(item)) {
|
|
1327
|
+
return new Date(item);
|
|
1328
|
+
} else if (typeof item === "string" && item.startsWith("ISODate(") && item.endsWith(")")) {
|
|
1329
|
+
const dateString = item.substring(8, item.length - 2);
|
|
1330
|
+
return isISODateString(dateString) ? new Date(dateString) : item;
|
|
1331
|
+
}
|
|
1332
|
+
return item;
|
|
1333
|
+
});
|
|
1334
|
+
} else {
|
|
1335
|
+
result[key] = processObjectIdInFilter(
|
|
1336
|
+
value,
|
|
1337
|
+
objectIdMode,
|
|
1338
|
+
depth + 1
|
|
1339
|
+
);
|
|
1340
|
+
}
|
|
1341
|
+
} else {
|
|
1342
|
+
result[key] = value;
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
return result;
|
|
1346
|
+
}
|
|
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)
|
|
1138
1361
|
}
|
|
1139
|
-
|
|
1140
|
-
}
|
|
1141
|
-
console.error("Error completing collection names:", error);
|
|
1142
|
-
return emptyCompletionResult();
|
|
1143
|
-
}
|
|
1362
|
+
]
|
|
1363
|
+
};
|
|
1144
1364
|
}
|
|
1145
|
-
function
|
|
1365
|
+
function toolExecutionError(message) {
|
|
1146
1366
|
return {
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
total: 0,
|
|
1150
|
-
hasMore: false
|
|
1151
|
-
}
|
|
1367
|
+
content: [{ type: "text", text: message }],
|
|
1368
|
+
isError: true
|
|
1152
1369
|
};
|
|
1153
1370
|
}
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
}) {
|
|
1371
|
+
function handleError(error, operation, collectionName) {
|
|
1372
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
1373
|
+
throw error;
|
|
1374
|
+
}
|
|
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");
|
|
1382
|
+
}
|
|
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);
|
|
1391
|
+
}
|
|
1163
1392
|
try {
|
|
1164
|
-
if (
|
|
1165
|
-
|
|
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);
|
|
1166
1404
|
}
|
|
1167
1405
|
signal?.throwIfAborted();
|
|
1168
|
-
const
|
|
1169
|
-
|
|
1170
|
-
|
|
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");
|
|
1171
1410
|
}
|
|
1172
|
-
|
|
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
|
+
}
|
|
1435
|
+
);
|
|
1436
|
+
return formatResponse({
|
|
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
|
+
}
|
|
1446
|
+
});
|
|
1173
1447
|
} catch (error) {
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
}
|
|
1177
|
-
throw new Error("MongoDB ping failed: Unknown error");
|
|
1448
|
+
signal?.throwIfAborted();
|
|
1449
|
+
return handleError(error, "query", collection.collectionName);
|
|
1178
1450
|
}
|
|
1179
1451
|
}
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
request,
|
|
1184
|
-
client,
|
|
1185
|
-
db,
|
|
1186
|
-
isReadOnlyMode,
|
|
1187
|
-
signal,
|
|
1188
|
-
connectionSelection = false
|
|
1189
|
-
}) {
|
|
1190
|
-
return {
|
|
1191
|
-
prompts: [
|
|
1192
|
-
{
|
|
1193
|
-
name: "analyze_collection",
|
|
1194
|
-
description: "Analyze a MongoDB collection structure and contents",
|
|
1195
|
-
arguments: [
|
|
1196
|
-
{
|
|
1197
|
-
name: "collection",
|
|
1198
|
-
description: "Name of the collection to analyze",
|
|
1199
|
-
required: true
|
|
1200
|
-
},
|
|
1201
|
-
...connectionSelection ? [
|
|
1202
|
-
{
|
|
1203
|
-
name: "connectionId",
|
|
1204
|
-
description: "Configured MongoDB connection ID"
|
|
1205
|
-
},
|
|
1206
|
-
{
|
|
1207
|
-
name: "database",
|
|
1208
|
-
description: "Allowed MongoDB database name"
|
|
1209
|
-
}
|
|
1210
|
-
] : []
|
|
1211
|
-
]
|
|
1212
|
-
}
|
|
1213
|
-
]
|
|
1214
|
-
};
|
|
1215
|
-
}
|
|
1216
|
-
async function handleGetPromptRequest({
|
|
1217
|
-
request,
|
|
1218
|
-
client,
|
|
1219
|
-
db,
|
|
1220
|
-
isReadOnlyMode,
|
|
1221
|
-
signal,
|
|
1222
|
-
sendProgress,
|
|
1223
|
-
connectionId,
|
|
1224
|
-
showTarget = false
|
|
1225
|
-
}) {
|
|
1226
|
-
const { name, arguments: args = {} } = request.params;
|
|
1227
|
-
if (name !== "analyze_collection") {
|
|
1228
|
-
throw new Error("Unknown prompt");
|
|
1452
|
+
async function handleAggregate(collection, args, objectIdMode = "auto", isReadOnlyMode = false, allowCrossDb = false, allowServerJs = false, limits = {}, signal) {
|
|
1453
|
+
if (!collection) {
|
|
1454
|
+
throw new Error("Collection is required for aggregate operation");
|
|
1229
1455
|
}
|
|
1230
|
-
const
|
|
1231
|
-
if (!
|
|
1232
|
-
throw new Error("
|
|
1456
|
+
const { pipeline, explain } = args;
|
|
1457
|
+
if (!Array.isArray(pipeline)) {
|
|
1458
|
+
throw new Error("Pipeline must be an array");
|
|
1459
|
+
}
|
|
1460
|
+
if (isReadOnlyMode) {
|
|
1461
|
+
const forbidden = findAggOperator(
|
|
1462
|
+
pipeline,
|
|
1463
|
+
READONLY_FORBIDDEN_AGG_OPERATORS
|
|
1464
|
+
);
|
|
1465
|
+
if (forbidden) {
|
|
1466
|
+
throw new Error(
|
|
1467
|
+
`ReadonlyError: Aggregation operator '${forbidden}' is not allowed in read-only mode`
|
|
1468
|
+
);
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
if (!allowServerJs) {
|
|
1472
|
+
assertNoServerJs(pipeline);
|
|
1473
|
+
}
|
|
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
|
+
);
|
|
1480
|
+
}
|
|
1233
1481
|
}
|
|
1482
|
+
const processedPipeline = pipeline.map((stage) => {
|
|
1483
|
+
if (typeof stage === "object" && stage !== null) {
|
|
1484
|
+
return processObjectIdInFilter(
|
|
1485
|
+
stage,
|
|
1486
|
+
objectIdMode
|
|
1487
|
+
);
|
|
1488
|
+
}
|
|
1489
|
+
return stage;
|
|
1490
|
+
});
|
|
1234
1491
|
try {
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
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);
|
|
1238
1502
|
}
|
|
1239
|
-
await sendProgress?.(1, 3, "Getting schema");
|
|
1240
|
-
signal?.throwIfAborted();
|
|
1241
|
-
const schemaSample = await collection.findOne({});
|
|
1242
|
-
await sendProgress?.(2, 3, "Getting stats");
|
|
1243
1503
|
signal?.throwIfAborted();
|
|
1244
|
-
const
|
|
1245
|
-
|
|
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 } : {}
|
|
1514
|
+
}
|
|
1515
|
+
});
|
|
1516
|
+
} catch (error) {
|
|
1246
1517
|
signal?.throwIfAborted();
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1518
|
+
return handleError(error, "aggregate", collection.collectionName);
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
async function handleUpdate(collection, args, objectIdMode = "auto", allowServerJs = false, signal) {
|
|
1522
|
+
if (!collection) {
|
|
1523
|
+
throw new Error("Collection is required for update operation");
|
|
1524
|
+
}
|
|
1525
|
+
const { filter, update, upsert, multi } = args;
|
|
1526
|
+
const queryFilter = parseFilter(filter, objectIdMode);
|
|
1527
|
+
if (!allowServerJs) {
|
|
1528
|
+
assertNoServerJs(queryFilter);
|
|
1529
|
+
}
|
|
1530
|
+
let processedUpdate = update;
|
|
1531
|
+
if (update && typeof update === "object" && !Array.isArray(update)) {
|
|
1532
|
+
processedUpdate = processObjectIdInFilter(
|
|
1533
|
+
update,
|
|
1534
|
+
objectIdMode
|
|
1535
|
+
);
|
|
1536
|
+
}
|
|
1537
|
+
if (!processedUpdate || typeof processedUpdate !== "object" || Array.isArray(processedUpdate)) {
|
|
1538
|
+
throw new Error("Update must be a valid MongoDB update document");
|
|
1539
|
+
}
|
|
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.)"
|
|
1557
|
+
);
|
|
1558
|
+
}
|
|
1559
|
+
try {
|
|
1560
|
+
const options = {
|
|
1561
|
+
upsert: !!upsert,
|
|
1562
|
+
multi: !!multi
|
|
1278
1563
|
};
|
|
1564
|
+
const updateMethod = options.multi ? "updateMany" : "updateOne";
|
|
1565
|
+
signal?.throwIfAborted();
|
|
1566
|
+
const result = await collection[updateMethod](
|
|
1567
|
+
queryFilter,
|
|
1568
|
+
processedUpdate,
|
|
1569
|
+
options
|
|
1570
|
+
);
|
|
1571
|
+
return formatResponse({
|
|
1572
|
+
matchedCount: result.matchedCount,
|
|
1573
|
+
modifiedCount: result.modifiedCount,
|
|
1574
|
+
upsertedCount: result.upsertedCount,
|
|
1575
|
+
upsertedId: result.upsertedId
|
|
1576
|
+
});
|
|
1279
1577
|
} catch (error) {
|
|
1280
|
-
|
|
1281
|
-
throw new Error(`Failed to analyze collection ${collectionName}: ${msg}`);
|
|
1578
|
+
return handleError(error, "update", collection.collectionName);
|
|
1282
1579
|
}
|
|
1283
1580
|
}
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1581
|
+
async function handleServerInfo(db, isReadOnlyMode, args, signal) {
|
|
1582
|
+
const { includeDebugInfo } = args;
|
|
1583
|
+
try {
|
|
1584
|
+
signal?.throwIfAborted();
|
|
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
|
+
};
|
|
1301
1622
|
}
|
|
1302
|
-
return
|
|
1303
|
-
}
|
|
1304
|
-
|
|
1305
|
-
return "Document";
|
|
1623
|
+
return formatResponse(serverInfo);
|
|
1624
|
+
} catch (error) {
|
|
1625
|
+
return handleError(error, "get server information");
|
|
1306
1626
|
}
|
|
1307
|
-
return typeof value;
|
|
1308
1627
|
}
|
|
1309
|
-
function
|
|
1310
|
-
if (!
|
|
1311
|
-
|
|
1628
|
+
async function handleInsert(collection, args, objectIdMode = "auto", signal) {
|
|
1629
|
+
if (!collection) {
|
|
1630
|
+
throw new Error("Collection is required for insert operation");
|
|
1312
1631
|
}
|
|
1313
|
-
const
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
if (!fieldMap.has(key)) {
|
|
1317
|
-
fieldMap.set(key, {
|
|
1318
|
-
name: key,
|
|
1319
|
-
types: /* @__PURE__ */ new Set([detectMongoType(value)]),
|
|
1320
|
-
nullable: false,
|
|
1321
|
-
// Store sample values for complex types
|
|
1322
|
-
samples: [value]
|
|
1323
|
-
});
|
|
1324
|
-
} else {
|
|
1325
|
-
const fieldInfo = fieldMap.get(key);
|
|
1326
|
-
fieldInfo.types.add(detectMongoType(value));
|
|
1327
|
-
if (fieldInfo.samples.length < 3 && !fieldInfo.samples.some(
|
|
1328
|
-
(sample) => JSON.stringify(sample) === JSON.stringify(value)
|
|
1329
|
-
)) {
|
|
1330
|
-
fieldInfo.samples.push(value);
|
|
1331
|
-
}
|
|
1332
|
-
}
|
|
1333
|
-
}
|
|
1632
|
+
const { documents, ordered, writeConcern, bypassDocumentValidation } = args;
|
|
1633
|
+
if (!Array.isArray(documents)) {
|
|
1634
|
+
throw new Error("Documents must be an array");
|
|
1334
1635
|
}
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
if (!(key in doc)) {
|
|
1338
|
-
const fieldInfo = fieldMap.get(key);
|
|
1339
|
-
fieldInfo.nullable = true;
|
|
1340
|
-
}
|
|
1341
|
-
}
|
|
1636
|
+
if (documents.length === 0) {
|
|
1637
|
+
throw new Error("Documents array cannot be empty");
|
|
1342
1638
|
}
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
).map((doc) => doc[key]);
|
|
1348
|
-
if (nestedDocs.length > 0) {
|
|
1349
|
-
fieldInfo.nestedSchema = inferSchemaFromSamples(nestedDocs);
|
|
1350
|
-
}
|
|
1351
|
-
}
|
|
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");
|
|
1352
1643
|
}
|
|
1353
|
-
const
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1644
|
+
const processedDocuments = documents.map(
|
|
1645
|
+
(doc) => processObjectIdInFilter(doc, objectIdMode)
|
|
1646
|
+
);
|
|
1647
|
+
try {
|
|
1648
|
+
const options = {
|
|
1649
|
+
ordered: ordered !== false,
|
|
1650
|
+
// default to true if not specified
|
|
1651
|
+
writeConcern,
|
|
1652
|
+
bypassDocumentValidation
|
|
1362
1653
|
};
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
return sample;
|
|
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
|
|
1373
1663
|
});
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1664
|
+
} catch (error) {
|
|
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
|
+
});
|
|
1673
|
+
}
|
|
1674
|
+
return handleError(error, "insert", collection.collectionName);
|
|
1675
|
+
}
|
|
1378
1676
|
}
|
|
1379
|
-
async function
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
}
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1677
|
+
async function handleCreateIndex(collection, args, objectIdMode = "auto", signal) {
|
|
1678
|
+
if (!collection) {
|
|
1679
|
+
throw new Error("Collection is required for createIndex operation");
|
|
1680
|
+
}
|
|
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");
|
|
1684
|
+
}
|
|
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
|
+
);
|
|
1689
|
+
}
|
|
1690
|
+
if (commitQuorum !== void 0 && typeof commitQuorum !== "string" && typeof commitQuorum !== "number") {
|
|
1691
|
+
throw new Error("Commit quorum must be a string or number");
|
|
1692
|
+
}
|
|
1693
|
+
const processedIndexes = indexes.map((index) => {
|
|
1694
|
+
if (index && typeof index === "object") {
|
|
1695
|
+
return processObjectIdInFilter(
|
|
1696
|
+
index,
|
|
1697
|
+
objectIdMode
|
|
1394
1698
|
);
|
|
1395
1699
|
}
|
|
1396
|
-
|
|
1397
|
-
}
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1700
|
+
return index;
|
|
1701
|
+
});
|
|
1702
|
+
try {
|
|
1703
|
+
const indexOptions = {
|
|
1704
|
+
commitQuorum
|
|
1705
|
+
};
|
|
1706
|
+
signal?.throwIfAborted();
|
|
1707
|
+
const createdIndexes = await collection.createIndexes(
|
|
1708
|
+
processedIndexes,
|
|
1709
|
+
indexOptions
|
|
1402
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);
|
|
1403
1718
|
}
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
);
|
|
1719
|
+
}
|
|
1720
|
+
async function handleCount(collection, args, objectIdMode = "auto", allowServerJs = false, signal) {
|
|
1721
|
+
if (!collection) {
|
|
1722
|
+
throw new Error("Collection is required for count operation");
|
|
1409
1723
|
}
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
);
|
|
1724
|
+
const { query, limit, skip, hint, readConcern, maxTimeMS, collation } = args;
|
|
1725
|
+
const countQuery = parseFilter(query, objectIdMode);
|
|
1726
|
+
if (!allowServerJs) {
|
|
1727
|
+
assertNoServerJs(countQuery);
|
|
1415
1728
|
}
|
|
1416
1729
|
try {
|
|
1417
|
-
const
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
}
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
signal?.throwIfAborted();
|
|
1429
|
-
sampleDocuments = await collection.find({}).limit(sampleSize).toArray();
|
|
1430
|
-
}
|
|
1431
|
-
await sendProgress?.(2, 4, "Getting indexes");
|
|
1432
|
-
signal?.throwIfAborted();
|
|
1433
|
-
const indexes = await collection.indexes();
|
|
1434
|
-
const inferredSchema = inferSchemaFromSamples(sampleDocuments);
|
|
1435
|
-
await sendProgress?.(3, 4, "Counting documents");
|
|
1436
|
-
let documentCount = null;
|
|
1437
|
-
try {
|
|
1438
|
-
signal?.throwIfAborted();
|
|
1439
|
-
documentCount = await Promise.race([
|
|
1440
|
-
collection.countDocuments(),
|
|
1441
|
-
new Promise(
|
|
1442
|
-
(_, reject) => setTimeout(
|
|
1443
|
-
() => reject(new Error("Count operation timed out")),
|
|
1444
|
-
5e3
|
|
1445
|
-
)
|
|
1446
|
-
)
|
|
1447
|
-
]);
|
|
1448
|
-
} catch (countError) {
|
|
1449
|
-
console.warn(
|
|
1450
|
-
`Count operation failed or timed out for ${collectionName}: ${countError}`
|
|
1451
|
-
);
|
|
1452
|
-
try {
|
|
1453
|
-
signal?.throwIfAborted();
|
|
1454
|
-
const stats = await db.command({ collStats: collectionName });
|
|
1455
|
-
documentCount = stats.count;
|
|
1456
|
-
} catch {
|
|
1457
|
-
documentCount = "unknown (count operation timed out)";
|
|
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];
|
|
1458
1741
|
}
|
|
1459
1742
|
}
|
|
1460
|
-
|
|
1461
|
-
const
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
name: idx.name,
|
|
1467
|
-
keys: idx.key
|
|
1468
|
-
})),
|
|
1469
|
-
documentCount,
|
|
1470
|
-
sampleSize: sampleDocuments.length,
|
|
1471
|
-
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
1472
|
-
};
|
|
1473
|
-
return {
|
|
1474
|
-
contents: [
|
|
1475
|
-
{
|
|
1476
|
-
uri: request.params.uri,
|
|
1477
|
-
mimeType: "application/json",
|
|
1478
|
-
text: JSON.stringify(schema, null, 2)
|
|
1479
|
-
}
|
|
1480
|
-
]
|
|
1481
|
-
};
|
|
1743
|
+
signal?.throwIfAborted();
|
|
1744
|
+
const count = await collection.countDocuments(countQuery, options);
|
|
1745
|
+
return formatResponse({
|
|
1746
|
+
count,
|
|
1747
|
+
ok: 1
|
|
1748
|
+
});
|
|
1482
1749
|
} catch (error) {
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1750
|
+
return handleError(error, "count documents", collection.collectionName);
|
|
1751
|
+
}
|
|
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);
|
|
1487
1767
|
}
|
|
1768
|
+
} else {
|
|
1488
1769
|
throw new Error(
|
|
1489
|
-
|
|
1770
|
+
"convertTime 'input' must be a Unix timestamp (number) or a date string"
|
|
1771
|
+
);
|
|
1772
|
+
}
|
|
1773
|
+
if (Number.isNaN(date.getTime())) {
|
|
1774
|
+
throw new Error(
|
|
1775
|
+
`Could not interpret '${String(input)}' as a date or Unix timestamp`
|
|
1490
1776
|
);
|
|
1491
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
|
+
});
|
|
1492
1794
|
}
|
|
1493
|
-
async function
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
isReadOnlyMode,
|
|
1498
|
-
signal
|
|
1499
|
-
}) {
|
|
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;
|
|
1500
1799
|
try {
|
|
1800
|
+
const processedFilter = parseFilter(filter, objectIdMode);
|
|
1501
1801
|
signal?.throwIfAborted();
|
|
1502
|
-
const collections = await db.listCollections().toArray();
|
|
1503
|
-
const
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
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
|
|
1814
|
+
}
|
|
1815
|
+
});
|
|
1515
1816
|
} catch (error) {
|
|
1516
|
-
|
|
1517
|
-
throw new Error(`Failed to list collections: ${error.message}`);
|
|
1518
|
-
}
|
|
1519
|
-
throw new Error("Failed to list collections: Unknown error");
|
|
1817
|
+
return handleError(error, "list collections");
|
|
1520
1818
|
}
|
|
1521
1819
|
}
|
|
1522
1820
|
|
|
1523
|
-
// src/schemas/
|
|
1524
|
-
async function
|
|
1821
|
+
// src/schemas/completion.ts
|
|
1822
|
+
async function handleCompletionRequest({
|
|
1525
1823
|
request,
|
|
1526
1824
|
client,
|
|
1527
1825
|
db,
|
|
1528
1826
|
isReadOnlyMode,
|
|
1529
1827
|
signal
|
|
1530
1828
|
}) {
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
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) {
|
|
1881
|
+
try {
|
|
1882
|
+
console.warn(
|
|
1883
|
+
`Completing collection names with partial value: ${partialValue}`
|
|
1884
|
+
);
|
|
1885
|
+
signal?.throwIfAborted();
|
|
1886
|
+
const collections = await db.listCollections().toArray();
|
|
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
|
+
);
|
|
1903
|
+
return {
|
|
1904
|
+
completion: {
|
|
1905
|
+
values: limitedResults,
|
|
1906
|
+
total: matchingCollections.length,
|
|
1907
|
+
hasMore: !!nextCursor
|
|
1565
1908
|
}
|
|
1566
|
-
|
|
1909
|
+
};
|
|
1910
|
+
} catch (error) {
|
|
1911
|
+
console.error("Error completing collection names:", error);
|
|
1912
|
+
return emptyCompletionResult();
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
function emptyCompletionResult() {
|
|
1916
|
+
return {
|
|
1917
|
+
completion: {
|
|
1918
|
+
values: [],
|
|
1919
|
+
total: 0,
|
|
1920
|
+
hasMore: false
|
|
1921
|
+
}
|
|
1567
1922
|
};
|
|
1568
1923
|
}
|
|
1569
1924
|
|
|
1570
|
-
// src/schemas/
|
|
1571
|
-
async function
|
|
1925
|
+
// src/schemas/ping.ts
|
|
1926
|
+
async function handlePingRequest({
|
|
1572
1927
|
request,
|
|
1573
1928
|
client,
|
|
1574
1929
|
db,
|
|
1575
1930
|
isReadOnlyMode,
|
|
1576
|
-
signal
|
|
1577
|
-
connectionSelection = false
|
|
1931
|
+
signal
|
|
1578
1932
|
}) {
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
},
|
|
1609
|
-
sort: {
|
|
1610
|
-
type: "object",
|
|
1611
|
-
description: "Sort order as a field-to-direction map, e.g. { orderDate: -1 } for descending or { name: 1 } for ascending"
|
|
1612
|
-
},
|
|
1613
|
-
explain: {
|
|
1614
|
-
type: "string",
|
|
1615
|
-
description: "Optional: Get query execution information",
|
|
1616
|
-
enum: ["queryPlanner", "executionStats", "allPlansExecution"]
|
|
1617
|
-
},
|
|
1618
|
-
objectIdMode: {
|
|
1619
|
-
type: "string",
|
|
1620
|
-
description: "Control how 24-character hex strings are handled",
|
|
1621
|
-
enum: ["auto", "none", "force"],
|
|
1622
|
-
default: "auto"
|
|
1623
|
-
}
|
|
1624
|
-
},
|
|
1625
|
-
required: ["collection"]
|
|
1626
|
-
}
|
|
1627
|
-
},
|
|
1628
|
-
{
|
|
1629
|
-
name: "aggregate",
|
|
1630
|
-
description: "Run a MongoDB aggregation pipeline for grouping, joins, or multi-stage transformations. Request an execution plan to inspect the pipeline.",
|
|
1631
|
-
inputSchema: {
|
|
1632
|
-
type: "object",
|
|
1633
|
-
properties: {
|
|
1634
|
-
collection: {
|
|
1635
|
-
type: "string",
|
|
1636
|
-
description: "Name of the collection to aggregate"
|
|
1637
|
-
},
|
|
1638
|
-
pipeline: {
|
|
1639
|
-
type: "array",
|
|
1640
|
-
description: "Aggregation pipeline stages. Supports date strings in ISO format ('2025-01-01T00:00:00Z') and ISODate('2025-01-01T00:00:00Z') notation",
|
|
1641
|
-
items: {
|
|
1642
|
-
type: "object"
|
|
1643
|
-
}
|
|
1644
|
-
},
|
|
1645
|
-
explain: {
|
|
1646
|
-
type: "string",
|
|
1647
|
-
description: "Optional: Get aggregation execution information (queryPlanner, executionStats, or allPlansExecution)",
|
|
1648
|
-
enum: ["queryPlanner", "executionStats", "allPlansExecution"]
|
|
1649
|
-
},
|
|
1650
|
-
objectIdMode: {
|
|
1651
|
-
type: "string",
|
|
1652
|
-
description: "Control how 24-character hex strings are handled",
|
|
1653
|
-
enum: ["auto", "none", "force"],
|
|
1654
|
-
default: "auto"
|
|
1655
|
-
}
|
|
1656
|
-
},
|
|
1657
|
-
required: ["collection", "pipeline"]
|
|
1658
|
-
}
|
|
1659
|
-
},
|
|
1660
|
-
{
|
|
1661
|
-
name: "update",
|
|
1662
|
-
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.",
|
|
1663
|
-
inputSchema: {
|
|
1664
|
-
type: "object",
|
|
1665
|
-
properties: {
|
|
1666
|
-
collection: {
|
|
1667
|
-
type: "string",
|
|
1668
|
-
description: "Name of the collection to update"
|
|
1669
|
-
},
|
|
1670
|
-
filter: {
|
|
1671
|
-
type: "object",
|
|
1672
|
-
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"
|
|
1673
|
-
},
|
|
1674
|
-
update: {
|
|
1675
|
-
type: "object",
|
|
1676
|
-
description: "Update operations to apply ($set, $unset, $inc, etc.)"
|
|
1677
|
-
},
|
|
1678
|
-
upsert: {
|
|
1679
|
-
type: "boolean",
|
|
1680
|
-
description: "Create a new document if no documents match the filter"
|
|
1681
|
-
},
|
|
1682
|
-
multi: {
|
|
1683
|
-
type: "boolean",
|
|
1684
|
-
description: "Update multiple documents that match the filter"
|
|
1685
|
-
},
|
|
1686
|
-
objectIdMode: {
|
|
1687
|
-
type: "string",
|
|
1688
|
-
description: "Control how 24-character hex strings are handled",
|
|
1689
|
-
enum: ["auto", "none", "force"],
|
|
1690
|
-
default: "auto"
|
|
1691
|
-
}
|
|
1692
|
-
},
|
|
1693
|
-
required: ["collection", "filter", "update"]
|
|
1694
|
-
}
|
|
1695
|
-
},
|
|
1696
|
-
{
|
|
1697
|
-
name: "serverInfo",
|
|
1698
|
-
description: "Inspect MongoDB server metadata such as version and storage engine. Enable includeDebugInfo for additional diagnostics.",
|
|
1699
|
-
inputSchema: {
|
|
1700
|
-
type: "object",
|
|
1701
|
-
properties: {
|
|
1702
|
-
includeDebugInfo: {
|
|
1703
|
-
type: "boolean",
|
|
1704
|
-
description: "Include additional debug information about the server"
|
|
1705
|
-
}
|
|
1706
|
-
}
|
|
1707
|
-
}
|
|
1708
|
-
},
|
|
1709
|
-
{
|
|
1710
|
-
name: "insert",
|
|
1711
|
-
description: "Insert new documents into a MongoDB collection. Use update to modify existing records.",
|
|
1712
|
-
inputSchema: {
|
|
1713
|
-
type: "object",
|
|
1714
|
-
properties: {
|
|
1715
|
-
collection: {
|
|
1716
|
-
type: "string",
|
|
1717
|
-
description: "Name of the collection to insert into"
|
|
1718
|
-
},
|
|
1719
|
-
documents: {
|
|
1720
|
-
type: "array",
|
|
1721
|
-
description: "Array of documents to insert",
|
|
1722
|
-
items: { type: "object" }
|
|
1723
|
-
},
|
|
1724
|
-
ordered: {
|
|
1725
|
-
type: "boolean",
|
|
1726
|
-
description: "If true, perform ordered insert. If false, insert unordered"
|
|
1727
|
-
},
|
|
1728
|
-
writeConcern: {
|
|
1729
|
-
type: "object",
|
|
1730
|
-
description: "Write concern for the insert operation"
|
|
1731
|
-
},
|
|
1732
|
-
bypassDocumentValidation: {
|
|
1733
|
-
type: "boolean",
|
|
1734
|
-
description: "Allow insert to bypass schema validation"
|
|
1735
|
-
},
|
|
1736
|
-
objectIdMode: {
|
|
1737
|
-
type: "string",
|
|
1738
|
-
description: "Control how 24-character hex strings are handled",
|
|
1739
|
-
enum: ["auto", "none", "force"],
|
|
1740
|
-
default: "auto"
|
|
1741
|
-
}
|
|
1742
|
-
},
|
|
1743
|
-
required: ["collection", "documents"]
|
|
1744
|
-
}
|
|
1745
|
-
},
|
|
1746
|
-
{
|
|
1747
|
-
name: "createIndex",
|
|
1748
|
-
description: "Create indexes to improve query performance, enforce uniqueness, or expire documents with a TTL index.",
|
|
1749
|
-
inputSchema: {
|
|
1750
|
-
type: "object",
|
|
1751
|
-
properties: {
|
|
1752
|
-
collection: {
|
|
1753
|
-
type: "string",
|
|
1754
|
-
description: "Name of the collection to create indexes on"
|
|
1755
|
-
},
|
|
1756
|
-
indexes: {
|
|
1757
|
-
type: "array",
|
|
1758
|
-
description: "Array of index specifications",
|
|
1759
|
-
items: {
|
|
1760
|
-
type: "object",
|
|
1761
|
-
properties: {
|
|
1762
|
-
key: {
|
|
1763
|
-
type: "object",
|
|
1764
|
-
description: "Index key pattern, e.g. { field: 1 }"
|
|
1765
|
-
},
|
|
1766
|
-
name: {
|
|
1767
|
-
type: "string",
|
|
1768
|
-
description: "Optional: Name of the index"
|
|
1769
|
-
},
|
|
1770
|
-
unique: {
|
|
1771
|
-
type: "boolean",
|
|
1772
|
-
description: "Optional: Creates a unique index"
|
|
1773
|
-
},
|
|
1774
|
-
sparse: {
|
|
1775
|
-
type: "boolean",
|
|
1776
|
-
description: "Optional: Creates a sparse index"
|
|
1777
|
-
},
|
|
1778
|
-
background: {
|
|
1779
|
-
type: "boolean",
|
|
1780
|
-
description: "Optional: Builds index in background"
|
|
1781
|
-
},
|
|
1782
|
-
expireAfterSeconds: {
|
|
1783
|
-
type: "number",
|
|
1784
|
-
description: "TTL in seconds for documents"
|
|
1785
|
-
},
|
|
1786
|
-
partialFilterExpression: {
|
|
1787
|
-
type: "object",
|
|
1788
|
-
description: "Filter expression for partial indexes"
|
|
1789
|
-
}
|
|
1790
|
-
},
|
|
1791
|
-
required: ["key"]
|
|
1792
|
-
}
|
|
1793
|
-
},
|
|
1794
|
-
writeConcern: {
|
|
1795
|
-
type: "object",
|
|
1796
|
-
description: "Write concern for index creation"
|
|
1797
|
-
},
|
|
1798
|
-
commitQuorum: {
|
|
1799
|
-
type: ["string", "number"],
|
|
1800
|
-
description: "Replica-set commit quorum as a member count or a mode such as majority"
|
|
1801
|
-
},
|
|
1802
|
-
objectIdMode: {
|
|
1803
|
-
type: "string",
|
|
1804
|
-
description: "Control how 24-character hex strings are handled",
|
|
1805
|
-
enum: ["auto", "none", "force"],
|
|
1806
|
-
default: "auto"
|
|
1807
|
-
}
|
|
1808
|
-
},
|
|
1809
|
-
required: ["collection", "indexes"]
|
|
1810
|
-
}
|
|
1811
|
-
},
|
|
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
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
// src/schemas/prompts.ts
|
|
1952
|
+
async function handleListPromptsRequest({
|
|
1953
|
+
request,
|
|
1954
|
+
client,
|
|
1955
|
+
db,
|
|
1956
|
+
isReadOnlyMode,
|
|
1957
|
+
signal,
|
|
1958
|
+
connectionSelection = false
|
|
1959
|
+
}) {
|
|
1960
|
+
return {
|
|
1961
|
+
prompts: [
|
|
1812
1962
|
{
|
|
1813
|
-
name: "
|
|
1814
|
-
description: "
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
limit: { type: "integer", description: "Max documents to count" },
|
|
1821
|
-
skip: {
|
|
1822
|
-
type: "integer",
|
|
1823
|
-
description: "Docs to skip before counting"
|
|
1824
|
-
},
|
|
1825
|
-
hint: { type: "object", description: "Index hint" },
|
|
1826
|
-
readConcern: { type: "object", description: "Read concern option" },
|
|
1827
|
-
maxTimeMS: { type: "integer", description: "Max execution time" },
|
|
1828
|
-
collation: {
|
|
1829
|
-
type: "object",
|
|
1830
|
-
description: "Collation rules for comparison"
|
|
1831
|
-
},
|
|
1832
|
-
objectIdMode: {
|
|
1833
|
-
type: "string",
|
|
1834
|
-
description: "Control how 24-character hex strings are handled",
|
|
1835
|
-
enum: ["auto", "none", "force"],
|
|
1836
|
-
default: "auto"
|
|
1837
|
-
}
|
|
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
|
|
1838
1970
|
},
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
name: "listCollections",
|
|
1844
|
-
description: "List collections in the connected database. Filter by name or other collection properties and request names only for a compact result.",
|
|
1845
|
-
inputSchema: {
|
|
1846
|
-
type: "object",
|
|
1847
|
-
properties: {
|
|
1848
|
-
nameOnly: {
|
|
1849
|
-
type: "boolean",
|
|
1850
|
-
description: "If true, return only collection names"
|
|
1851
|
-
},
|
|
1852
|
-
filter: {
|
|
1853
|
-
type: "object",
|
|
1854
|
-
description: "Filter for collections"
|
|
1855
|
-
},
|
|
1856
|
-
skip: {
|
|
1857
|
-
type: "number",
|
|
1858
|
-
description: "Number of collections to skip before returning results",
|
|
1859
|
-
default: 0
|
|
1860
|
-
},
|
|
1861
|
-
limit: {
|
|
1862
|
-
type: "number",
|
|
1863
|
-
description: "Maximum number of collections to return",
|
|
1864
|
-
default: 20
|
|
1971
|
+
...connectionSelection ? [
|
|
1972
|
+
{
|
|
1973
|
+
name: "connectionId",
|
|
1974
|
+
description: "Configured MongoDB connection ID"
|
|
1865
1975
|
},
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
description: "
|
|
1869
|
-
enum: ["auto", "none", "force"],
|
|
1870
|
-
default: "auto"
|
|
1871
|
-
}
|
|
1872
|
-
}
|
|
1873
|
-
}
|
|
1874
|
-
},
|
|
1875
|
-
{
|
|
1876
|
-
name: "convertTime",
|
|
1877
|
-
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.",
|
|
1878
|
-
inputSchema: {
|
|
1879
|
-
type: "object",
|
|
1880
|
-
properties: {
|
|
1881
|
-
input: {
|
|
1882
|
-
type: "string",
|
|
1883
|
-
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."
|
|
1976
|
+
{
|
|
1977
|
+
name: "database",
|
|
1978
|
+
description: "Allowed MongoDB database name"
|
|
1884
1979
|
}
|
|
1885
|
-
|
|
1886
|
-
|
|
1980
|
+
] : []
|
|
1981
|
+
]
|
|
1887
1982
|
}
|
|
1888
1983
|
]
|
|
1889
1984
|
};
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
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");
|
|
1908
2008
|
}
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
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)}`
|
|
2038
|
+
}
|
|
2039
|
+
},
|
|
2040
|
+
{
|
|
2041
|
+
role: "user",
|
|
2042
|
+
content: {
|
|
2043
|
+
type: "text",
|
|
2044
|
+
text: "Provide insights about the collection's structure, data types, and basic statistics."
|
|
2045
|
+
}
|
|
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: [
|
|
1915
2065
|
{
|
|
1916
|
-
name: "
|
|
1917
|
-
description: "
|
|
1918
|
-
|
|
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.`
|
|
1919
2097
|
}
|
|
1920
|
-
|
|
1921
|
-
}
|
|
1922
|
-
return result;
|
|
2098
|
+
]
|
|
2099
|
+
};
|
|
1923
2100
|
}
|
|
1924
2101
|
|
|
1925
2102
|
// src/server.ts
|
|
@@ -1930,13 +2107,13 @@ function createSendProgress(progressToken, notify) {
|
|
|
1930
2107
|
params: { progressToken, progress, total, message }
|
|
1931
2108
|
});
|
|
1932
2109
|
}
|
|
1933
|
-
function createServer(client, db, isReadOnlyMode = false, allowCrossDb = false, allowServerJs = false, options = {}, connections2) {
|
|
2110
|
+
function createServer(client, db, isReadOnlyMode = false, allowCrossDb = false, allowServerJs = false, options = {}, connections2, queryLimits) {
|
|
1934
2111
|
const defaultTarget = { client, db, isReadOnlyMode };
|
|
1935
2112
|
const server = new Server(
|
|
1936
2113
|
{
|
|
1937
2114
|
name: "mongodb",
|
|
1938
2115
|
title: "MongoDB MCP Server",
|
|
1939
|
-
version: "3.
|
|
2116
|
+
version: "3.2.0",
|
|
1940
2117
|
description: "MCP server for MongoDB: query, aggregate, and manage collections with read-only mode, progress notifications, and cancellation",
|
|
1941
2118
|
websiteUrl: "https://github.com/kiliczsh/mcp-mongo-server",
|
|
1942
2119
|
...options
|
|
@@ -1997,6 +2174,8 @@ function createServer(client, db, isReadOnlyMode = false, allowCrossDb = false,
|
|
|
1997
2174
|
db,
|
|
1998
2175
|
isReadOnlyMode,
|
|
1999
2176
|
connectionSelection: !!connections2,
|
|
2177
|
+
hasWritableConnection: connections2?.list().some(({ readOnly }) => !readOnly),
|
|
2178
|
+
queryLimits,
|
|
2000
2179
|
signal: ctx.mcpReq.signal
|
|
2001
2180
|
})
|
|
2002
2181
|
);
|
|
@@ -2010,6 +2189,7 @@ function createServer(client, db, isReadOnlyMode = false, allowCrossDb = false,
|
|
|
2010
2189
|
allowCrossDb,
|
|
2011
2190
|
allowServerJs,
|
|
2012
2191
|
connections: connections2,
|
|
2192
|
+
queryLimits,
|
|
2013
2193
|
signal: ctx.mcpReq.signal
|
|
2014
2194
|
})
|
|
2015
2195
|
);
|
|
@@ -2174,6 +2354,7 @@ async function main() {
|
|
|
2174
2354
|
connectionUrl = process.env.MCP_MONGODB_URI || "";
|
|
2175
2355
|
}
|
|
2176
2356
|
try {
|
|
2357
|
+
const queryLimits = readQueryLimits(process.env);
|
|
2177
2358
|
connections = new Connections(
|
|
2178
2359
|
connectionUrl,
|
|
2179
2360
|
process.env.MCP_MONGODB_CONNECTIONS,
|
|
@@ -2188,6 +2369,7 @@ async function main() {
|
|
|
2188
2369
|
allowCrossDb,
|
|
2189
2370
|
allowServerJs,
|
|
2190
2371
|
connections,
|
|
2372
|
+
queryLimits,
|
|
2191
2373
|
port,
|
|
2192
2374
|
allowedOrigins,
|
|
2193
2375
|
jsonLimit,
|
|
@@ -2200,16 +2382,17 @@ async function main() {
|
|
|
2200
2382
|
isReadOnlyMode,
|
|
2201
2383
|
allowCrossDb,
|
|
2202
2384
|
allowServerJs,
|
|
2203
|
-
connections
|
|
2385
|
+
connections,
|
|
2386
|
+
queryLimits
|
|
2204
2387
|
);
|
|
2205
2388
|
}
|
|
2206
2389
|
} catch (error) {
|
|
2207
|
-
console.error("Failed to
|
|
2390
|
+
console.error("Failed to start MongoDB MCP server:", error);
|
|
2208
2391
|
await connections?.close();
|
|
2209
2392
|
process.exit(1);
|
|
2210
2393
|
}
|
|
2211
2394
|
}
|
|
2212
|
-
async function startStdioServer(client, db, isReadOnlyMode, allowCrossDb, allowServerJs, connections2) {
|
|
2395
|
+
async function startStdioServer(client, db, isReadOnlyMode, allowCrossDb, allowServerJs, connections2, queryLimits) {
|
|
2213
2396
|
serveStdio(
|
|
2214
2397
|
() => createServer(
|
|
2215
2398
|
client,
|
|
@@ -2218,7 +2401,8 @@ async function startStdioServer(client, db, isReadOnlyMode, allowCrossDb, allowS
|
|
|
2218
2401
|
allowCrossDb,
|
|
2219
2402
|
allowServerJs,
|
|
2220
2403
|
{},
|
|
2221
|
-
connections2
|
|
2404
|
+
connections2,
|
|
2405
|
+
queryLimits
|
|
2222
2406
|
)
|
|
2223
2407
|
);
|
|
2224
2408
|
console.warn("Server connected successfully via stdio");
|
|
@@ -2246,7 +2430,7 @@ function bearerTokenValid(authorization, expected) {
|
|
|
2246
2430
|
const wanted = Buffer.from(expected);
|
|
2247
2431
|
return provided.length === wanted.length && timingSafeEqual(provided, wanted);
|
|
2248
2432
|
}
|
|
2249
|
-
async function startHttpServer(client, db, isReadOnlyMode, allowCrossDb, allowServerJs, connections2, port, allowedOrigins, jsonLimit, authToken) {
|
|
2433
|
+
async function startHttpServer(client, db, isReadOnlyMode, allowCrossDb, allowServerJs, connections2, queryLimits, port, allowedOrigins, jsonLimit, authToken) {
|
|
2250
2434
|
const app = createMcpExpressApp({ host: "0.0.0.0", jsonLimit });
|
|
2251
2435
|
app.use((req, res, next) => {
|
|
2252
2436
|
const start = Date.now();
|
|
@@ -2304,7 +2488,8 @@ async function startHttpServer(client, db, isReadOnlyMode, allowCrossDb, allowSe
|
|
|
2304
2488
|
allowCrossDb,
|
|
2305
2489
|
allowServerJs,
|
|
2306
2490
|
{},
|
|
2307
|
-
connections2
|
|
2491
|
+
connections2,
|
|
2492
|
+
queryLimits
|
|
2308
2493
|
)
|
|
2309
2494
|
);
|
|
2310
2495
|
const nodeHandler = toNodeHandler(mcpHandler, {
|