mcp-mongo-server 1.3.0 → 2.1.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/build/index.js CHANGED
@@ -1,78 +1,1840 @@
1
1
  #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
2
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
- import { connectToMongoDB } from "./mongo.js";
4
- import { createServer } from "./server.js";
5
- // Declare a client variable in the global scope for cleanup handlers
6
- let mongoClient = null; // TODO: Fix Typescript error
7
- /**
8
- * Start the server using stdio transport and initialize MongoDB connection.
9
- */
10
- async function main() {
11
- const args = process.argv.slice(2);
12
- // Default to environment variables
13
- let connectionUrl = "";
14
- let readOnlyMode = process.env.MCP_MONGODB_READONLY === "true" || false;
15
- // Parse command line arguments (these take precedence)
16
- for (let i = 0; i < args.length; i++) {
17
- if (args[i] === "--read-only" || args[i] === "-r") {
18
- readOnlyMode = true;
19
- }
20
- else if (!connectionUrl) {
21
- connectionUrl = args[i];
22
- }
23
- }
24
- // If no connection URL from command line, use environment variable
25
- if (!connectionUrl) {
26
- connectionUrl = process.env.MCP_MONGODB_URI || "";
27
- }
28
- if (!connectionUrl) {
29
- console.error("Please provide a MongoDB connection URL via command-line argument or MCP_MONGODB_URI environment variable");
30
- console.error("Usage: command <mongodb-url> [--read-only|-r]");
31
- console.error(" or: MCP_MONGODB_URI=<mongodb-url> [MCP_MONGODB_READONLY=true] command");
32
- process.exit(1);
6
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
7
+
8
+ // src/mongo.ts
9
+ import { MongoClient, ReadPreference } from "mongodb";
10
+ async function connectToMongoDB(url, readOnly) {
11
+ try {
12
+ const options = readOnly ? { readPreference: ReadPreference.SECONDARY } : {};
13
+ const client = new MongoClient(url, options);
14
+ await client.connect();
15
+ const db = client.db();
16
+ console.warn(`Connected to MongoDB database: ${db.databaseName}`);
17
+ return {
18
+ client,
19
+ db,
20
+ isConnected: true,
21
+ isReadOnlyMode: readOnly
22
+ };
23
+ } catch (error) {
24
+ console.error("Failed to connect to MongoDB:", error);
25
+ return {
26
+ client: null,
27
+ db: null,
28
+ isConnected: false,
29
+ isReadOnlyMode: readOnly
30
+ };
31
+ }
32
+ }
33
+
34
+ // src/server.ts
35
+ import {
36
+ InMemoryTaskMessageQueue,
37
+ InMemoryTaskStore
38
+ } from "@modelcontextprotocol/sdk/experimental/tasks";
39
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
40
+ import {
41
+ CallToolRequestSchema,
42
+ CompleteRequestSchema,
43
+ GetPromptRequestSchema,
44
+ ListPromptsRequestSchema,
45
+ ListResourcesRequestSchema,
46
+ ListResourceTemplatesRequestSchema,
47
+ ListToolsRequestSchema,
48
+ PingRequestSchema,
49
+ ReadResourceRequestSchema
50
+ } from "@modelcontextprotocol/sdk/types.js";
51
+
52
+ // src/schemas/call.ts
53
+ import {
54
+ ErrorCode,
55
+ McpError
56
+ } from "@modelcontextprotocol/sdk/types.js";
57
+ import { ObjectId } from "mongodb";
58
+ var COLLECTION_OPERATIONS = [
59
+ "query",
60
+ "aggregate",
61
+ "update",
62
+ "insert",
63
+ "createIndex",
64
+ "count"
65
+ ];
66
+ var WRITE_OPERATIONS = ["update", "insert", "createIndex"];
67
+ async function handleCallToolRequest({
68
+ request,
69
+ client,
70
+ db,
71
+ isReadOnlyMode,
72
+ signal,
73
+ taskStore,
74
+ taskTtl
75
+ }) {
76
+ const { name, arguments: args = {} } = request.params;
77
+ const operation = name;
78
+ const objectIdMode = args.objectIdMode || "auto";
79
+ const filteredArgs = {};
80
+ for (const [key, value] of Object.entries(args)) {
81
+ if (key !== "objectIdMode") {
82
+ filteredArgs[key] = value;
33
83
  }
34
- // Ensure connection URL has the correct prefix
35
- if (!connectionUrl.startsWith("mongodb://") &&
36
- !connectionUrl.startsWith("mongodb+srv://")) {
37
- console.error("Invalid MongoDB connection URL. URL must start with 'mongodb://' or 'mongodb+srv://'");
38
- process.exit(1);
84
+ }
85
+ if (args.sort) {
86
+ args.sort = parseSort(args.sort);
87
+ }
88
+ Object.assign(args, filteredArgs);
89
+ validateOperation(operation);
90
+ try {
91
+ checkReadOnlyMode(operation, isReadOnlyMode);
92
+ let collection = null;
93
+ if (COLLECTION_OPERATIONS.includes(operation)) {
94
+ const collectionName = args.collection;
95
+ if (!collectionName) {
96
+ throw new Error(
97
+ `Collection name is required for '${operation}' operation`
98
+ );
99
+ }
100
+ collection = db.collection(collectionName);
101
+ validateCollection(collection);
102
+ }
103
+ signal?.throwIfAborted();
104
+ if (taskStore) {
105
+ const task = await taskStore.createTask({
106
+ ttl: taskTtl ?? void 0
107
+ });
108
+ (async () => {
109
+ try {
110
+ const result = await executeOperation(
111
+ operation,
112
+ collection,
113
+ db,
114
+ isReadOnlyMode,
115
+ args,
116
+ objectIdMode,
117
+ signal
118
+ );
119
+ await taskStore.storeTaskResult(task.taskId, "completed", result);
120
+ } catch (error) {
121
+ try {
122
+ const message = error instanceof Error ? error.message : "Unknown error";
123
+ await taskStore.storeTaskResult(task.taskId, "failed", {
124
+ content: [{ type: "text", text: message }],
125
+ isError: true
126
+ });
127
+ } catch {
128
+ }
129
+ }
130
+ })();
131
+ return { task };
132
+ }
133
+ return await executeOperation(
134
+ operation,
135
+ collection,
136
+ db,
137
+ isReadOnlyMode,
138
+ args,
139
+ objectIdMode,
140
+ signal
141
+ );
142
+ } catch (error) {
143
+ if (error instanceof McpError || signal?.aborted) {
144
+ throw error;
39
145
  }
146
+ return toolExecutionError(
147
+ error instanceof Error ? error.message : "Unknown error"
148
+ );
149
+ }
150
+ }
151
+ async function executeOperation(operation, collection, db, isReadOnlyMode, args, objectIdMode, signal) {
152
+ switch (operation) {
153
+ case "query":
154
+ return handleQuery(collection, args, objectIdMode, signal);
155
+ case "aggregate":
156
+ return handleAggregate(collection, args, objectIdMode, signal);
157
+ case "update":
158
+ return handleUpdate(collection, args, objectIdMode, signal);
159
+ case "serverInfo":
160
+ return handleServerInfo(db, isReadOnlyMode, args, signal);
161
+ case "insert":
162
+ return handleInsert(collection, args, objectIdMode, signal);
163
+ case "createIndex":
164
+ return handleCreateIndex(collection, args, objectIdMode, signal);
165
+ case "count":
166
+ return handleCount(collection, args, objectIdMode, signal);
167
+ case "listCollections":
168
+ return handleListCollections(db, args, objectIdMode, signal);
169
+ default:
170
+ throw new McpError(ErrorCode.InvalidParams, `Unknown tool: ${operation}`);
171
+ }
172
+ }
173
+ function validateOperation(operation) {
174
+ const validOperations = [
175
+ "query",
176
+ "aggregate",
177
+ "update",
178
+ "serverInfo",
179
+ "insert",
180
+ "createIndex",
181
+ "count",
182
+ "listCollections"
183
+ ];
184
+ if (!validOperations.includes(operation)) {
185
+ throw new McpError(ErrorCode.InvalidParams, `Unknown tool: ${operation}`);
186
+ }
187
+ }
188
+ function validateCollection(collection) {
189
+ if (!collection.collectionName) {
190
+ throw new Error("Collection name cannot be empty");
191
+ }
192
+ if (collection.collectionName.startsWith("system.")) {
193
+ throw new Error("Access to system collections is not allowed");
194
+ }
195
+ }
196
+ function checkReadOnlyMode(operation, isReadOnlyMode) {
197
+ if (isReadOnlyMode && WRITE_OPERATIONS.includes(operation)) {
198
+ throw new Error(
199
+ `ReadonlyError: Operation '${operation}' is not allowed in read-only mode`
200
+ );
201
+ }
202
+ }
203
+ function parseSort(sort) {
204
+ if (!sort) return null;
205
+ if (typeof sort !== "object" || sort === null || Array.isArray(sort)) {
206
+ return null;
207
+ }
208
+ const validSort = {};
209
+ for (const [key, value] of Object.entries(sort)) {
210
+ if (typeof value === "number" && (value === 1 || value === -1)) {
211
+ validSort[key] = value;
212
+ }
213
+ }
214
+ return Object.keys(validSort).length > 0 ? validSort : null;
215
+ }
216
+ function parseFilter(filter, objectIdMode = "auto") {
217
+ if (!filter) {
218
+ return {};
219
+ }
220
+ if (typeof filter === "string") {
40
221
  try {
41
- const { client, db, isConnected, isReadOnlyMode } = await connectToMongoDB(connectionUrl, readOnlyMode);
42
- // Store client in global variable for cleanup
43
- mongoClient = client;
44
- if (!isConnected || !client || !db) {
45
- console.error("Failed to connect to MongoDB");
46
- process.exit(1);
47
- }
48
- // Pass db instead of client to createServer
49
- const server = createServer(client, db, isReadOnlyMode);
50
- const transport = new StdioServerTransport();
51
- await server.connect(transport);
52
- console.warn("Server connected successfully");
53
- }
54
- catch (error) {
55
- console.error("Failed to connect to MongoDB:", error);
56
- if (mongoClient) {
57
- await mongoClient.close();
222
+ return processObjectIdInFilter(JSON.parse(filter), objectIdMode);
223
+ } catch {
224
+ throw new Error("Invalid filter format: must be a valid JSON object");
225
+ }
226
+ }
227
+ if (typeof filter === "object" && filter !== null && !Array.isArray(filter)) {
228
+ return processObjectIdInFilter(
229
+ filter,
230
+ objectIdMode
231
+ );
232
+ }
233
+ throw new Error("Query filter must be a plain object or ObjectId");
234
+ }
235
+ function isObjectIdField(fieldName) {
236
+ const lowerFieldName = fieldName.toLowerCase();
237
+ return lowerFieldName === "_id" || lowerFieldName === "id" || lowerFieldName.endsWith("id") || lowerFieldName.endsWith("_id");
238
+ }
239
+ function processObjectIdInFilter(filter, objectIdMode = "auto") {
240
+ if (objectIdMode === "none") {
241
+ const result2 = {};
242
+ for (const [key, value] of Object.entries(filter)) {
243
+ if (typeof value === "string" && isISODateString(value)) {
244
+ result2[key] = new Date(value);
245
+ } else if (typeof value === "string" && value.startsWith("ISODate(") && value.endsWith(")")) {
246
+ const dateString = value.substring(8, value.length - 2);
247
+ if (isISODateString(dateString)) {
248
+ result2[key] = new Date(dateString);
249
+ } else {
250
+ result2[key] = value;
58
251
  }
59
- process.exit(1);
252
+ } else if (typeof value === "object" && value !== null) {
253
+ if (Array.isArray(value)) {
254
+ result2[key] = value.map((item) => {
255
+ if (typeof item === "string" && isISODateString(item)) {
256
+ return new Date(item);
257
+ } else if (typeof item === "string" && item.startsWith("ISODate(") && item.endsWith(")")) {
258
+ const dateString = item.substring(8, item.length - 2);
259
+ return isISODateString(dateString) ? new Date(dateString) : item;
260
+ }
261
+ return item;
262
+ });
263
+ } else {
264
+ result2[key] = processObjectIdInFilter(
265
+ value,
266
+ "none"
267
+ );
268
+ }
269
+ } else {
270
+ result2[key] = value;
271
+ }
272
+ }
273
+ return result2;
274
+ }
275
+ const result = {};
276
+ for (const [key, value] of Object.entries(filter)) {
277
+ if (typeof value === "string" && isObjectIdString(value)) {
278
+ if (objectIdMode === "force" || objectIdMode === "auto" && isObjectIdField(key)) {
279
+ result[key] = new ObjectId(value);
280
+ } else {
281
+ result[key] = value;
282
+ }
283
+ } else if (typeof value === "string" && isISODateString(value)) {
284
+ result[key] = new Date(value);
285
+ } else if (typeof value === "string" && value.startsWith("ISODate(") && value.endsWith(")")) {
286
+ const dateString = value.substring(8, value.length - 2);
287
+ if (isISODateString(dateString)) {
288
+ result[key] = new Date(dateString);
289
+ } else {
290
+ result[key] = value;
291
+ }
292
+ } else if (typeof value === "object" && value !== null) {
293
+ if (Array.isArray(value)) {
294
+ result[key] = value.map((item) => {
295
+ if (typeof item === "string" && isObjectIdString(item) && (objectIdMode === "force" || objectIdMode === "auto" && isObjectIdField(key))) {
296
+ return new ObjectId(item);
297
+ } else if (typeof item === "string" && isISODateString(item)) {
298
+ return new Date(item);
299
+ } else if (typeof item === "string" && item.startsWith("ISODate(") && item.endsWith(")")) {
300
+ const dateString = item.substring(8, item.length - 2);
301
+ return isISODateString(dateString) ? new Date(dateString) : item;
302
+ }
303
+ return item;
304
+ });
305
+ } else {
306
+ result[key] = processObjectIdInFilter(
307
+ value,
308
+ objectIdMode
309
+ );
310
+ }
311
+ } else {
312
+ result[key] = value;
60
313
  }
314
+ }
315
+ return result;
61
316
  }
62
- // Handle cleanup
63
- process.on("SIGINT", async () => {
317
+ function isObjectIdString(str) {
318
+ return /^[0-9a-fA-F]{24}$/.test(str);
319
+ }
320
+ function isISODateString(str) {
321
+ return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,3})?Z$/.test(str);
322
+ }
323
+ function formatResponse(data) {
324
+ return {
325
+ content: [
326
+ {
327
+ type: "text",
328
+ text: JSON.stringify(data, null, 2)
329
+ }
330
+ ]
331
+ };
332
+ }
333
+ function toolExecutionError(message) {
334
+ return {
335
+ content: [{ type: "text", text: message }],
336
+ isError: true
337
+ };
338
+ }
339
+ function handleError(error, operation, collectionName) {
340
+ if (error instanceof Error && error.name === "AbortError") {
341
+ throw error;
342
+ }
343
+ const context = collectionName ? `collection ${collectionName}` : "operation";
344
+ const message = error instanceof Error ? error.message : "Unknown error";
345
+ return toolExecutionError(`Failed to ${operation} ${context}: ${message}`);
346
+ }
347
+ async function handleQuery(collection, args, objectIdMode = "auto", signal) {
348
+ if (!collection) {
349
+ throw new Error("Collection is required for query operation");
350
+ }
351
+ const { filter, projection, explain, sort } = args;
352
+ const limit = args.limit || 10;
353
+ const skip = args.skip || 0;
354
+ const queryFilter = parseFilter(filter, objectIdMode);
355
+ try {
356
+ if (explain) {
357
+ signal?.throwIfAborted();
358
+ const explainResult = await collection.find(queryFilter, {
359
+ projection,
360
+ limit,
361
+ skip,
362
+ sort
363
+ }).explain(explain);
364
+ return formatResponse(explainResult);
365
+ }
366
+ signal?.throwIfAborted();
367
+ const [total, results] = await Promise.all([
368
+ collection.countDocuments(queryFilter),
369
+ collection.find(queryFilter, {
370
+ projection,
371
+ limit,
372
+ skip,
373
+ sort
374
+ }).toArray()
375
+ ]);
376
+ return formatResponse({
377
+ results,
378
+ metadata: {
379
+ total,
380
+ returned: results.length,
381
+ skip,
382
+ limit,
383
+ hasMore: skip + results.length < total
384
+ }
385
+ });
386
+ } catch (error) {
387
+ return handleError(error, "query", collection.collectionName);
388
+ }
389
+ }
390
+ async function handleAggregate(collection, args, objectIdMode = "auto", signal) {
391
+ if (!collection) {
392
+ throw new Error("Collection is required for aggregate operation");
393
+ }
394
+ const { pipeline, explain } = args;
395
+ if (!Array.isArray(pipeline)) {
396
+ throw new Error("Pipeline must be an array");
397
+ }
398
+ const processedPipeline = pipeline.map((stage) => {
399
+ if (typeof stage === "object" && stage !== null) {
400
+ return processObjectIdInFilter(
401
+ stage,
402
+ objectIdMode
403
+ );
404
+ }
405
+ return stage;
406
+ });
407
+ try {
408
+ if (explain) {
409
+ signal?.throwIfAborted();
410
+ const explainResult = await collection.aggregate(processedPipeline, {
411
+ explain: {
412
+ verbosity: explain
413
+ }
414
+ }).toArray();
415
+ return formatResponse(explainResult);
416
+ }
417
+ signal?.throwIfAborted();
418
+ const results = await collection.aggregate(processedPipeline).toArray();
419
+ return formatResponse({
420
+ results,
421
+ metadata: {
422
+ returned: results.length
423
+ }
424
+ });
425
+ } catch (error) {
426
+ return handleError(error, "aggregate", collection.collectionName);
427
+ }
428
+ }
429
+ async function handleUpdate(collection, args, objectIdMode = "auto", signal) {
430
+ if (!collection) {
431
+ throw new Error("Collection is required for update operation");
432
+ }
433
+ const { filter, update, upsert, multi } = args;
434
+ const queryFilter = parseFilter(filter, objectIdMode);
435
+ let processedUpdate = update;
436
+ if (update && typeof update === "object" && !Array.isArray(update)) {
437
+ processedUpdate = processObjectIdInFilter(
438
+ update,
439
+ objectIdMode
440
+ );
441
+ }
442
+ if (!processedUpdate || typeof processedUpdate !== "object" || Array.isArray(processedUpdate)) {
443
+ throw new Error("Update must be a valid MongoDB update document");
444
+ }
445
+ const validUpdateOperators = [
446
+ "$set",
447
+ "$unset",
448
+ "$inc",
449
+ "$push",
450
+ "$pull",
451
+ "$addToSet",
452
+ "$pop",
453
+ "$rename",
454
+ "$mul"
455
+ ];
456
+ const hasValidOperator = Object.keys(processedUpdate).some(
457
+ (key) => validUpdateOperators.includes(key)
458
+ );
459
+ if (!hasValidOperator) {
460
+ throw new Error(
461
+ "Update must include at least one valid update operator ($set, $unset, etc.)"
462
+ );
463
+ }
464
+ try {
465
+ const options = {
466
+ upsert: !!upsert,
467
+ multi: !!multi
468
+ };
469
+ const updateMethod = options.multi ? "updateMany" : "updateOne";
470
+ signal?.throwIfAborted();
471
+ const result = await collection[updateMethod](
472
+ queryFilter,
473
+ processedUpdate,
474
+ options
475
+ );
476
+ return formatResponse({
477
+ matchedCount: result.matchedCount,
478
+ modifiedCount: result.modifiedCount,
479
+ upsertedCount: result.upsertedCount,
480
+ upsertedId: result.upsertedId
481
+ });
482
+ } catch (error) {
483
+ return handleError(error, "update", collection.collectionName);
484
+ }
485
+ }
486
+ async function handleServerInfo(db, isReadOnlyMode, args, signal) {
487
+ const { includeDebugInfo } = args;
488
+ try {
489
+ signal?.throwIfAborted();
490
+ const buildInfo = await db.command({ buildInfo: 1 });
491
+ let serverStatus = null;
492
+ if (includeDebugInfo) {
493
+ signal?.throwIfAborted();
494
+ serverStatus = await db.command({ serverStatus: 1 });
495
+ }
496
+ const serverInfo = {
497
+ version: buildInfo.version,
498
+ gitVersion: buildInfo.gitVersion,
499
+ modules: buildInfo.modules,
500
+ allocator: buildInfo.allocator,
501
+ javascriptEngine: buildInfo.javascriptEngine,
502
+ sysInfo: buildInfo.sysInfo,
503
+ storageEngines: buildInfo.storageEngines,
504
+ debug: buildInfo.debug,
505
+ maxBsonObjectSize: buildInfo.maxBsonObjectSize,
506
+ openssl: buildInfo.openssl,
507
+ buildEnvironment: buildInfo.buildEnvironment,
508
+ bits: buildInfo.bits,
509
+ ok: buildInfo.ok,
510
+ status: {},
511
+ connectionInfo: {
512
+ readOnlyMode: isReadOnlyMode,
513
+ readPreference: isReadOnlyMode ? "secondary" : "primary"
514
+ }
515
+ };
516
+ if (serverStatus) {
517
+ serverInfo.status = {
518
+ host: serverStatus.host,
519
+ version: serverStatus.version,
520
+ process: serverStatus.process,
521
+ pid: serverStatus.pid,
522
+ uptime: serverStatus.uptime,
523
+ uptimeMillis: serverStatus.uptimeMillis,
524
+ uptimeEstimate: serverStatus.uptimeEstimate,
525
+ localTime: serverStatus.localTime,
526
+ connections: serverStatus.connections,
527
+ network: serverStatus.network,
528
+ memory: serverStatus.mem,
529
+ storageEngine: serverStatus.storageEngine,
530
+ security: serverStatus.security
531
+ };
532
+ }
533
+ return formatResponse(serverInfo);
534
+ } catch (error) {
535
+ return handleError(error, "get server information");
536
+ }
537
+ }
538
+ async function handleInsert(collection, args, objectIdMode = "auto", signal) {
539
+ if (!collection) {
540
+ throw new Error("Collection is required for insert operation");
541
+ }
542
+ const { documents, ordered, writeConcern, bypassDocumentValidation } = args;
543
+ if (!Array.isArray(documents)) {
544
+ throw new Error("Documents must be an array");
545
+ }
546
+ if (documents.length === 0) {
547
+ throw new Error("Documents array cannot be empty");
548
+ }
549
+ if (!documents.every(
550
+ (doc) => doc && typeof doc === "object" && !Array.isArray(doc)
551
+ )) {
552
+ throw new Error("Each document must be a valid MongoDB document object");
553
+ }
554
+ const processedDocuments = documents.map(
555
+ (doc) => processObjectIdInFilter(doc, objectIdMode)
556
+ );
557
+ try {
558
+ const options = {
559
+ ordered: ordered !== false,
560
+ // default to true if not specified
561
+ writeConcern,
562
+ bypassDocumentValidation
563
+ };
564
+ signal?.throwIfAborted();
565
+ const result = await collection.insertMany(
566
+ processedDocuments,
567
+ options
568
+ );
569
+ return formatResponse({
570
+ acknowledged: result.acknowledged,
571
+ insertedCount: result.insertedCount,
572
+ insertedIds: result.insertedIds
573
+ });
574
+ } catch (error) {
575
+ if (error instanceof Error && error.name === "BulkWriteError") {
576
+ const bulkError = error;
577
+ return formatResponse({
578
+ error: "Bulk write error occurred",
579
+ writeErrors: bulkError.writeErrors,
580
+ insertedCount: bulkError.result?.nInserted || 0,
581
+ failedCount: bulkError.result?.nFailedInserts || 0
582
+ });
583
+ }
584
+ return handleError(error, "insert", collection.collectionName);
585
+ }
586
+ }
587
+ async function handleCreateIndex(collection, args, objectIdMode = "auto", signal) {
588
+ if (!collection) {
589
+ throw new Error("Collection is required for createIndex operation");
590
+ }
591
+ const { indexes, commitQuorum, writeConcern } = args;
592
+ if (!Array.isArray(indexes) || indexes.length === 0) {
593
+ throw new Error("Indexes must be a non-empty array");
594
+ }
595
+ if (writeConcern && (typeof writeConcern !== "object" || Array.isArray(writeConcern))) {
596
+ throw new Error(
597
+ "Write concern must be a valid MongoDB write concern object"
598
+ );
599
+ }
600
+ if (commitQuorum && typeof commitQuorum !== "string" && typeof commitQuorum !== "number") {
601
+ throw new Error("Commit quorum must be a string or number");
602
+ }
603
+ const processedIndexes = indexes.map((index) => {
604
+ if (index && typeof index === "object") {
605
+ return processObjectIdInFilter(
606
+ index,
607
+ objectIdMode
608
+ );
609
+ }
610
+ return index;
611
+ });
612
+ try {
613
+ const indexOptions = {
614
+ commitQuorum: typeof commitQuorum === "number" ? commitQuorum : void 0
615
+ };
616
+ signal?.throwIfAborted();
617
+ const result = await collection.createIndexes(
618
+ processedIndexes,
619
+ indexOptions
620
+ );
621
+ return formatResponse({
622
+ acknowledged: result.acknowledged,
623
+ createdIndexes: result.createdIndexes,
624
+ numIndexesBefore: result.numIndexesBefore,
625
+ numIndexesAfter: result.numIndexesAfter
626
+ });
627
+ } catch (error) {
628
+ return handleError(error, "create indexes", collection.collectionName);
629
+ }
630
+ }
631
+ async function handleCount(collection, args, objectIdMode = "auto", signal) {
632
+ if (!collection) {
633
+ throw new Error("Collection is required for count operation");
634
+ }
635
+ const { query, limit, skip, hint, readConcern, maxTimeMS, collation } = args;
636
+ const countQuery = parseFilter(query, objectIdMode);
637
+ try {
638
+ const options = {
639
+ limit: typeof limit === "number" ? limit : void 0,
640
+ skip: typeof skip === "number" ? skip : void 0,
641
+ hint: typeof hint === "object" && hint !== null ? hint : void 0,
642
+ readConcern: typeof readConcern === "object" && readConcern !== null ? readConcern : void 0,
643
+ maxTimeMS: typeof maxTimeMS === "number" ? maxTimeMS : void 0,
644
+ collation: typeof collation === "object" && collation !== null ? collation : void 0
645
+ };
646
+ for (const key of Object.keys(options)) {
647
+ if (options[key] === void 0) {
648
+ delete options[key];
649
+ }
650
+ }
651
+ signal?.throwIfAborted();
652
+ const count = await collection.countDocuments(countQuery, options);
653
+ return formatResponse({
654
+ count,
655
+ ok: 1
656
+ });
657
+ } catch (error) {
658
+ return handleError(error, "count documents", collection.collectionName);
659
+ }
660
+ }
661
+ async function handleListCollections(db, args, objectIdMode = "auto", signal) {
662
+ const { nameOnly, filter } = args;
663
+ const skip = args.skip || 0;
664
+ const limit = args.limit || 20;
665
+ let processedFilter = filter;
666
+ if (filter && typeof filter === "object") {
667
+ processedFilter = processObjectIdInFilter(
668
+ filter,
669
+ objectIdMode
670
+ );
671
+ }
672
+ try {
673
+ const options = processedFilter ? { filter: processedFilter } : {};
674
+ signal?.throwIfAborted();
675
+ const collections = await db.listCollections(options).toArray();
676
+ const allResults = nameOnly ? collections.map((collection) => collection.name) : collections;
677
+ const total = allResults.length;
678
+ const paged = allResults.slice(skip, skip + limit);
679
+ return formatResponse({
680
+ results: paged,
681
+ metadata: {
682
+ total,
683
+ returned: paged.length,
684
+ skip,
685
+ limit,
686
+ hasMore: skip + paged.length < total
687
+ }
688
+ });
689
+ } catch (error) {
690
+ return handleError(error, "list collections");
691
+ }
692
+ }
693
+
694
+ // src/utils/pagination.ts
695
+ var DEFAULT_PAGE_SIZE = 20;
696
+ function encodeCursor(offset) {
697
+ const data = { offset };
698
+ return Buffer.from(JSON.stringify(data)).toString("base64");
699
+ }
700
+ function decodeCursor(cursor) {
701
+ if (!cursor) return 0;
702
+ try {
703
+ const data = JSON.parse(
704
+ Buffer.from(cursor, "base64").toString("utf-8")
705
+ );
706
+ return typeof data.offset === "number" && data.offset >= 0 ? data.offset : 0;
707
+ } catch {
708
+ return 0;
709
+ }
710
+ }
711
+ function paginate(items, cursor, pageSize = DEFAULT_PAGE_SIZE) {
712
+ const offset = decodeCursor(cursor);
713
+ const page = items.slice(offset, offset + pageSize);
714
+ const nextOffset = offset + pageSize;
715
+ const nextCursor = nextOffset < items.length ? encodeCursor(nextOffset) : void 0;
716
+ return { items: page, nextCursor };
717
+ }
718
+
719
+ // src/schemas/completion.ts
720
+ async function handleCompletionRequest({
721
+ request,
722
+ client,
723
+ db,
724
+ isReadOnlyMode,
725
+ signal
726
+ }) {
727
+ const { ref, argument } = request.params;
728
+ if (ref.type === "ref/prompt") {
729
+ return handlePromptCompletion(
730
+ client,
731
+ db,
732
+ isReadOnlyMode,
733
+ ref.name,
734
+ argument,
735
+ signal
736
+ );
737
+ }
738
+ if (ref.type === "ref/resource") {
739
+ return handleResourceCompletion(
740
+ client,
741
+ db,
742
+ isReadOnlyMode,
743
+ ref.uri,
744
+ argument,
745
+ signal
746
+ );
747
+ }
748
+ return emptyCompletionResult();
749
+ }
750
+ async function handlePromptCompletion(client, db, isReadOnlyMode, promptName, argument, signal) {
751
+ if (!promptName) {
752
+ return emptyCompletionResult();
753
+ }
754
+ if (argument.name === "collection") {
755
+ return await completeCollectionNames(
756
+ argument.value,
757
+ db,
758
+ isReadOnlyMode,
759
+ signal
760
+ );
761
+ }
762
+ return emptyCompletionResult();
763
+ }
764
+ async function handleResourceCompletion(client, db, isReadOnlyMode, promptName, argument, signal) {
765
+ if (!promptName) {
766
+ return emptyCompletionResult();
767
+ }
768
+ if (argument.name === "collection") {
769
+ return await completeCollectionNames(
770
+ argument.value,
771
+ db,
772
+ isReadOnlyMode,
773
+ signal
774
+ );
775
+ }
776
+ return emptyCompletionResult();
777
+ }
778
+ async function completeCollectionNames(partialValue, db, isReadOnlyMode, signal) {
779
+ try {
780
+ console.warn(
781
+ `Completing collection names with partial value: ${partialValue}`
782
+ );
783
+ signal?.throwIfAborted();
784
+ const collections = await db.listCollections().toArray();
785
+ const matchingCollections = collections.map(
786
+ (c) => c.name
787
+ ).filter(
788
+ (name) => (
789
+ // Filter out system collections
790
+ !name.startsWith("system.") && // Match partial value
791
+ name.toLowerCase().includes(partialValue.toLowerCase())
792
+ )
793
+ ).sort();
794
+ console.warn(`Found ${matchingCollections.length} matching collections`);
795
+ const PAGE_SIZE = 100;
796
+ const { items: limitedResults, nextCursor } = paginate(
797
+ matchingCollections,
798
+ void 0,
799
+ PAGE_SIZE
800
+ );
801
+ return {
802
+ completion: {
803
+ values: limitedResults,
804
+ total: matchingCollections.length,
805
+ hasMore: !!nextCursor
806
+ }
807
+ };
808
+ } catch (error) {
809
+ console.error("Error completing collection names:", error);
810
+ return emptyCompletionResult();
811
+ }
812
+ }
813
+ function emptyCompletionResult() {
814
+ return {
815
+ completion: {
816
+ values: [],
817
+ total: 0,
818
+ hasMore: false
819
+ }
820
+ };
821
+ }
822
+
823
+ // src/schemas/ping.ts
824
+ async function handlePingRequest({
825
+ request,
826
+ client,
827
+ db,
828
+ isReadOnlyMode,
829
+ signal
830
+ }) {
831
+ try {
832
+ if (!client) {
833
+ throw new Error("MongoDB connection is not available");
834
+ }
835
+ signal?.throwIfAborted();
836
+ const pong = await db.command({ ping: 1 });
837
+ if (pong.ok !== 1) {
838
+ throw new Error(`MongoDB ping failed: ${pong.errmsg}`);
839
+ }
840
+ return {};
841
+ } catch (error) {
842
+ if (error instanceof Error) {
843
+ throw new Error(`MongoDB ping failed: ${error.message}`);
844
+ }
845
+ throw new Error("MongoDB ping failed: Unknown error");
846
+ }
847
+ }
848
+
849
+ // src/schemas/prompts.ts
850
+ async function handleListPromptsRequest({
851
+ request,
852
+ client,
853
+ db,
854
+ isReadOnlyMode,
855
+ signal
856
+ }) {
857
+ return {
858
+ prompts: [
859
+ {
860
+ name: "analyze_collection",
861
+ description: "Analyze a MongoDB collection structure and contents",
862
+ arguments: [
863
+ {
864
+ name: "collection",
865
+ description: "Name of the collection to analyze",
866
+ required: true
867
+ }
868
+ ]
869
+ }
870
+ ]
871
+ };
872
+ }
873
+ async function handleGetPromptRequest({
874
+ request,
875
+ client,
876
+ db,
877
+ isReadOnlyMode,
878
+ signal,
879
+ sendProgress
880
+ }) {
881
+ const { name, arguments: args = {} } = request.params;
882
+ if (name !== "analyze_collection") {
883
+ throw new Error("Unknown prompt");
884
+ }
885
+ const collectionName = args.collection;
886
+ if (!collectionName) {
887
+ throw new Error("Collection name is required");
888
+ }
889
+ try {
890
+ const collection = db.collection(collectionName);
891
+ if (collection.collectionName.startsWith("system.")) {
892
+ throw new Error("Access to system collections is not allowed");
893
+ }
894
+ await sendProgress?.(1, 3, "Getting schema");
895
+ signal?.throwIfAborted();
896
+ const schemaSample = await collection.findOne({});
897
+ await sendProgress?.(2, 3, "Getting stats");
898
+ signal?.throwIfAborted();
899
+ const stats = await collection.aggregate([{ $collStats: { count: {} } }]).toArray();
900
+ await sendProgress?.(3, 3, "Fetching samples");
901
+ signal?.throwIfAborted();
902
+ const sampleDocs = await collection.find({}).limit(5).toArray();
903
+ const documentCount = stats[0]?.count ?? "unknown";
904
+ return {
905
+ messages: [
906
+ {
907
+ role: "user",
908
+ content: {
909
+ type: "text",
910
+ text: `Please analyze the following MongoDB collection:
911
+ Collection: ${collectionName}
912
+
913
+ Schema:
914
+ ${JSON.stringify(schemaSample, null, 2)}
915
+
916
+ Stats:
917
+ Document count: ${documentCount}
918
+
919
+ Sample documents:
920
+ ${JSON.stringify(sampleDocs, null, 2)}`
921
+ }
922
+ },
923
+ {
924
+ role: "user",
925
+ content: {
926
+ type: "text",
927
+ text: "Provide insights about the collection's structure, data types, and basic statistics."
928
+ }
929
+ }
930
+ ]
931
+ };
932
+ } catch (error) {
933
+ const msg = error instanceof Error ? error.message : "Unknown error";
934
+ throw new Error(`Failed to analyze collection ${collectionName}: ${msg}`);
935
+ }
936
+ }
937
+
938
+ // src/schemas/resource.ts
939
+ import { ObjectId as ObjectId2 } from "mongodb";
940
+ function detectMongoType(value) {
941
+ if (value === null) return "null";
942
+ if (value === void 0) return "undefined";
943
+ if (value instanceof ObjectId2) return "ObjectId";
944
+ if (value instanceof Date) return "Date";
945
+ if (Array.isArray(value)) {
946
+ if (value.length === 0) return "Array";
947
+ const elementTypes = new Set(value.map((item) => detectMongoType(item)));
948
+ if (elementTypes.size === 1) {
949
+ return `Array<${Array.from(elementTypes)[0]}>`;
950
+ }
951
+ return "Array<mixed>";
952
+ }
953
+ if (typeof value === "object") {
954
+ return "Document";
955
+ }
956
+ return typeof value;
957
+ }
958
+ function inferSchemaFromSamples(documents) {
959
+ if (!documents || documents.length === 0) {
960
+ return { fields: [] };
961
+ }
962
+ const fieldMap = /* @__PURE__ */ new Map();
963
+ for (const doc of documents) {
964
+ for (const [key, value] of Object.entries(doc)) {
965
+ if (!fieldMap.has(key)) {
966
+ fieldMap.set(key, {
967
+ name: key,
968
+ types: /* @__PURE__ */ new Set([detectMongoType(value)]),
969
+ nullable: false,
970
+ // Store sample values for complex types
971
+ samples: [value]
972
+ });
973
+ } else {
974
+ const fieldInfo = fieldMap.get(key);
975
+ fieldInfo.types.add(detectMongoType(value));
976
+ if (fieldInfo.samples.length < 3 && !fieldInfo.samples.some(
977
+ (sample) => JSON.stringify(sample) === JSON.stringify(value)
978
+ )) {
979
+ fieldInfo.samples.push(value);
980
+ }
981
+ }
982
+ }
983
+ }
984
+ for (const doc of documents) {
985
+ for (const [key] of fieldMap.entries()) {
986
+ if (!(key in doc)) {
987
+ const fieldInfo = fieldMap.get(key);
988
+ fieldInfo.nullable = true;
989
+ }
990
+ }
991
+ }
992
+ for (const [key, fieldInfo] of fieldMap.entries()) {
993
+ if (fieldInfo.types.has("Document")) {
994
+ const nestedDocs = documents.filter(
995
+ (doc) => doc[key] && typeof doc[key] === "object" && !Array.isArray(doc[key])
996
+ ).map((doc) => doc[key]);
997
+ if (nestedDocs.length > 0) {
998
+ fieldInfo.nestedSchema = inferSchemaFromSamples(nestedDocs);
999
+ }
1000
+ }
1001
+ }
1002
+ const fields = Array.from(fieldMap.values()).map((fieldInfo) => {
1003
+ const result = {
1004
+ name: fieldInfo.name,
1005
+ types: Array.from(fieldInfo.types),
1006
+ nullable: fieldInfo.nullable,
1007
+ prevalence: `${Math.round(
1008
+ documents.filter((doc) => fieldInfo.name in doc).length / documents.length * 100
1009
+ )}%`,
1010
+ examples: []
1011
+ };
1012
+ if (fieldInfo.nestedSchema) {
1013
+ result.nestedSchema = fieldInfo.nestedSchema;
1014
+ }
1015
+ const sampleValues = fieldInfo.samples.map((sample) => {
1016
+ if (sample instanceof ObjectId2) return sample.toString();
1017
+ if (sample instanceof Date) return sample.toISOString();
1018
+ if (typeof sample === "object") {
1019
+ return Array.isArray(sample) ? "[...]" : "{...}";
1020
+ }
1021
+ return sample;
1022
+ });
1023
+ result.examples = sampleValues;
1024
+ return result;
1025
+ });
1026
+ return { fields };
1027
+ }
1028
+ async function handleReadResourceRequest({
1029
+ request,
1030
+ client,
1031
+ db,
1032
+ isReadOnlyMode,
1033
+ signal,
1034
+ sendProgress
1035
+ }) {
1036
+ const url = new URL(request.params.uri);
1037
+ const collectionName = url.pathname.replace(/^\//, "");
1038
+ try {
1039
+ const collection = db.collection(collectionName);
1040
+ const sampleSize = 100;
1041
+ let sampleDocuments = [];
1042
+ await sendProgress?.(1, 4, "Sampling documents");
1043
+ try {
1044
+ signal?.throwIfAborted();
1045
+ sampleDocuments = await collection.aggregate([{ $sample: { size: sampleSize } }]).toArray();
1046
+ } catch (sampleError) {
1047
+ console.warn(
1048
+ `$sample aggregation failed for ${collectionName}, falling back to sequential scan: ${sampleError}`
1049
+ );
1050
+ signal?.throwIfAborted();
1051
+ sampleDocuments = await collection.find({}).limit(sampleSize).toArray();
1052
+ }
1053
+ await sendProgress?.(2, 4, "Getting indexes");
1054
+ signal?.throwIfAborted();
1055
+ const indexes = await collection.indexes();
1056
+ const inferredSchema = inferSchemaFromSamples(sampleDocuments);
1057
+ await sendProgress?.(3, 4, "Counting documents");
1058
+ let documentCount = null;
1059
+ try {
1060
+ signal?.throwIfAborted();
1061
+ documentCount = await Promise.race([
1062
+ collection.countDocuments(),
1063
+ new Promise(
1064
+ (_, reject) => setTimeout(
1065
+ () => reject(new Error("Count operation timed out")),
1066
+ 5e3
1067
+ )
1068
+ )
1069
+ ]);
1070
+ } catch (countError) {
1071
+ console.warn(
1072
+ `Count operation failed or timed out for ${collectionName}: ${countError}`
1073
+ );
1074
+ try {
1075
+ signal?.throwIfAborted();
1076
+ const stats = await db.command({ collStats: collectionName });
1077
+ documentCount = stats.count;
1078
+ } catch {
1079
+ documentCount = "unknown (count operation timed out)";
1080
+ }
1081
+ }
1082
+ await sendProgress?.(4, 4, "Done");
1083
+ const schema = {
1084
+ type: "collection",
1085
+ name: collectionName,
1086
+ fields: inferredSchema.fields,
1087
+ indexes: indexes.map((idx) => ({
1088
+ name: idx.name,
1089
+ keys: idx.key
1090
+ })),
1091
+ documentCount,
1092
+ sampleSize: sampleDocuments.length,
1093
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
1094
+ };
1095
+ return {
1096
+ contents: [
1097
+ {
1098
+ uri: request.params.uri,
1099
+ mimeType: "application/json",
1100
+ text: JSON.stringify(schema, null, 2)
1101
+ }
1102
+ ]
1103
+ };
1104
+ } catch (error) {
1105
+ if (error instanceof Error) {
1106
+ throw new Error(
1107
+ `Failed to read collection ${collectionName}: ${error.message}`
1108
+ );
1109
+ }
1110
+ throw new Error(
1111
+ `Failed to read collection ${collectionName}: Unknown error`
1112
+ );
1113
+ }
1114
+ }
1115
+ async function handleListResourcesRequest({
1116
+ request,
1117
+ client,
1118
+ db,
1119
+ isReadOnlyMode,
1120
+ signal
1121
+ }) {
1122
+ try {
1123
+ signal?.throwIfAborted();
1124
+ const collections = await db.listCollections().toArray();
1125
+ const allResources = collections.map((collection) => ({
1126
+ uri: `mongodb:///${collection.name}`,
1127
+ mimeType: "application/json",
1128
+ name: collection.name,
1129
+ description: `MongoDB collection: ${collection.name}`
1130
+ }));
1131
+ const cursor = request.params?.cursor;
1132
+ const { items, nextCursor } = paginate(allResources, cursor);
1133
+ return {
1134
+ resources: items,
1135
+ ...nextCursor ? { nextCursor } : {}
1136
+ };
1137
+ } catch (error) {
1138
+ if (error instanceof Error) {
1139
+ throw new Error(`Failed to list collections: ${error.message}`);
1140
+ }
1141
+ throw new Error("Failed to list collections: Unknown error");
1142
+ }
1143
+ }
1144
+
1145
+ // src/schemas/templates.ts
1146
+ async function handleListResourceTemplatesRequest({
1147
+ request,
1148
+ client,
1149
+ db,
1150
+ isReadOnlyMode,
1151
+ signal
1152
+ }) {
1153
+ return {
1154
+ resourceTemplates: [
1155
+ {
1156
+ name: "mongodb_query",
1157
+ description: "Template for constructing MongoDB queries",
1158
+ uriTemplate: "mongodb:///{collection}",
1159
+ text: `To query MongoDB collections, you can use these operators:
1160
+
1161
+ Filter operators:
1162
+ - $eq: Matches values equal to a specified value
1163
+ - $gt/$gte: Matches values greater than (or equal to) a specified value
1164
+ - $lt/$lte: Matches values less than (or equal to) a specified value
1165
+ - $in: Matches any of the values in an array
1166
+ - $nin: Matches none of the values in an array
1167
+ - $ne: Matches values not equal to a specified value
1168
+ - $exists: Matches documents that have the specified field
1169
+
1170
+ Example queries:
1171
+ 1. Find documents where age > 21:
1172
+ { "age": { "$gt": 21 } }
1173
+
1174
+ 2. Find documents with specific status:
1175
+ { "status": { "$in": ["active", "pending"] } }
1176
+
1177
+ 3. Find documents with existing email:
1178
+ { "email": { "$exists": true } }
1179
+
1180
+ 4. Find documents with dates:
1181
+ // Using ISO date string format
1182
+ { "createdAt": { "$gt": "2023-01-01T00:00:00Z" } }
1183
+ // Using ISODate syntax
1184
+ { "createdAt": { "$gt": ISODate("2023-01-01T00:00:00Z") } }
1185
+
1186
+ Use these patterns to construct MongoDB queries.`
1187
+ }
1188
+ ]
1189
+ };
1190
+ }
1191
+
1192
+ // src/schemas/tools.ts
1193
+ async function handleListToolsRequest({
1194
+ request,
1195
+ client,
1196
+ db,
1197
+ isReadOnlyMode,
1198
+ signal
1199
+ }) {
1200
+ return {
1201
+ tools: [
1202
+ {
1203
+ name: "query",
1204
+ description: "Execute a MongoDB query with optional execution plan analysis",
1205
+ execution: { taskSupport: "optional" },
1206
+ inputSchema: {
1207
+ type: "object",
1208
+ properties: {
1209
+ collection: {
1210
+ type: "string",
1211
+ description: "Name of the collection to query"
1212
+ },
1213
+ filter: {
1214
+ type: "object",
1215
+ description: "MongoDB query filter. Supports date strings in ISO format ('2025-01-01T00:00:00Z') and ISODate('2025-01-01T00:00:00Z') notation"
1216
+ },
1217
+ projection: {
1218
+ type: "object",
1219
+ description: "Fields to include/exclude"
1220
+ },
1221
+ limit: {
1222
+ type: "number",
1223
+ description: "Maximum number of documents to return",
1224
+ default: 10
1225
+ },
1226
+ skip: {
1227
+ type: "number",
1228
+ description: "Number of documents to skip before returning results",
1229
+ default: 0
1230
+ },
1231
+ explain: {
1232
+ type: "string",
1233
+ description: "Optional: Get query execution information",
1234
+ enum: ["queryPlanner", "executionStats", "allPlansExecution"]
1235
+ },
1236
+ objectIdMode: {
1237
+ type: "string",
1238
+ description: "Control how 24-character hex strings are handled",
1239
+ enum: ["auto", "none", "force"],
1240
+ default: "auto"
1241
+ }
1242
+ },
1243
+ required: ["collection"]
1244
+ }
1245
+ },
1246
+ {
1247
+ name: "aggregate",
1248
+ description: "Execute a MongoDB aggregation pipeline with optional execution plan analysis",
1249
+ execution: { taskSupport: "optional" },
1250
+ inputSchema: {
1251
+ type: "object",
1252
+ properties: {
1253
+ collection: {
1254
+ type: "string",
1255
+ description: "Name of the collection to aggregate"
1256
+ },
1257
+ pipeline: {
1258
+ type: "array",
1259
+ description: "Aggregation pipeline stages. Supports date strings in ISO format ('2025-01-01T00:00:00Z') and ISODate('2025-01-01T00:00:00Z') notation",
1260
+ items: {
1261
+ type: "object"
1262
+ }
1263
+ },
1264
+ explain: {
1265
+ type: "string",
1266
+ description: "Optional: Get aggregation execution information (queryPlanner, executionStats, or allPlansExecution)",
1267
+ enum: ["queryPlanner", "executionStats", "allPlansExecution"]
1268
+ },
1269
+ objectIdMode: {
1270
+ type: "string",
1271
+ description: "Control how 24-character hex strings are handled",
1272
+ enum: ["auto", "none", "force"],
1273
+ default: "auto"
1274
+ }
1275
+ },
1276
+ required: ["collection", "pipeline"]
1277
+ }
1278
+ },
1279
+ {
1280
+ name: "update",
1281
+ description: "Update documents in a MongoDB collection",
1282
+ execution: { taskSupport: "optional" },
1283
+ inputSchema: {
1284
+ type: "object",
1285
+ properties: {
1286
+ collection: {
1287
+ type: "string",
1288
+ description: "Name of the collection to update"
1289
+ },
1290
+ filter: {
1291
+ type: "object",
1292
+ 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"
1293
+ },
1294
+ update: {
1295
+ type: "object",
1296
+ description: "Update operations to apply ($set, $unset, $inc, etc.)"
1297
+ },
1298
+ upsert: {
1299
+ type: "boolean",
1300
+ description: "Create a new document if no documents match the filter"
1301
+ },
1302
+ multi: {
1303
+ type: "boolean",
1304
+ description: "Update multiple documents that match the filter"
1305
+ },
1306
+ objectIdMode: {
1307
+ type: "string",
1308
+ description: "Control how 24-character hex strings are handled",
1309
+ enum: ["auto", "none", "force"],
1310
+ default: "auto"
1311
+ }
1312
+ },
1313
+ required: ["collection", "filter", "update"]
1314
+ }
1315
+ },
1316
+ {
1317
+ name: "serverInfo",
1318
+ description: "Get MongoDB server information including version, storage engine, and other details",
1319
+ execution: { taskSupport: "optional" },
1320
+ inputSchema: {
1321
+ type: "object",
1322
+ properties: {
1323
+ includeDebugInfo: {
1324
+ type: "boolean",
1325
+ description: "Include additional debug information about the server"
1326
+ }
1327
+ }
1328
+ }
1329
+ },
1330
+ {
1331
+ name: "insert",
1332
+ description: "Insert one or more documents into a MongoDB collection",
1333
+ execution: { taskSupport: "optional" },
1334
+ inputSchema: {
1335
+ type: "object",
1336
+ properties: {
1337
+ collection: {
1338
+ type: "string",
1339
+ description: "Name of the collection to insert into"
1340
+ },
1341
+ documents: {
1342
+ type: "array",
1343
+ description: "Array of documents to insert",
1344
+ items: { type: "object" }
1345
+ },
1346
+ ordered: {
1347
+ type: "boolean",
1348
+ description: "If true, perform ordered insert. If false, insert unordered"
1349
+ },
1350
+ writeConcern: {
1351
+ type: "object",
1352
+ description: "Write concern for the insert operation"
1353
+ },
1354
+ bypassDocumentValidation: {
1355
+ type: "boolean",
1356
+ description: "Allow insert to bypass schema validation"
1357
+ },
1358
+ objectIdMode: {
1359
+ type: "string",
1360
+ description: "Control how 24-character hex strings are handled",
1361
+ enum: ["auto", "none", "force"],
1362
+ default: "auto"
1363
+ }
1364
+ },
1365
+ required: ["collection", "documents"]
1366
+ }
1367
+ },
1368
+ {
1369
+ name: "createIndex",
1370
+ description: "Create one or more indexes on a MongoDB collection",
1371
+ execution: { taskSupport: "optional" },
1372
+ inputSchema: {
1373
+ type: "object",
1374
+ properties: {
1375
+ collection: {
1376
+ type: "string",
1377
+ description: "Name of the collection to create indexes on"
1378
+ },
1379
+ indexes: {
1380
+ type: "array",
1381
+ description: "Array of index specifications",
1382
+ items: {
1383
+ type: "object",
1384
+ properties: {
1385
+ key: {
1386
+ type: "object",
1387
+ description: "Index key pattern, e.g. { field: 1 }"
1388
+ },
1389
+ name: {
1390
+ type: "string",
1391
+ description: "Optional: Name of the index"
1392
+ },
1393
+ unique: {
1394
+ type: "boolean",
1395
+ description: "Optional: Creates a unique index"
1396
+ },
1397
+ sparse: {
1398
+ type: "boolean",
1399
+ description: "Optional: Creates a sparse index"
1400
+ },
1401
+ background: {
1402
+ type: "boolean",
1403
+ description: "Optional: Builds index in background"
1404
+ },
1405
+ expireAfterSeconds: {
1406
+ type: "number",
1407
+ description: "TTL in seconds for documents"
1408
+ },
1409
+ partialFilterExpression: {
1410
+ type: "object",
1411
+ description: "Filter expression for partial indexes"
1412
+ }
1413
+ },
1414
+ required: ["key"]
1415
+ }
1416
+ },
1417
+ writeConcern: {
1418
+ type: "object",
1419
+ description: "Write concern for index creation"
1420
+ },
1421
+ commitQuorum: {
1422
+ type: "string",
1423
+ description: "Number of members required to create the index"
1424
+ },
1425
+ objectIdMode: {
1426
+ type: "string",
1427
+ description: "Control how 24-character hex strings are handled",
1428
+ enum: ["auto", "none", "force"],
1429
+ default: "auto"
1430
+ }
1431
+ },
1432
+ required: ["collection", "indexes"]
1433
+ }
1434
+ },
1435
+ {
1436
+ name: "count",
1437
+ description: "Count documents in a collection matching a query",
1438
+ execution: { taskSupport: "optional" },
1439
+ inputSchema: {
1440
+ type: "object",
1441
+ properties: {
1442
+ collection: { type: "string", description: "Collection name" },
1443
+ query: { type: "object", description: "Query filter to count" },
1444
+ limit: { type: "integer", description: "Max documents to count" },
1445
+ skip: {
1446
+ type: "integer",
1447
+ description: "Docs to skip before counting"
1448
+ },
1449
+ hint: { type: "object", description: "Index hint" },
1450
+ readConcern: { type: "object", description: "Read concern option" },
1451
+ maxTimeMS: { type: "integer", description: "Max execution time" },
1452
+ collation: {
1453
+ type: "object",
1454
+ description: "Collation rules for comparison"
1455
+ },
1456
+ objectIdMode: {
1457
+ type: "string",
1458
+ description: "Control how 24-character hex strings are handled",
1459
+ enum: ["auto", "none", "force"],
1460
+ default: "auto"
1461
+ }
1462
+ },
1463
+ required: ["collection"]
1464
+ }
1465
+ },
1466
+ {
1467
+ name: "listCollections",
1468
+ description: "List all collections in the MongoDB database",
1469
+ execution: { taskSupport: "optional" },
1470
+ inputSchema: {
1471
+ type: "object",
1472
+ properties: {
1473
+ nameOnly: {
1474
+ type: "boolean",
1475
+ description: "If true, return only collection names"
1476
+ },
1477
+ filter: {
1478
+ type: "object",
1479
+ description: "Filter for collections"
1480
+ },
1481
+ skip: {
1482
+ type: "number",
1483
+ description: "Number of collections to skip before returning results",
1484
+ default: 0
1485
+ },
1486
+ limit: {
1487
+ type: "number",
1488
+ description: "Maximum number of collections to return",
1489
+ default: 20
1490
+ },
1491
+ objectIdMode: {
1492
+ type: "string",
1493
+ description: "Control how 24-character hex strings are handled",
1494
+ enum: ["auto", "none", "force"],
1495
+ default: "auto"
1496
+ }
1497
+ }
1498
+ }
1499
+ }
1500
+ ]
1501
+ };
1502
+ }
1503
+
1504
+ // src/server.ts
1505
+ function createSendProgress(extra) {
1506
+ const progressToken = extra._meta?.progressToken;
1507
+ if (progressToken === void 0) return void 0;
1508
+ return (progress, total, message) => extra.sendNotification({
1509
+ method: "notifications/progress",
1510
+ params: { progressToken, progress, total, message }
1511
+ });
1512
+ }
1513
+ function createServer(client, db, isReadOnlyMode = false, options = {}) {
1514
+ const taskStore = new InMemoryTaskStore();
1515
+ const taskMessageQueue = new InMemoryTaskMessageQueue();
1516
+ const server = new Server(
1517
+ {
1518
+ name: "mongodb",
1519
+ title: "MongoDB MCP Server",
1520
+ version: "2.1.0",
1521
+ description: "MCP server for MongoDB: query, aggregate, and manage collections with read-only mode, progress notifications, cancellation, and task support",
1522
+ websiteUrl: "https://github.com/kiliczsh/mcp-mongo-server",
1523
+ ...options
1524
+ },
1525
+ {
1526
+ capabilities: {
1527
+ completions: {},
1528
+ resources: {},
1529
+ tools: {},
1530
+ prompts: {},
1531
+ tasks: {
1532
+ list: {},
1533
+ cancel: {},
1534
+ requests: { tools: { call: {} } }
1535
+ }
1536
+ },
1537
+ taskStore,
1538
+ taskMessageQueue,
1539
+ ...options
1540
+ }
1541
+ );
1542
+ server.setRequestHandler(
1543
+ PingRequestSchema,
1544
+ (request, extra) => handlePingRequest({
1545
+ request,
1546
+ client,
1547
+ db,
1548
+ isReadOnlyMode,
1549
+ signal: extra.signal
1550
+ })
1551
+ );
1552
+ server.setRequestHandler(
1553
+ ListResourcesRequestSchema,
1554
+ (request, extra) => handleListResourcesRequest({
1555
+ request,
1556
+ client,
1557
+ db,
1558
+ isReadOnlyMode,
1559
+ signal: extra.signal
1560
+ })
1561
+ );
1562
+ server.setRequestHandler(
1563
+ ReadResourceRequestSchema,
1564
+ (request, extra) => handleReadResourceRequest({
1565
+ request,
1566
+ client,
1567
+ db,
1568
+ isReadOnlyMode,
1569
+ signal: extra.signal,
1570
+ sendProgress: createSendProgress(extra)
1571
+ })
1572
+ );
1573
+ server.setRequestHandler(
1574
+ ListToolsRequestSchema,
1575
+ (request, extra) => handleListToolsRequest({
1576
+ request,
1577
+ client,
1578
+ db,
1579
+ isReadOnlyMode,
1580
+ signal: extra.signal
1581
+ })
1582
+ );
1583
+ server.setRequestHandler(CallToolRequestSchema, (request, extra) => {
1584
+ const hasTaskParams = !!request.params.task;
1585
+ return handleCallToolRequest({
1586
+ request,
1587
+ client,
1588
+ db,
1589
+ isReadOnlyMode,
1590
+ signal: extra.signal,
1591
+ taskStore: hasTaskParams ? extra.taskStore : void 0,
1592
+ taskTtl: hasTaskParams ? extra.taskRequestedTtl : void 0
1593
+ });
1594
+ });
1595
+ server.setRequestHandler(
1596
+ ListPromptsRequestSchema,
1597
+ (request, extra) => handleListPromptsRequest({
1598
+ request,
1599
+ client,
1600
+ db,
1601
+ isReadOnlyMode,
1602
+ signal: extra.signal
1603
+ })
1604
+ );
1605
+ server.setRequestHandler(
1606
+ GetPromptRequestSchema,
1607
+ (request, extra) => handleGetPromptRequest({
1608
+ request,
1609
+ client,
1610
+ db,
1611
+ isReadOnlyMode,
1612
+ signal: extra.signal,
1613
+ sendProgress: createSendProgress(extra)
1614
+ })
1615
+ );
1616
+ server.setRequestHandler(
1617
+ ListResourceTemplatesRequestSchema,
1618
+ (request, extra) => handleListResourceTemplatesRequest({
1619
+ request,
1620
+ client,
1621
+ db,
1622
+ isReadOnlyMode,
1623
+ signal: extra.signal
1624
+ })
1625
+ );
1626
+ server.setRequestHandler(
1627
+ CompleteRequestSchema,
1628
+ (request, extra) => handleCompletionRequest({
1629
+ request,
1630
+ client,
1631
+ db,
1632
+ isReadOnlyMode,
1633
+ signal: extra.signal
1634
+ })
1635
+ );
1636
+ return server;
1637
+ }
1638
+
1639
+ // src/index.ts
1640
+ var mongoClient = null;
1641
+ async function main() {
1642
+ const args = process.argv.slice(2);
1643
+ let connectionUrl = "";
1644
+ let readOnlyMode = process.env.MCP_MONGODB_READONLY === "true" || false;
1645
+ let transportMode = "stdio";
1646
+ let port = Number(process.env.MCP_PORT) || 3001;
1647
+ const allowedOrigins = (process.env.MCP_HTTP_ALLOWED_ORIGINS || "").split(",").map((origin) => origin.trim()).filter(Boolean);
1648
+ for (let i = 0; i < args.length; i++) {
1649
+ if (args[i] === "--read-only" || args[i] === "-r") {
1650
+ readOnlyMode = true;
1651
+ } else if (args[i] === "--transport" || args[i] === "-t") {
1652
+ const value = args[++i];
1653
+ if (value !== "stdio" && value !== "http") {
1654
+ console.error("Invalid transport mode. Use 'stdio' or 'http'.");
1655
+ process.exit(1);
1656
+ }
1657
+ transportMode = value;
1658
+ } else if (args[i] === "--port" || args[i] === "-p") {
1659
+ port = Number(args[++i]);
1660
+ if (Number.isNaN(port)) {
1661
+ console.error("Invalid port number.");
1662
+ process.exit(1);
1663
+ }
1664
+ } else if (args[i] === "--allowed-origins") {
1665
+ allowedOrigins.push(
1666
+ ...(args[++i] || "").split(",").map((origin) => origin.trim()).filter(Boolean)
1667
+ );
1668
+ } else if (!connectionUrl) {
1669
+ connectionUrl = args[i];
1670
+ }
1671
+ }
1672
+ if (!connectionUrl) {
1673
+ connectionUrl = process.env.MCP_MONGODB_URI || "";
1674
+ }
1675
+ if (!connectionUrl) {
1676
+ console.error(
1677
+ "Please provide a MongoDB connection URL via command-line argument or MCP_MONGODB_URI environment variable"
1678
+ );
1679
+ console.error(
1680
+ "Usage: command <mongodb-url> [--read-only|-r] [--transport stdio|http] [--port 3001]"
1681
+ );
1682
+ console.error(
1683
+ " or: MCP_MONGODB_URI=<mongodb-url> [MCP_MONGODB_READONLY=true] command"
1684
+ );
1685
+ process.exit(1);
1686
+ }
1687
+ if (!connectionUrl.startsWith("mongodb://") && !connectionUrl.startsWith("mongodb+srv://")) {
1688
+ console.error(
1689
+ "Invalid MongoDB connection URL. URL must start with 'mongodb://' or 'mongodb+srv://'"
1690
+ );
1691
+ process.exit(1);
1692
+ }
1693
+ try {
1694
+ const { client, db, isConnected, isReadOnlyMode } = await connectToMongoDB(
1695
+ connectionUrl,
1696
+ readOnlyMode
1697
+ );
1698
+ mongoClient = client;
1699
+ if (!isConnected || !client || !db) {
1700
+ console.error("Failed to connect to MongoDB");
1701
+ process.exit(1);
1702
+ }
1703
+ if (transportMode === "http") {
1704
+ await startHttpServer(client, db, isReadOnlyMode, port, allowedOrigins);
1705
+ } else {
1706
+ await startStdioServer(client, db, isReadOnlyMode);
1707
+ }
1708
+ } catch (error) {
1709
+ console.error("Failed to connect to MongoDB:", error);
64
1710
  if (mongoClient) {
65
- await mongoClient.close();
1711
+ await mongoClient.close();
1712
+ }
1713
+ process.exit(1);
1714
+ }
1715
+ }
1716
+ async function startStdioServer(client, db, isReadOnlyMode) {
1717
+ const server = createServer(client, db, isReadOnlyMode);
1718
+ const transport = new StdioServerTransport();
1719
+ await server.connect(transport);
1720
+ console.warn("Server connected successfully via stdio");
1721
+ }
1722
+ function isOriginAllowed(origin, allowedOrigins) {
1723
+ if (!origin) {
1724
+ return true;
1725
+ }
1726
+ if (allowedOrigins.includes(origin)) {
1727
+ return true;
1728
+ }
1729
+ try {
1730
+ const { hostname } = new URL(origin);
1731
+ return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
1732
+ } catch {
1733
+ return false;
1734
+ }
1735
+ }
1736
+ async function startHttpServer(client, db, isReadOnlyMode, port, allowedOrigins) {
1737
+ const app = createMcpExpressApp({ host: "0.0.0.0" });
1738
+ app.use((req, res, next) => {
1739
+ const start = Date.now();
1740
+ const ip = req.headers["x-forwarded-for"] || req.socket.remoteAddress;
1741
+ const method = req.method;
1742
+ const url = req.url;
1743
+ let mcpMethod = "-";
1744
+ if (req.body && typeof req.body === "object" && "method" in req.body) {
1745
+ mcpMethod = req.body.method;
1746
+ }
1747
+ res.on("finish", () => {
1748
+ const duration = Date.now() - start;
1749
+ console.log(
1750
+ `${(/* @__PURE__ */ new Date()).toISOString()} | ${ip} | ${method} ${url} | ${res.statusCode} | ${duration}ms | mcp:${mcpMethod}`
1751
+ );
1752
+ });
1753
+ next();
1754
+ });
1755
+ app.use((req, res, next) => {
1756
+ const origin = req.headers.origin;
1757
+ if (!isOriginAllowed(origin, allowedOrigins)) {
1758
+ res.status(403).json({
1759
+ jsonrpc: "2.0",
1760
+ error: {
1761
+ code: -32603,
1762
+ message: "Forbidden: origin not allowed"
1763
+ },
1764
+ id: null
1765
+ });
1766
+ return;
66
1767
  }
67
- process.exit(0);
1768
+ next();
1769
+ });
1770
+ app.post("/mcp", async (req, res) => {
1771
+ const server = createServer(client, db, isReadOnlyMode);
1772
+ try {
1773
+ const transport = new StreamableHTTPServerTransport({
1774
+ sessionIdGenerator: void 0
1775
+ });
1776
+ await server.connect(transport);
1777
+ await transport.handleRequest(req, res, req.body);
1778
+ res.on("close", () => {
1779
+ transport.close();
1780
+ server.close();
1781
+ });
1782
+ } catch (error) {
1783
+ console.error("Error handling MCP request:", error);
1784
+ if (!res.headersSent) {
1785
+ res.status(500).json({
1786
+ jsonrpc: "2.0",
1787
+ error: {
1788
+ code: -32603,
1789
+ message: "Internal server error"
1790
+ },
1791
+ id: null
1792
+ });
1793
+ }
1794
+ }
1795
+ });
1796
+ app.get("/mcp", async (_req, res) => {
1797
+ res.writeHead(405).end(
1798
+ JSON.stringify({
1799
+ jsonrpc: "2.0",
1800
+ error: {
1801
+ code: -32e3,
1802
+ message: "Method not allowed."
1803
+ },
1804
+ id: null
1805
+ })
1806
+ );
1807
+ });
1808
+ app.delete("/mcp", async (_req, res) => {
1809
+ res.writeHead(405).end(
1810
+ JSON.stringify({
1811
+ jsonrpc: "2.0",
1812
+ error: {
1813
+ code: -32e3,
1814
+ message: "Method not allowed."
1815
+ },
1816
+ id: null
1817
+ })
1818
+ );
1819
+ });
1820
+ app.listen(port, () => {
1821
+ console.log(`MCP MongoDB Streamable HTTP Server listening on port ${port}`);
1822
+ console.log(`Endpoint: http://localhost:${port}/mcp`);
1823
+ });
1824
+ }
1825
+ process.on("SIGINT", async () => {
1826
+ if (mongoClient) {
1827
+ await mongoClient.close();
1828
+ }
1829
+ process.exit(0);
68
1830
  });
69
1831
  process.on("SIGTERM", async () => {
70
- if (mongoClient) {
71
- await mongoClient.close();
72
- }
73
- process.exit(0);
1832
+ if (mongoClient) {
1833
+ await mongoClient.close();
1834
+ }
1835
+ process.exit(0);
74
1836
  });
75
1837
  main().catch((error) => {
76
- console.error("Server error:", error);
77
- process.exit(1);
1838
+ console.error("Server error:", error);
1839
+ process.exit(1);
78
1840
  });