mcp-mongo-server 2.0.2 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +0 -2
  2. package/build/index.js +454 -99
  3. package/package.json +24 -8
package/README.md CHANGED
@@ -4,8 +4,6 @@
4
4
  ![NPM Version](https://img.shields.io/npm/v/mcp-mongo-server)
5
5
  ![NPM Downloads](https://img.shields.io/npm/dm/mcp-mongo-server)
6
6
  ![NPM License](https://img.shields.io/npm/l/mcp-mongo-server)
7
- [![smithery badge](https://smithery.ai/badge/mcp-mongo-server)](https://smithery.ai/server/mcp-mongo-server)
8
- [![Verified on MseeP](https://mseep.ai/badge.svg)](https://mseep.ai/app/e274a3dd-7fe6-4440-8c43-043bae668251)
9
7
 
10
8
  A Model Context Protocol server that enables LLMs to interact with MongoDB databases. This server provides capabilities for inspecting collection schemas and executing MongoDB operations through a standardized interface.
11
9
 
package/build/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
+ import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
4
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
6
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
6
- import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
7
7
 
8
8
  // src/mongo.ts
9
9
  import { MongoClient, ReadPreference } from "mongodb";
@@ -32,6 +32,10 @@ async function connectToMongoDB(url, readOnly) {
32
32
  }
33
33
 
34
34
  // src/server.ts
35
+ import {
36
+ InMemoryTaskMessageQueue,
37
+ InMemoryTaskStore
38
+ } from "@modelcontextprotocol/sdk/experimental/tasks";
35
39
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
36
40
  import {
37
41
  CallToolRequestSchema,
@@ -46,6 +50,10 @@ import {
46
50
  } from "@modelcontextprotocol/sdk/types.js";
47
51
 
48
52
  // src/schemas/call.ts
53
+ import {
54
+ ErrorCode,
55
+ McpError
56
+ } from "@modelcontextprotocol/sdk/types.js";
49
57
  import { ObjectId } from "mongodb";
50
58
  var COLLECTION_OPERATIONS = [
51
59
  "query",
@@ -56,11 +64,40 @@ var COLLECTION_OPERATIONS = [
56
64
  "count"
57
65
  ];
58
66
  var WRITE_OPERATIONS = ["update", "insert", "createIndex"];
67
+ var READONLY_FORBIDDEN_AGG_OPERATORS = /* @__PURE__ */ new Set([
68
+ "$out",
69
+ "$merge",
70
+ "$function",
71
+ "$accumulator",
72
+ "$where"
73
+ ]);
74
+ function findForbiddenAggOperator(value) {
75
+ if (Array.isArray(value)) {
76
+ for (const item of value) {
77
+ const found = findForbiddenAggOperator(item);
78
+ if (found) return found;
79
+ }
80
+ return null;
81
+ }
82
+ if (value && typeof value === "object") {
83
+ for (const [key, nested] of Object.entries(value)) {
84
+ if (READONLY_FORBIDDEN_AGG_OPERATORS.has(key)) {
85
+ return key;
86
+ }
87
+ const found = findForbiddenAggOperator(nested);
88
+ if (found) return found;
89
+ }
90
+ }
91
+ return null;
92
+ }
59
93
  async function handleCallToolRequest({
60
94
  request,
61
95
  client,
62
96
  db,
63
- isReadOnlyMode
97
+ isReadOnlyMode,
98
+ signal,
99
+ taskStore,
100
+ taskTtl
64
101
  }) {
65
102
  const { name, arguments: args = {} } = request.params;
66
103
  const operation = name;
@@ -76,37 +113,93 @@ async function handleCallToolRequest({
76
113
  }
77
114
  Object.assign(args, filteredArgs);
78
115
  validateOperation(operation);
79
- checkReadOnlyMode(operation, isReadOnlyMode);
80
- let collection = null;
81
- if (COLLECTION_OPERATIONS.includes(operation)) {
82
- const collectionName = args.collection;
83
- if (!collectionName) {
84
- throw new Error(
85
- `Collection name is required for '${operation}' operation`
86
- );
116
+ try {
117
+ checkReadOnlyMode(operation, isReadOnlyMode);
118
+ let collection = null;
119
+ if (COLLECTION_OPERATIONS.includes(operation)) {
120
+ const collectionName = args.collection;
121
+ if (!collectionName) {
122
+ throw new Error(
123
+ `Collection name is required for '${operation}' operation`
124
+ );
125
+ }
126
+ collection = db.collection(collectionName);
127
+ validateCollection(collection);
128
+ }
129
+ signal?.throwIfAborted();
130
+ if (taskStore) {
131
+ const task = await taskStore.createTask({
132
+ ttl: taskTtl ?? void 0
133
+ });
134
+ (async () => {
135
+ try {
136
+ const result = await executeOperation(
137
+ operation,
138
+ collection,
139
+ db,
140
+ isReadOnlyMode,
141
+ args,
142
+ objectIdMode,
143
+ signal
144
+ );
145
+ await taskStore.storeTaskResult(task.taskId, "completed", result);
146
+ } catch (error) {
147
+ try {
148
+ const message = error instanceof Error ? error.message : "Unknown error";
149
+ await taskStore.storeTaskResult(task.taskId, "failed", {
150
+ content: [{ type: "text", text: message }],
151
+ isError: true
152
+ });
153
+ } catch {
154
+ }
155
+ }
156
+ })();
157
+ return { task };
158
+ }
159
+ return await executeOperation(
160
+ operation,
161
+ collection,
162
+ db,
163
+ isReadOnlyMode,
164
+ args,
165
+ objectIdMode,
166
+ signal
167
+ );
168
+ } catch (error) {
169
+ if (error instanceof McpError || signal?.aborted) {
170
+ throw error;
87
171
  }
88
- collection = db.collection(collectionName);
89
- validateCollection(collection);
172
+ return toolExecutionError(
173
+ error instanceof Error ? error.message : "Unknown error"
174
+ );
90
175
  }
176
+ }
177
+ async function executeOperation(operation, collection, db, isReadOnlyMode, args, objectIdMode, signal) {
91
178
  switch (operation) {
92
179
  case "query":
93
- return handleQuery(collection, args, objectIdMode);
180
+ return handleQuery(collection, args, objectIdMode, signal);
94
181
  case "aggregate":
95
- return handleAggregate(collection, args, objectIdMode);
182
+ return handleAggregate(
183
+ collection,
184
+ args,
185
+ objectIdMode,
186
+ isReadOnlyMode,
187
+ signal
188
+ );
96
189
  case "update":
97
- return handleUpdate(collection, args, objectIdMode);
190
+ return handleUpdate(collection, args, objectIdMode, signal);
98
191
  case "serverInfo":
99
- return handleServerInfo(db, isReadOnlyMode, args);
192
+ return handleServerInfo(db, isReadOnlyMode, args, signal);
100
193
  case "insert":
101
- return handleInsert(collection, args, objectIdMode);
194
+ return handleInsert(collection, args, objectIdMode, signal);
102
195
  case "createIndex":
103
- return handleCreateIndex(collection, args, objectIdMode);
196
+ return handleCreateIndex(collection, args, objectIdMode, signal);
104
197
  case "count":
105
- return handleCount(collection, args, objectIdMode);
198
+ return handleCount(collection, args, objectIdMode, signal);
106
199
  case "listCollections":
107
- return handleListCollections(db, args, objectIdMode);
200
+ return handleListCollections(db, args, objectIdMode, signal);
108
201
  default:
109
- throw new Error(`Unknown operation: ${operation}`);
202
+ throw new McpError(ErrorCode.InvalidParams, `Unknown tool: ${operation}`);
110
203
  }
111
204
  }
112
205
  function validateOperation(operation) {
@@ -121,7 +214,7 @@ function validateOperation(operation) {
121
214
  "listCollections"
122
215
  ];
123
216
  if (!validOperations.includes(operation)) {
124
- throw new Error(`Unknown operation: ${operation}`);
217
+ throw new McpError(ErrorCode.InvalidParams, `Unknown tool: ${operation}`);
125
218
  }
126
219
  }
127
220
  function validateCollection(collection) {
@@ -269,40 +362,64 @@ function formatResponse(data) {
269
362
  ]
270
363
  };
271
364
  }
365
+ function toolExecutionError(message) {
366
+ return {
367
+ content: [{ type: "text", text: message }],
368
+ isError: true
369
+ };
370
+ }
272
371
  function handleError(error, operation, collectionName) {
273
- const context = collectionName ? `collection ${collectionName}` : "operation";
274
- if (error instanceof Error) {
275
- throw new Error(`Failed to ${operation} ${context}: ${error.message}`);
372
+ if (error instanceof Error && error.name === "AbortError") {
373
+ throw error;
276
374
  }
277
- throw new Error(`Failed to ${operation} ${context}: Unknown error`);
375
+ const context = collectionName ? `collection ${collectionName}` : "operation";
376
+ const message = error instanceof Error ? error.message : "Unknown error";
377
+ return toolExecutionError(`Failed to ${operation} ${context}: ${message}`);
278
378
  }
279
- async function handleQuery(collection, args, objectIdMode = "auto") {
379
+ async function handleQuery(collection, args, objectIdMode = "auto", signal) {
280
380
  if (!collection) {
281
381
  throw new Error("Collection is required for query operation");
282
382
  }
283
- const { filter, projection, limit, explain, sort } = args;
383
+ const { filter, projection, explain, sort } = args;
384
+ const limit = args.limit || 10;
385
+ const skip = args.skip || 0;
284
386
  const queryFilter = parseFilter(filter, objectIdMode);
285
387
  try {
286
388
  if (explain) {
389
+ signal?.throwIfAborted();
287
390
  const explainResult = await collection.find(queryFilter, {
288
391
  projection,
289
- limit: limit || 100,
392
+ limit,
393
+ skip,
290
394
  sort
291
395
  }).explain(explain);
292
396
  return formatResponse(explainResult);
293
397
  }
294
- const cursor = collection.find(queryFilter, {
295
- projection,
296
- limit: limit || 100,
297
- sort
398
+ signal?.throwIfAborted();
399
+ const [total, results] = await Promise.all([
400
+ collection.countDocuments(queryFilter),
401
+ collection.find(queryFilter, {
402
+ projection,
403
+ limit,
404
+ skip,
405
+ sort
406
+ }).toArray()
407
+ ]);
408
+ return formatResponse({
409
+ results,
410
+ metadata: {
411
+ total,
412
+ returned: results.length,
413
+ skip,
414
+ limit,
415
+ hasMore: skip + results.length < total
416
+ }
298
417
  });
299
- const results = await cursor.toArray();
300
- return formatResponse(results);
301
418
  } catch (error) {
302
419
  return handleError(error, "query", collection.collectionName);
303
420
  }
304
421
  }
305
- async function handleAggregate(collection, args, objectIdMode = "auto") {
422
+ async function handleAggregate(collection, args, objectIdMode = "auto", isReadOnlyMode = false, signal) {
306
423
  if (!collection) {
307
424
  throw new Error("Collection is required for aggregate operation");
308
425
  }
@@ -310,6 +427,14 @@ async function handleAggregate(collection, args, objectIdMode = "auto") {
310
427
  if (!Array.isArray(pipeline)) {
311
428
  throw new Error("Pipeline must be an array");
312
429
  }
430
+ if (isReadOnlyMode) {
431
+ const forbidden = findForbiddenAggOperator(pipeline);
432
+ if (forbidden) {
433
+ throw new Error(
434
+ `ReadonlyError: Aggregation operator '${forbidden}' is not allowed in read-only mode`
435
+ );
436
+ }
437
+ }
313
438
  const processedPipeline = pipeline.map((stage) => {
314
439
  if (typeof stage === "object" && stage !== null) {
315
440
  return processObjectIdInFilter(
@@ -321,6 +446,7 @@ async function handleAggregate(collection, args, objectIdMode = "auto") {
321
446
  });
322
447
  try {
323
448
  if (explain) {
449
+ signal?.throwIfAborted();
324
450
  const explainResult = await collection.aggregate(processedPipeline, {
325
451
  explain: {
326
452
  verbosity: explain
@@ -328,13 +454,19 @@ async function handleAggregate(collection, args, objectIdMode = "auto") {
328
454
  }).toArray();
329
455
  return formatResponse(explainResult);
330
456
  }
457
+ signal?.throwIfAborted();
331
458
  const results = await collection.aggregate(processedPipeline).toArray();
332
- return formatResponse(results);
459
+ return formatResponse({
460
+ results,
461
+ metadata: {
462
+ returned: results.length
463
+ }
464
+ });
333
465
  } catch (error) {
334
466
  return handleError(error, "aggregate", collection.collectionName);
335
467
  }
336
468
  }
337
- async function handleUpdate(collection, args, objectIdMode = "auto") {
469
+ async function handleUpdate(collection, args, objectIdMode = "auto", signal) {
338
470
  if (!collection) {
339
471
  throw new Error("Collection is required for update operation");
340
472
  }
@@ -375,6 +507,7 @@ async function handleUpdate(collection, args, objectIdMode = "auto") {
375
507
  multi: !!multi
376
508
  };
377
509
  const updateMethod = options.multi ? "updateMany" : "updateOne";
510
+ signal?.throwIfAborted();
378
511
  const result = await collection[updateMethod](
379
512
  queryFilter,
380
513
  processedUpdate,
@@ -390,35 +523,33 @@ async function handleUpdate(collection, args, objectIdMode = "auto") {
390
523
  return handleError(error, "update", collection.collectionName);
391
524
  }
392
525
  }
393
- async function handleServerInfo(db, isReadOnlyMode, args) {
526
+ async function handleServerInfo(db, isReadOnlyMode, args, signal) {
394
527
  const { includeDebugInfo } = args;
395
528
  try {
529
+ signal?.throwIfAborted();
396
530
  const buildInfo = await db.command({ buildInfo: 1 });
397
- let serverStatus = null;
398
- if (includeDebugInfo) {
399
- serverStatus = await db.command({ serverStatus: 1 });
400
- }
401
531
  const serverInfo = {
402
532
  version: buildInfo.version,
403
533
  gitVersion: buildInfo.gitVersion,
404
- modules: buildInfo.modules,
405
- allocator: buildInfo.allocator,
406
- javascriptEngine: buildInfo.javascriptEngine,
407
- sysInfo: buildInfo.sysInfo,
408
534
  storageEngines: buildInfo.storageEngines,
409
- debug: buildInfo.debug,
410
535
  maxBsonObjectSize: buildInfo.maxBsonObjectSize,
411
- openssl: buildInfo.openssl,
412
- buildEnvironment: buildInfo.buildEnvironment,
413
536
  bits: buildInfo.bits,
414
537
  ok: buildInfo.ok,
415
- status: {},
416
538
  connectionInfo: {
417
539
  readOnlyMode: isReadOnlyMode,
418
540
  readPreference: isReadOnlyMode ? "secondary" : "primary"
419
541
  }
420
542
  };
421
- if (serverStatus) {
543
+ if (includeDebugInfo) {
544
+ signal?.throwIfAborted();
545
+ const serverStatus = await db.command({ serverStatus: 1 });
546
+ serverInfo.modules = buildInfo.modules;
547
+ serverInfo.allocator = buildInfo.allocator;
548
+ serverInfo.javascriptEngine = buildInfo.javascriptEngine;
549
+ serverInfo.sysInfo = buildInfo.sysInfo;
550
+ serverInfo.debug = buildInfo.debug;
551
+ serverInfo.openssl = buildInfo.openssl;
552
+ serverInfo.buildEnvironment = buildInfo.buildEnvironment;
422
553
  serverInfo.status = {
423
554
  host: serverStatus.host,
424
555
  version: serverStatus.version,
@@ -440,7 +571,7 @@ async function handleServerInfo(db, isReadOnlyMode, args) {
440
571
  return handleError(error, "get server information");
441
572
  }
442
573
  }
443
- async function handleInsert(collection, args, objectIdMode = "auto") {
574
+ async function handleInsert(collection, args, objectIdMode = "auto", signal) {
444
575
  if (!collection) {
445
576
  throw new Error("Collection is required for insert operation");
446
577
  }
@@ -466,6 +597,7 @@ async function handleInsert(collection, args, objectIdMode = "auto") {
466
597
  writeConcern,
467
598
  bypassDocumentValidation
468
599
  };
600
+ signal?.throwIfAborted();
469
601
  const result = await collection.insertMany(
470
602
  processedDocuments,
471
603
  options
@@ -488,7 +620,7 @@ async function handleInsert(collection, args, objectIdMode = "auto") {
488
620
  return handleError(error, "insert", collection.collectionName);
489
621
  }
490
622
  }
491
- async function handleCreateIndex(collection, args, objectIdMode = "auto") {
623
+ async function handleCreateIndex(collection, args, objectIdMode = "auto", signal) {
492
624
  if (!collection) {
493
625
  throw new Error("Collection is required for createIndex operation");
494
626
  }
@@ -517,6 +649,7 @@ async function handleCreateIndex(collection, args, objectIdMode = "auto") {
517
649
  const indexOptions = {
518
650
  commitQuorum: typeof commitQuorum === "number" ? commitQuorum : void 0
519
651
  };
652
+ signal?.throwIfAborted();
520
653
  const result = await collection.createIndexes(
521
654
  processedIndexes,
522
655
  indexOptions
@@ -531,7 +664,7 @@ async function handleCreateIndex(collection, args, objectIdMode = "auto") {
531
664
  return handleError(error, "create indexes", collection.collectionName);
532
665
  }
533
666
  }
534
- async function handleCount(collection, args, objectIdMode = "auto") {
667
+ async function handleCount(collection, args, objectIdMode = "auto", signal) {
535
668
  if (!collection) {
536
669
  throw new Error("Collection is required for count operation");
537
670
  }
@@ -551,6 +684,7 @@ async function handleCount(collection, args, objectIdMode = "auto") {
551
684
  delete options[key];
552
685
  }
553
686
  }
687
+ signal?.throwIfAborted();
554
688
  const count = await collection.countDocuments(countQuery, options);
555
689
  return formatResponse({
556
690
  count,
@@ -560,8 +694,10 @@ async function handleCount(collection, args, objectIdMode = "auto") {
560
694
  return handleError(error, "count documents", collection.collectionName);
561
695
  }
562
696
  }
563
- async function handleListCollections(db, args, objectIdMode = "auto") {
697
+ async function handleListCollections(db, args, objectIdMode = "auto", signal) {
564
698
  const { nameOnly, filter } = args;
699
+ const skip = args.skip || 0;
700
+ const limit = args.limit || 20;
565
701
  let processedFilter = filter;
566
702
  if (filter && typeof filter === "object") {
567
703
  processedFilter = processObjectIdInFilter(
@@ -571,20 +707,58 @@ async function handleListCollections(db, args, objectIdMode = "auto") {
571
707
  }
572
708
  try {
573
709
  const options = processedFilter ? { filter: processedFilter } : {};
710
+ signal?.throwIfAborted();
574
711
  const collections = await db.listCollections(options).toArray();
575
- const result = nameOnly ? collections.map((collection) => collection.name) : collections;
576
- return formatResponse(result);
712
+ const allResults = nameOnly ? collections.map((collection) => collection.name) : collections;
713
+ const total = allResults.length;
714
+ const paged = allResults.slice(skip, skip + limit);
715
+ return formatResponse({
716
+ results: paged,
717
+ metadata: {
718
+ total,
719
+ returned: paged.length,
720
+ skip,
721
+ limit,
722
+ hasMore: skip + paged.length < total
723
+ }
724
+ });
577
725
  } catch (error) {
578
726
  return handleError(error, "list collections");
579
727
  }
580
728
  }
581
729
 
730
+ // src/utils/pagination.ts
731
+ var DEFAULT_PAGE_SIZE = 20;
732
+ function encodeCursor(offset) {
733
+ const data = { offset };
734
+ return Buffer.from(JSON.stringify(data)).toString("base64");
735
+ }
736
+ function decodeCursor(cursor) {
737
+ if (!cursor) return 0;
738
+ try {
739
+ const data = JSON.parse(
740
+ Buffer.from(cursor, "base64").toString("utf-8")
741
+ );
742
+ return typeof data.offset === "number" && data.offset >= 0 ? data.offset : 0;
743
+ } catch {
744
+ return 0;
745
+ }
746
+ }
747
+ function paginate(items, cursor, pageSize = DEFAULT_PAGE_SIZE) {
748
+ const offset = decodeCursor(cursor);
749
+ const page = items.slice(offset, offset + pageSize);
750
+ const nextOffset = offset + pageSize;
751
+ const nextCursor = nextOffset < items.length ? encodeCursor(nextOffset) : void 0;
752
+ return { items: page, nextCursor };
753
+ }
754
+
582
755
  // src/schemas/completion.ts
583
756
  async function handleCompletionRequest({
584
757
  request,
585
758
  client,
586
759
  db,
587
- isReadOnlyMode
760
+ isReadOnlyMode,
761
+ signal
588
762
  }) {
589
763
  const { ref, argument } = request.params;
590
764
  if (ref.type === "ref/prompt") {
@@ -593,7 +767,8 @@ async function handleCompletionRequest({
593
767
  db,
594
768
  isReadOnlyMode,
595
769
  ref.name,
596
- argument
770
+ argument,
771
+ signal
597
772
  );
598
773
  }
599
774
  if (ref.type === "ref/resource") {
@@ -602,34 +777,46 @@ async function handleCompletionRequest({
602
777
  db,
603
778
  isReadOnlyMode,
604
779
  ref.uri,
605
- argument
780
+ argument,
781
+ signal
606
782
  );
607
783
  }
608
784
  return emptyCompletionResult();
609
785
  }
610
- async function handlePromptCompletion(client, db, isReadOnlyMode, promptName, argument) {
786
+ async function handlePromptCompletion(client, db, isReadOnlyMode, promptName, argument, signal) {
611
787
  if (!promptName) {
612
788
  return emptyCompletionResult();
613
789
  }
614
790
  if (argument.name === "collection") {
615
- return await completeCollectionNames(argument.value, db, isReadOnlyMode);
791
+ return await completeCollectionNames(
792
+ argument.value,
793
+ db,
794
+ isReadOnlyMode,
795
+ signal
796
+ );
616
797
  }
617
798
  return emptyCompletionResult();
618
799
  }
619
- async function handleResourceCompletion(client, db, isReadOnlyMode, promptName, argument) {
800
+ async function handleResourceCompletion(client, db, isReadOnlyMode, promptName, argument, signal) {
620
801
  if (!promptName) {
621
802
  return emptyCompletionResult();
622
803
  }
623
804
  if (argument.name === "collection") {
624
- return await completeCollectionNames(argument.value, db, isReadOnlyMode);
805
+ return await completeCollectionNames(
806
+ argument.value,
807
+ db,
808
+ isReadOnlyMode,
809
+ signal
810
+ );
625
811
  }
626
812
  return emptyCompletionResult();
627
813
  }
628
- async function completeCollectionNames(partialValue, db, isReadOnlyMode) {
814
+ async function completeCollectionNames(partialValue, db, isReadOnlyMode, signal) {
629
815
  try {
630
816
  console.warn(
631
817
  `Completing collection names with partial value: ${partialValue}`
632
818
  );
819
+ signal?.throwIfAborted();
633
820
  const collections = await db.listCollections().toArray();
634
821
  const matchingCollections = collections.map(
635
822
  (c) => c.name
@@ -641,14 +828,17 @@ async function completeCollectionNames(partialValue, db, isReadOnlyMode) {
641
828
  )
642
829
  ).sort();
643
830
  console.warn(`Found ${matchingCollections.length} matching collections`);
644
- const MAX_ITEMS = 100;
645
- const limitedResults = matchingCollections.slice(0, MAX_ITEMS);
646
- const hasMore = matchingCollections.length > MAX_ITEMS;
831
+ const PAGE_SIZE = 100;
832
+ const { items: limitedResults, nextCursor } = paginate(
833
+ matchingCollections,
834
+ void 0,
835
+ PAGE_SIZE
836
+ );
647
837
  return {
648
838
  completion: {
649
839
  values: limitedResults,
650
840
  total: matchingCollections.length,
651
- hasMore
841
+ hasMore: !!nextCursor
652
842
  }
653
843
  };
654
844
  } catch (error) {
@@ -671,12 +861,14 @@ async function handlePingRequest({
671
861
  request,
672
862
  client,
673
863
  db,
674
- isReadOnlyMode
864
+ isReadOnlyMode,
865
+ signal
675
866
  }) {
676
867
  try {
677
868
  if (!client) {
678
869
  throw new Error("MongoDB connection is not available");
679
870
  }
871
+ signal?.throwIfAborted();
680
872
  const pong = await db.command({ ping: 1 });
681
873
  if (pong.ok !== 1) {
682
874
  throw new Error(`MongoDB ping failed: ${pong.errmsg}`);
@@ -695,7 +887,8 @@ async function handleListPromptsRequest({
695
887
  request,
696
888
  client,
697
889
  db,
698
- isReadOnlyMode
890
+ isReadOnlyMode,
891
+ signal
699
892
  }) {
700
893
  return {
701
894
  prompts: [
@@ -717,7 +910,9 @@ async function handleGetPromptRequest({
717
910
  request,
718
911
  client,
719
912
  db,
720
- isReadOnlyMode
913
+ isReadOnlyMode,
914
+ signal,
915
+ sendProgress
721
916
  }) {
722
917
  const { name, arguments: args = {} } = request.params;
723
918
  if (name !== "analyze_collection") {
@@ -732,8 +927,14 @@ async function handleGetPromptRequest({
732
927
  if (collection.collectionName.startsWith("system.")) {
733
928
  throw new Error("Access to system collections is not allowed");
734
929
  }
930
+ await sendProgress?.(1, 3, "Getting schema");
931
+ signal?.throwIfAborted();
735
932
  const schemaSample = await collection.findOne({});
933
+ await sendProgress?.(2, 3, "Getting stats");
934
+ signal?.throwIfAborted();
736
935
  const stats = await collection.aggregate([{ $collStats: { count: {} } }]).toArray();
936
+ await sendProgress?.(3, 3, "Fetching samples");
937
+ signal?.throwIfAborted();
737
938
  const sampleDocs = await collection.find({}).limit(5).toArray();
738
939
  const documentCount = stats[0]?.count ?? "unknown";
739
940
  return {
@@ -864,7 +1065,9 @@ async function handleReadResourceRequest({
864
1065
  request,
865
1066
  client,
866
1067
  db,
867
- isReadOnlyMode
1068
+ isReadOnlyMode,
1069
+ signal,
1070
+ sendProgress
868
1071
  }) {
869
1072
  const url = new URL(request.params.uri);
870
1073
  const collectionName = url.pathname.replace(/^\//, "");
@@ -872,18 +1075,25 @@ async function handleReadResourceRequest({
872
1075
  const collection = db.collection(collectionName);
873
1076
  const sampleSize = 100;
874
1077
  let sampleDocuments = [];
1078
+ await sendProgress?.(1, 4, "Sampling documents");
875
1079
  try {
1080
+ signal?.throwIfAborted();
876
1081
  sampleDocuments = await collection.aggregate([{ $sample: { size: sampleSize } }]).toArray();
877
1082
  } catch (sampleError) {
878
1083
  console.warn(
879
1084
  `$sample aggregation failed for ${collectionName}, falling back to sequential scan: ${sampleError}`
880
1085
  );
1086
+ signal?.throwIfAborted();
881
1087
  sampleDocuments = await collection.find({}).limit(sampleSize).toArray();
882
1088
  }
1089
+ await sendProgress?.(2, 4, "Getting indexes");
1090
+ signal?.throwIfAborted();
883
1091
  const indexes = await collection.indexes();
884
1092
  const inferredSchema = inferSchemaFromSamples(sampleDocuments);
1093
+ await sendProgress?.(3, 4, "Counting documents");
885
1094
  let documentCount = null;
886
1095
  try {
1096
+ signal?.throwIfAborted();
887
1097
  documentCount = await Promise.race([
888
1098
  collection.countDocuments(),
889
1099
  new Promise(
@@ -898,12 +1108,14 @@ async function handleReadResourceRequest({
898
1108
  `Count operation failed or timed out for ${collectionName}: ${countError}`
899
1109
  );
900
1110
  try {
1111
+ signal?.throwIfAborted();
901
1112
  const stats = await db.command({ collStats: collectionName });
902
1113
  documentCount = stats.count;
903
1114
  } catch {
904
1115
  documentCount = "unknown (count operation timed out)";
905
1116
  }
906
1117
  }
1118
+ await sendProgress?.(4, 4, "Done");
907
1119
  const schema = {
908
1120
  type: "collection",
909
1121
  name: collectionName,
@@ -940,17 +1152,23 @@ async function handleListResourcesRequest({
940
1152
  request,
941
1153
  client,
942
1154
  db,
943
- isReadOnlyMode
1155
+ isReadOnlyMode,
1156
+ signal
944
1157
  }) {
945
1158
  try {
1159
+ signal?.throwIfAborted();
946
1160
  const collections = await db.listCollections().toArray();
1161
+ const allResources = collections.map((collection) => ({
1162
+ uri: `mongodb:///${collection.name}`,
1163
+ mimeType: "application/json",
1164
+ name: collection.name,
1165
+ description: `MongoDB collection: ${collection.name}`
1166
+ }));
1167
+ const cursor = request.params?.cursor;
1168
+ const { items, nextCursor } = paginate(allResources, cursor);
947
1169
  return {
948
- resources: collections.map((collection) => ({
949
- uri: `mongodb:///${collection.name}`,
950
- mimeType: "application/json",
951
- name: collection.name,
952
- description: `MongoDB collection: ${collection.name}`
953
- }))
1170
+ resources: items,
1171
+ ...nextCursor ? { nextCursor } : {}
954
1172
  };
955
1173
  } catch (error) {
956
1174
  if (error instanceof Error) {
@@ -965,7 +1183,8 @@ async function handleListResourceTemplatesRequest({
965
1183
  request,
966
1184
  client,
967
1185
  db,
968
- isReadOnlyMode
1186
+ isReadOnlyMode,
1187
+ signal
969
1188
  }) {
970
1189
  return {
971
1190
  resourceTemplates: [
@@ -1011,13 +1230,15 @@ async function handleListToolsRequest({
1011
1230
  request,
1012
1231
  client,
1013
1232
  db,
1014
- isReadOnlyMode
1233
+ isReadOnlyMode,
1234
+ signal
1015
1235
  }) {
1016
1236
  return {
1017
1237
  tools: [
1018
1238
  {
1019
1239
  name: "query",
1020
1240
  description: "Execute a MongoDB query with optional execution plan analysis",
1241
+ execution: { taskSupport: "optional" },
1021
1242
  inputSchema: {
1022
1243
  type: "object",
1023
1244
  properties: {
@@ -1038,6 +1259,11 @@ async function handleListToolsRequest({
1038
1259
  description: "Maximum number of documents to return",
1039
1260
  default: 10
1040
1261
  },
1262
+ skip: {
1263
+ type: "number",
1264
+ description: "Number of documents to skip before returning results",
1265
+ default: 0
1266
+ },
1041
1267
  explain: {
1042
1268
  type: "string",
1043
1269
  description: "Optional: Get query execution information",
@@ -1056,6 +1282,7 @@ async function handleListToolsRequest({
1056
1282
  {
1057
1283
  name: "aggregate",
1058
1284
  description: "Execute a MongoDB aggregation pipeline with optional execution plan analysis",
1285
+ execution: { taskSupport: "optional" },
1059
1286
  inputSchema: {
1060
1287
  type: "object",
1061
1288
  properties: {
@@ -1088,6 +1315,7 @@ async function handleListToolsRequest({
1088
1315
  {
1089
1316
  name: "update",
1090
1317
  description: "Update documents in a MongoDB collection",
1318
+ execution: { taskSupport: "optional" },
1091
1319
  inputSchema: {
1092
1320
  type: "object",
1093
1321
  properties: {
@@ -1124,6 +1352,7 @@ async function handleListToolsRequest({
1124
1352
  {
1125
1353
  name: "serverInfo",
1126
1354
  description: "Get MongoDB server information including version, storage engine, and other details",
1355
+ execution: { taskSupport: "optional" },
1127
1356
  inputSchema: {
1128
1357
  type: "object",
1129
1358
  properties: {
@@ -1137,6 +1366,7 @@ async function handleListToolsRequest({
1137
1366
  {
1138
1367
  name: "insert",
1139
1368
  description: "Insert one or more documents into a MongoDB collection",
1369
+ execution: { taskSupport: "optional" },
1140
1370
  inputSchema: {
1141
1371
  type: "object",
1142
1372
  properties: {
@@ -1174,6 +1404,7 @@ async function handleListToolsRequest({
1174
1404
  {
1175
1405
  name: "createIndex",
1176
1406
  description: "Create one or more indexes on a MongoDB collection",
1407
+ execution: { taskSupport: "optional" },
1177
1408
  inputSchema: {
1178
1409
  type: "object",
1179
1410
  properties: {
@@ -1240,6 +1471,7 @@ async function handleListToolsRequest({
1240
1471
  {
1241
1472
  name: "count",
1242
1473
  description: "Count documents in a collection matching a query",
1474
+ execution: { taskSupport: "optional" },
1243
1475
  inputSchema: {
1244
1476
  type: "object",
1245
1477
  properties: {
@@ -1270,6 +1502,7 @@ async function handleListToolsRequest({
1270
1502
  {
1271
1503
  name: "listCollections",
1272
1504
  description: "List all collections in the MongoDB database",
1505
+ execution: { taskSupport: "optional" },
1273
1506
  inputSchema: {
1274
1507
  type: "object",
1275
1508
  properties: {
@@ -1281,6 +1514,16 @@ async function handleListToolsRequest({
1281
1514
  type: "object",
1282
1515
  description: "Filter for collections"
1283
1516
  },
1517
+ skip: {
1518
+ type: "number",
1519
+ description: "Number of collections to skip before returning results",
1520
+ default: 0
1521
+ },
1522
+ limit: {
1523
+ type: "number",
1524
+ description: "Maximum number of collections to return",
1525
+ default: 20
1526
+ },
1284
1527
  objectIdMode: {
1285
1528
  type: "string",
1286
1529
  description: "Control how 24-character hex strings are handled",
@@ -1295,11 +1538,24 @@ async function handleListToolsRequest({
1295
1538
  }
1296
1539
 
1297
1540
  // src/server.ts
1541
+ function createSendProgress(extra) {
1542
+ const progressToken = extra._meta?.progressToken;
1543
+ if (progressToken === void 0) return void 0;
1544
+ return (progress, total, message) => extra.sendNotification({
1545
+ method: "notifications/progress",
1546
+ params: { progressToken, progress, total, message }
1547
+ });
1548
+ }
1298
1549
  function createServer(client, db, isReadOnlyMode = false, options = {}) {
1550
+ const taskStore = new InMemoryTaskStore();
1551
+ const taskMessageQueue = new InMemoryTaskMessageQueue();
1299
1552
  const server = new Server(
1300
1553
  {
1301
1554
  name: "mongodb",
1302
- version: "2.0.2",
1555
+ title: "MongoDB MCP Server",
1556
+ version: "2.1.1",
1557
+ description: "MCP server for MongoDB: query, aggregate, and manage collections with read-only mode, progress notifications, cancellation, and task support",
1558
+ websiteUrl: "https://github.com/kiliczsh/mcp-mongo-server",
1303
1559
  ...options
1304
1560
  },
1305
1561
  {
@@ -1307,46 +1563,111 @@ function createServer(client, db, isReadOnlyMode = false, options = {}) {
1307
1563
  completions: {},
1308
1564
  resources: {},
1309
1565
  tools: {},
1310
- prompts: {}
1566
+ prompts: {},
1567
+ tasks: {
1568
+ list: {},
1569
+ cancel: {},
1570
+ requests: { tools: { call: {} } }
1571
+ }
1311
1572
  },
1573
+ taskStore,
1574
+ taskMessageQueue,
1312
1575
  ...options
1313
1576
  }
1314
1577
  );
1315
1578
  server.setRequestHandler(
1316
1579
  PingRequestSchema,
1317
- (request) => handlePingRequest({ request, client, db, isReadOnlyMode })
1580
+ (request, extra) => handlePingRequest({
1581
+ request,
1582
+ client,
1583
+ db,
1584
+ isReadOnlyMode,
1585
+ signal: extra.signal
1586
+ })
1318
1587
  );
1319
1588
  server.setRequestHandler(
1320
1589
  ListResourcesRequestSchema,
1321
- (request) => handleListResourcesRequest({ request, client, db, isReadOnlyMode })
1590
+ (request, extra) => handleListResourcesRequest({
1591
+ request,
1592
+ client,
1593
+ db,
1594
+ isReadOnlyMode,
1595
+ signal: extra.signal
1596
+ })
1322
1597
  );
1323
1598
  server.setRequestHandler(
1324
1599
  ReadResourceRequestSchema,
1325
- (request) => handleReadResourceRequest({ request, client, db, isReadOnlyMode })
1600
+ (request, extra) => handleReadResourceRequest({
1601
+ request,
1602
+ client,
1603
+ db,
1604
+ isReadOnlyMode,
1605
+ signal: extra.signal,
1606
+ sendProgress: createSendProgress(extra)
1607
+ })
1326
1608
  );
1327
1609
  server.setRequestHandler(
1328
1610
  ListToolsRequestSchema,
1329
- (request) => handleListToolsRequest({ request, client, db, isReadOnlyMode })
1330
- );
1331
- server.setRequestHandler(
1332
- CallToolRequestSchema,
1333
- (request) => handleCallToolRequest({ request, client, db, isReadOnlyMode })
1611
+ (request, extra) => handleListToolsRequest({
1612
+ request,
1613
+ client,
1614
+ db,
1615
+ isReadOnlyMode,
1616
+ signal: extra.signal
1617
+ })
1334
1618
  );
1619
+ server.setRequestHandler(CallToolRequestSchema, (request, extra) => {
1620
+ const hasTaskParams = !!request.params.task;
1621
+ return handleCallToolRequest({
1622
+ request,
1623
+ client,
1624
+ db,
1625
+ isReadOnlyMode,
1626
+ signal: extra.signal,
1627
+ taskStore: hasTaskParams ? extra.taskStore : void 0,
1628
+ taskTtl: hasTaskParams ? extra.taskRequestedTtl : void 0
1629
+ });
1630
+ });
1335
1631
  server.setRequestHandler(
1336
1632
  ListPromptsRequestSchema,
1337
- (request) => handleListPromptsRequest({ request, client, db, isReadOnlyMode })
1633
+ (request, extra) => handleListPromptsRequest({
1634
+ request,
1635
+ client,
1636
+ db,
1637
+ isReadOnlyMode,
1638
+ signal: extra.signal
1639
+ })
1338
1640
  );
1339
1641
  server.setRequestHandler(
1340
1642
  GetPromptRequestSchema,
1341
- (request) => handleGetPromptRequest({ request, client, db, isReadOnlyMode })
1643
+ (request, extra) => handleGetPromptRequest({
1644
+ request,
1645
+ client,
1646
+ db,
1647
+ isReadOnlyMode,
1648
+ signal: extra.signal,
1649
+ sendProgress: createSendProgress(extra)
1650
+ })
1342
1651
  );
1343
1652
  server.setRequestHandler(
1344
1653
  ListResourceTemplatesRequestSchema,
1345
- (request) => handleListResourceTemplatesRequest({ request, client, db, isReadOnlyMode })
1654
+ (request, extra) => handleListResourceTemplatesRequest({
1655
+ request,
1656
+ client,
1657
+ db,
1658
+ isReadOnlyMode,
1659
+ signal: extra.signal
1660
+ })
1346
1661
  );
1347
1662
  server.setRequestHandler(
1348
1663
  CompleteRequestSchema,
1349
- (request) => handleCompletionRequest({ request, client, db, isReadOnlyMode })
1664
+ (request, extra) => handleCompletionRequest({
1665
+ request,
1666
+ client,
1667
+ db,
1668
+ isReadOnlyMode,
1669
+ signal: extra.signal
1670
+ })
1350
1671
  );
1351
1672
  return server;
1352
1673
  }
@@ -1359,6 +1680,7 @@ async function main() {
1359
1680
  let readOnlyMode = process.env.MCP_MONGODB_READONLY === "true" || false;
1360
1681
  let transportMode = "stdio";
1361
1682
  let port = Number(process.env.MCP_PORT) || 3001;
1683
+ const allowedOrigins = (process.env.MCP_HTTP_ALLOWED_ORIGINS || "").split(",").map((origin) => origin.trim()).filter(Boolean);
1362
1684
  for (let i = 0; i < args.length; i++) {
1363
1685
  if (args[i] === "--read-only" || args[i] === "-r") {
1364
1686
  readOnlyMode = true;
@@ -1375,6 +1697,10 @@ async function main() {
1375
1697
  console.error("Invalid port number.");
1376
1698
  process.exit(1);
1377
1699
  }
1700
+ } else if (args[i] === "--allowed-origins") {
1701
+ allowedOrigins.push(
1702
+ ...(args[++i] || "").split(",").map((origin) => origin.trim()).filter(Boolean)
1703
+ );
1378
1704
  } else if (!connectionUrl) {
1379
1705
  connectionUrl = args[i];
1380
1706
  }
@@ -1411,7 +1737,7 @@ async function main() {
1411
1737
  process.exit(1);
1412
1738
  }
1413
1739
  if (transportMode === "http") {
1414
- await startHttpServer(client, db, isReadOnlyMode, port);
1740
+ await startHttpServer(client, db, isReadOnlyMode, port, allowedOrigins);
1415
1741
  } else {
1416
1742
  await startStdioServer(client, db, isReadOnlyMode);
1417
1743
  }
@@ -1429,7 +1755,21 @@ async function startStdioServer(client, db, isReadOnlyMode) {
1429
1755
  await server.connect(transport);
1430
1756
  console.warn("Server connected successfully via stdio");
1431
1757
  }
1432
- async function startHttpServer(client, db, isReadOnlyMode, port) {
1758
+ function isOriginAllowed(origin, allowedOrigins) {
1759
+ if (!origin) {
1760
+ return true;
1761
+ }
1762
+ if (allowedOrigins.includes(origin)) {
1763
+ return true;
1764
+ }
1765
+ try {
1766
+ const { hostname } = new URL(origin);
1767
+ return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
1768
+ } catch {
1769
+ return false;
1770
+ }
1771
+ }
1772
+ async function startHttpServer(client, db, isReadOnlyMode, port, allowedOrigins) {
1433
1773
  const app = createMcpExpressApp({ host: "0.0.0.0" });
1434
1774
  app.use((req, res, next) => {
1435
1775
  const start = Date.now();
@@ -1448,6 +1788,21 @@ async function startHttpServer(client, db, isReadOnlyMode, port) {
1448
1788
  });
1449
1789
  next();
1450
1790
  });
1791
+ app.use((req, res, next) => {
1792
+ const origin = req.headers.origin;
1793
+ if (!isOriginAllowed(origin, allowedOrigins)) {
1794
+ res.status(403).json({
1795
+ jsonrpc: "2.0",
1796
+ error: {
1797
+ code: -32603,
1798
+ message: "Forbidden: origin not allowed"
1799
+ },
1800
+ id: null
1801
+ });
1802
+ return;
1803
+ }
1804
+ next();
1805
+ });
1451
1806
  app.post("/mcp", async (req, res) => {
1452
1807
  const server = createServer(client, db, isReadOnlyMode);
1453
1808
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-mongo-server",
3
- "version": "2.0.2",
3
+ "version": "2.1.1",
4
4
  "description": "A Model Context Protocol server for MongoDB connections",
5
5
  "private": false,
6
6
  "type": "module",
@@ -23,14 +23,14 @@
23
23
  "inspector": "mcp-inspector --config inspector-config.json --server mongodb"
24
24
  },
25
25
  "dependencies": {
26
- "@modelcontextprotocol/sdk": "^1.26.0",
27
- "mongodb": "^7.1.0",
28
- "zod": "^3.25.76"
26
+ "@modelcontextprotocol/sdk": "^1.29.0",
27
+ "mongodb": "^7.5.0",
28
+ "zod": "^4.4.3"
29
29
  },
30
30
  "devDependencies": {
31
- "@biomejs/biome": "^2.3.14",
32
- "@modelcontextprotocol/inspector": "^0.19.0",
33
- "@types/node": "^25.2.1",
31
+ "@biomejs/biome": "^2.5.5",
32
+ "@modelcontextprotocol/inspector": "^1.0.0",
33
+ "@types/node": "^26.1.1",
34
34
  "tsup": "^8.5.1",
35
35
  "typescript": "^5.9.3"
36
36
  },
@@ -56,5 +56,21 @@
56
56
  "type": "git",
57
57
  "url": "https://github.com/kiliczsh/mcp-mongo-server"
58
58
  },
59
- "homepage": "https://github.com/kiliczsh/mcp-mongo-server"
59
+ "homepage": "https://github.com/kiliczsh/mcp-mongo-server",
60
+ "overrides": {
61
+ "qs": "^6.15.2",
62
+ "hono": "^4.12.18",
63
+ "@hono/node-server": "^1.19.10",
64
+ "express-rate-limit": "^8.2.2",
65
+ "fast-uri": "^3.1.2",
66
+ "ws": "^8.21.1",
67
+ "shell-quote": "^1.10.0",
68
+ "minimatch": "^3.1.3",
69
+ "brace-expansion": "^1.1.13",
70
+ "rollup": "^4.59.0",
71
+ "picomatch": "^4.0.4",
72
+ "body-parser": "^2.3.0",
73
+ "ip-address": "^10.2.0",
74
+ "esbuild": "^0.28.1"
75
+ }
60
76
  }