mcp-mongo-server 2.0.2 → 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.
Files changed (2) hide show
  1. package/build/index.js +405 -86
  2. package/package.json +24 -8
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",
@@ -60,7 +68,10 @@ async function handleCallToolRequest({
60
68
  request,
61
69
  client,
62
70
  db,
63
- isReadOnlyMode
71
+ isReadOnlyMode,
72
+ signal,
73
+ taskStore,
74
+ taskTtl
64
75
  }) {
65
76
  const { name, arguments: args = {} } = request.params;
66
77
  const operation = name;
@@ -76,37 +87,87 @@ async function handleCallToolRequest({
76
87
  }
77
88
  Object.assign(args, filteredArgs);
78
89
  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
- );
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 };
87
132
  }
88
- collection = db.collection(collectionName);
89
- validateCollection(collection);
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;
145
+ }
146
+ return toolExecutionError(
147
+ error instanceof Error ? error.message : "Unknown error"
148
+ );
90
149
  }
150
+ }
151
+ async function executeOperation(operation, collection, db, isReadOnlyMode, args, objectIdMode, signal) {
91
152
  switch (operation) {
92
153
  case "query":
93
- return handleQuery(collection, args, objectIdMode);
154
+ return handleQuery(collection, args, objectIdMode, signal);
94
155
  case "aggregate":
95
- return handleAggregate(collection, args, objectIdMode);
156
+ return handleAggregate(collection, args, objectIdMode, signal);
96
157
  case "update":
97
- return handleUpdate(collection, args, objectIdMode);
158
+ return handleUpdate(collection, args, objectIdMode, signal);
98
159
  case "serverInfo":
99
- return handleServerInfo(db, isReadOnlyMode, args);
160
+ return handleServerInfo(db, isReadOnlyMode, args, signal);
100
161
  case "insert":
101
- return handleInsert(collection, args, objectIdMode);
162
+ return handleInsert(collection, args, objectIdMode, signal);
102
163
  case "createIndex":
103
- return handleCreateIndex(collection, args, objectIdMode);
164
+ return handleCreateIndex(collection, args, objectIdMode, signal);
104
165
  case "count":
105
- return handleCount(collection, args, objectIdMode);
166
+ return handleCount(collection, args, objectIdMode, signal);
106
167
  case "listCollections":
107
- return handleListCollections(db, args, objectIdMode);
168
+ return handleListCollections(db, args, objectIdMode, signal);
108
169
  default:
109
- throw new Error(`Unknown operation: ${operation}`);
170
+ throw new McpError(ErrorCode.InvalidParams, `Unknown tool: ${operation}`);
110
171
  }
111
172
  }
112
173
  function validateOperation(operation) {
@@ -121,7 +182,7 @@ function validateOperation(operation) {
121
182
  "listCollections"
122
183
  ];
123
184
  if (!validOperations.includes(operation)) {
124
- throw new Error(`Unknown operation: ${operation}`);
185
+ throw new McpError(ErrorCode.InvalidParams, `Unknown tool: ${operation}`);
125
186
  }
126
187
  }
127
188
  function validateCollection(collection) {
@@ -269,40 +330,64 @@ function formatResponse(data) {
269
330
  ]
270
331
  };
271
332
  }
333
+ function toolExecutionError(message) {
334
+ return {
335
+ content: [{ type: "text", text: message }],
336
+ isError: true
337
+ };
338
+ }
272
339
  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}`);
340
+ if (error instanceof Error && error.name === "AbortError") {
341
+ throw error;
276
342
  }
277
- throw new Error(`Failed to ${operation} ${context}: Unknown error`);
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}`);
278
346
  }
279
- async function handleQuery(collection, args, objectIdMode = "auto") {
347
+ async function handleQuery(collection, args, objectIdMode = "auto", signal) {
280
348
  if (!collection) {
281
349
  throw new Error("Collection is required for query operation");
282
350
  }
283
- const { filter, projection, limit, explain, sort } = args;
351
+ const { filter, projection, explain, sort } = args;
352
+ const limit = args.limit || 10;
353
+ const skip = args.skip || 0;
284
354
  const queryFilter = parseFilter(filter, objectIdMode);
285
355
  try {
286
356
  if (explain) {
357
+ signal?.throwIfAborted();
287
358
  const explainResult = await collection.find(queryFilter, {
288
359
  projection,
289
- limit: limit || 100,
360
+ limit,
361
+ skip,
290
362
  sort
291
363
  }).explain(explain);
292
364
  return formatResponse(explainResult);
293
365
  }
294
- const cursor = collection.find(queryFilter, {
295
- projection,
296
- limit: limit || 100,
297
- sort
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
+ }
298
385
  });
299
- const results = await cursor.toArray();
300
- return formatResponse(results);
301
386
  } catch (error) {
302
387
  return handleError(error, "query", collection.collectionName);
303
388
  }
304
389
  }
305
- async function handleAggregate(collection, args, objectIdMode = "auto") {
390
+ async function handleAggregate(collection, args, objectIdMode = "auto", signal) {
306
391
  if (!collection) {
307
392
  throw new Error("Collection is required for aggregate operation");
308
393
  }
@@ -321,6 +406,7 @@ async function handleAggregate(collection, args, objectIdMode = "auto") {
321
406
  });
322
407
  try {
323
408
  if (explain) {
409
+ signal?.throwIfAborted();
324
410
  const explainResult = await collection.aggregate(processedPipeline, {
325
411
  explain: {
326
412
  verbosity: explain
@@ -328,13 +414,19 @@ async function handleAggregate(collection, args, objectIdMode = "auto") {
328
414
  }).toArray();
329
415
  return formatResponse(explainResult);
330
416
  }
417
+ signal?.throwIfAborted();
331
418
  const results = await collection.aggregate(processedPipeline).toArray();
332
- return formatResponse(results);
419
+ return formatResponse({
420
+ results,
421
+ metadata: {
422
+ returned: results.length
423
+ }
424
+ });
333
425
  } catch (error) {
334
426
  return handleError(error, "aggregate", collection.collectionName);
335
427
  }
336
428
  }
337
- async function handleUpdate(collection, args, objectIdMode = "auto") {
429
+ async function handleUpdate(collection, args, objectIdMode = "auto", signal) {
338
430
  if (!collection) {
339
431
  throw new Error("Collection is required for update operation");
340
432
  }
@@ -375,6 +467,7 @@ async function handleUpdate(collection, args, objectIdMode = "auto") {
375
467
  multi: !!multi
376
468
  };
377
469
  const updateMethod = options.multi ? "updateMany" : "updateOne";
470
+ signal?.throwIfAborted();
378
471
  const result = await collection[updateMethod](
379
472
  queryFilter,
380
473
  processedUpdate,
@@ -390,12 +483,14 @@ async function handleUpdate(collection, args, objectIdMode = "auto") {
390
483
  return handleError(error, "update", collection.collectionName);
391
484
  }
392
485
  }
393
- async function handleServerInfo(db, isReadOnlyMode, args) {
486
+ async function handleServerInfo(db, isReadOnlyMode, args, signal) {
394
487
  const { includeDebugInfo } = args;
395
488
  try {
489
+ signal?.throwIfAborted();
396
490
  const buildInfo = await db.command({ buildInfo: 1 });
397
491
  let serverStatus = null;
398
492
  if (includeDebugInfo) {
493
+ signal?.throwIfAborted();
399
494
  serverStatus = await db.command({ serverStatus: 1 });
400
495
  }
401
496
  const serverInfo = {
@@ -440,7 +535,7 @@ async function handleServerInfo(db, isReadOnlyMode, args) {
440
535
  return handleError(error, "get server information");
441
536
  }
442
537
  }
443
- async function handleInsert(collection, args, objectIdMode = "auto") {
538
+ async function handleInsert(collection, args, objectIdMode = "auto", signal) {
444
539
  if (!collection) {
445
540
  throw new Error("Collection is required for insert operation");
446
541
  }
@@ -466,6 +561,7 @@ async function handleInsert(collection, args, objectIdMode = "auto") {
466
561
  writeConcern,
467
562
  bypassDocumentValidation
468
563
  };
564
+ signal?.throwIfAborted();
469
565
  const result = await collection.insertMany(
470
566
  processedDocuments,
471
567
  options
@@ -488,7 +584,7 @@ async function handleInsert(collection, args, objectIdMode = "auto") {
488
584
  return handleError(error, "insert", collection.collectionName);
489
585
  }
490
586
  }
491
- async function handleCreateIndex(collection, args, objectIdMode = "auto") {
587
+ async function handleCreateIndex(collection, args, objectIdMode = "auto", signal) {
492
588
  if (!collection) {
493
589
  throw new Error("Collection is required for createIndex operation");
494
590
  }
@@ -517,6 +613,7 @@ async function handleCreateIndex(collection, args, objectIdMode = "auto") {
517
613
  const indexOptions = {
518
614
  commitQuorum: typeof commitQuorum === "number" ? commitQuorum : void 0
519
615
  };
616
+ signal?.throwIfAborted();
520
617
  const result = await collection.createIndexes(
521
618
  processedIndexes,
522
619
  indexOptions
@@ -531,7 +628,7 @@ async function handleCreateIndex(collection, args, objectIdMode = "auto") {
531
628
  return handleError(error, "create indexes", collection.collectionName);
532
629
  }
533
630
  }
534
- async function handleCount(collection, args, objectIdMode = "auto") {
631
+ async function handleCount(collection, args, objectIdMode = "auto", signal) {
535
632
  if (!collection) {
536
633
  throw new Error("Collection is required for count operation");
537
634
  }
@@ -551,6 +648,7 @@ async function handleCount(collection, args, objectIdMode = "auto") {
551
648
  delete options[key];
552
649
  }
553
650
  }
651
+ signal?.throwIfAborted();
554
652
  const count = await collection.countDocuments(countQuery, options);
555
653
  return formatResponse({
556
654
  count,
@@ -560,8 +658,10 @@ async function handleCount(collection, args, objectIdMode = "auto") {
560
658
  return handleError(error, "count documents", collection.collectionName);
561
659
  }
562
660
  }
563
- async function handleListCollections(db, args, objectIdMode = "auto") {
661
+ async function handleListCollections(db, args, objectIdMode = "auto", signal) {
564
662
  const { nameOnly, filter } = args;
663
+ const skip = args.skip || 0;
664
+ const limit = args.limit || 20;
565
665
  let processedFilter = filter;
566
666
  if (filter && typeof filter === "object") {
567
667
  processedFilter = processObjectIdInFilter(
@@ -571,20 +671,58 @@ async function handleListCollections(db, args, objectIdMode = "auto") {
571
671
  }
572
672
  try {
573
673
  const options = processedFilter ? { filter: processedFilter } : {};
674
+ signal?.throwIfAborted();
574
675
  const collections = await db.listCollections(options).toArray();
575
- const result = nameOnly ? collections.map((collection) => collection.name) : collections;
576
- return formatResponse(result);
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
+ });
577
689
  } catch (error) {
578
690
  return handleError(error, "list collections");
579
691
  }
580
692
  }
581
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
+
582
719
  // src/schemas/completion.ts
583
720
  async function handleCompletionRequest({
584
721
  request,
585
722
  client,
586
723
  db,
587
- isReadOnlyMode
724
+ isReadOnlyMode,
725
+ signal
588
726
  }) {
589
727
  const { ref, argument } = request.params;
590
728
  if (ref.type === "ref/prompt") {
@@ -593,7 +731,8 @@ async function handleCompletionRequest({
593
731
  db,
594
732
  isReadOnlyMode,
595
733
  ref.name,
596
- argument
734
+ argument,
735
+ signal
597
736
  );
598
737
  }
599
738
  if (ref.type === "ref/resource") {
@@ -602,34 +741,46 @@ async function handleCompletionRequest({
602
741
  db,
603
742
  isReadOnlyMode,
604
743
  ref.uri,
605
- argument
744
+ argument,
745
+ signal
606
746
  );
607
747
  }
608
748
  return emptyCompletionResult();
609
749
  }
610
- async function handlePromptCompletion(client, db, isReadOnlyMode, promptName, argument) {
750
+ async function handlePromptCompletion(client, db, isReadOnlyMode, promptName, argument, signal) {
611
751
  if (!promptName) {
612
752
  return emptyCompletionResult();
613
753
  }
614
754
  if (argument.name === "collection") {
615
- return await completeCollectionNames(argument.value, db, isReadOnlyMode);
755
+ return await completeCollectionNames(
756
+ argument.value,
757
+ db,
758
+ isReadOnlyMode,
759
+ signal
760
+ );
616
761
  }
617
762
  return emptyCompletionResult();
618
763
  }
619
- async function handleResourceCompletion(client, db, isReadOnlyMode, promptName, argument) {
764
+ async function handleResourceCompletion(client, db, isReadOnlyMode, promptName, argument, signal) {
620
765
  if (!promptName) {
621
766
  return emptyCompletionResult();
622
767
  }
623
768
  if (argument.name === "collection") {
624
- return await completeCollectionNames(argument.value, db, isReadOnlyMode);
769
+ return await completeCollectionNames(
770
+ argument.value,
771
+ db,
772
+ isReadOnlyMode,
773
+ signal
774
+ );
625
775
  }
626
776
  return emptyCompletionResult();
627
777
  }
628
- async function completeCollectionNames(partialValue, db, isReadOnlyMode) {
778
+ async function completeCollectionNames(partialValue, db, isReadOnlyMode, signal) {
629
779
  try {
630
780
  console.warn(
631
781
  `Completing collection names with partial value: ${partialValue}`
632
782
  );
783
+ signal?.throwIfAborted();
633
784
  const collections = await db.listCollections().toArray();
634
785
  const matchingCollections = collections.map(
635
786
  (c) => c.name
@@ -641,14 +792,17 @@ async function completeCollectionNames(partialValue, db, isReadOnlyMode) {
641
792
  )
642
793
  ).sort();
643
794
  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;
795
+ const PAGE_SIZE = 100;
796
+ const { items: limitedResults, nextCursor } = paginate(
797
+ matchingCollections,
798
+ void 0,
799
+ PAGE_SIZE
800
+ );
647
801
  return {
648
802
  completion: {
649
803
  values: limitedResults,
650
804
  total: matchingCollections.length,
651
- hasMore
805
+ hasMore: !!nextCursor
652
806
  }
653
807
  };
654
808
  } catch (error) {
@@ -671,12 +825,14 @@ async function handlePingRequest({
671
825
  request,
672
826
  client,
673
827
  db,
674
- isReadOnlyMode
828
+ isReadOnlyMode,
829
+ signal
675
830
  }) {
676
831
  try {
677
832
  if (!client) {
678
833
  throw new Error("MongoDB connection is not available");
679
834
  }
835
+ signal?.throwIfAborted();
680
836
  const pong = await db.command({ ping: 1 });
681
837
  if (pong.ok !== 1) {
682
838
  throw new Error(`MongoDB ping failed: ${pong.errmsg}`);
@@ -695,7 +851,8 @@ async function handleListPromptsRequest({
695
851
  request,
696
852
  client,
697
853
  db,
698
- isReadOnlyMode
854
+ isReadOnlyMode,
855
+ signal
699
856
  }) {
700
857
  return {
701
858
  prompts: [
@@ -717,7 +874,9 @@ async function handleGetPromptRequest({
717
874
  request,
718
875
  client,
719
876
  db,
720
- isReadOnlyMode
877
+ isReadOnlyMode,
878
+ signal,
879
+ sendProgress
721
880
  }) {
722
881
  const { name, arguments: args = {} } = request.params;
723
882
  if (name !== "analyze_collection") {
@@ -732,8 +891,14 @@ async function handleGetPromptRequest({
732
891
  if (collection.collectionName.startsWith("system.")) {
733
892
  throw new Error("Access to system collections is not allowed");
734
893
  }
894
+ await sendProgress?.(1, 3, "Getting schema");
895
+ signal?.throwIfAborted();
735
896
  const schemaSample = await collection.findOne({});
897
+ await sendProgress?.(2, 3, "Getting stats");
898
+ signal?.throwIfAborted();
736
899
  const stats = await collection.aggregate([{ $collStats: { count: {} } }]).toArray();
900
+ await sendProgress?.(3, 3, "Fetching samples");
901
+ signal?.throwIfAborted();
737
902
  const sampleDocs = await collection.find({}).limit(5).toArray();
738
903
  const documentCount = stats[0]?.count ?? "unknown";
739
904
  return {
@@ -864,7 +1029,9 @@ async function handleReadResourceRequest({
864
1029
  request,
865
1030
  client,
866
1031
  db,
867
- isReadOnlyMode
1032
+ isReadOnlyMode,
1033
+ signal,
1034
+ sendProgress
868
1035
  }) {
869
1036
  const url = new URL(request.params.uri);
870
1037
  const collectionName = url.pathname.replace(/^\//, "");
@@ -872,18 +1039,25 @@ async function handleReadResourceRequest({
872
1039
  const collection = db.collection(collectionName);
873
1040
  const sampleSize = 100;
874
1041
  let sampleDocuments = [];
1042
+ await sendProgress?.(1, 4, "Sampling documents");
875
1043
  try {
1044
+ signal?.throwIfAborted();
876
1045
  sampleDocuments = await collection.aggregate([{ $sample: { size: sampleSize } }]).toArray();
877
1046
  } catch (sampleError) {
878
1047
  console.warn(
879
1048
  `$sample aggregation failed for ${collectionName}, falling back to sequential scan: ${sampleError}`
880
1049
  );
1050
+ signal?.throwIfAborted();
881
1051
  sampleDocuments = await collection.find({}).limit(sampleSize).toArray();
882
1052
  }
1053
+ await sendProgress?.(2, 4, "Getting indexes");
1054
+ signal?.throwIfAborted();
883
1055
  const indexes = await collection.indexes();
884
1056
  const inferredSchema = inferSchemaFromSamples(sampleDocuments);
1057
+ await sendProgress?.(3, 4, "Counting documents");
885
1058
  let documentCount = null;
886
1059
  try {
1060
+ signal?.throwIfAborted();
887
1061
  documentCount = await Promise.race([
888
1062
  collection.countDocuments(),
889
1063
  new Promise(
@@ -898,12 +1072,14 @@ async function handleReadResourceRequest({
898
1072
  `Count operation failed or timed out for ${collectionName}: ${countError}`
899
1073
  );
900
1074
  try {
1075
+ signal?.throwIfAborted();
901
1076
  const stats = await db.command({ collStats: collectionName });
902
1077
  documentCount = stats.count;
903
1078
  } catch {
904
1079
  documentCount = "unknown (count operation timed out)";
905
1080
  }
906
1081
  }
1082
+ await sendProgress?.(4, 4, "Done");
907
1083
  const schema = {
908
1084
  type: "collection",
909
1085
  name: collectionName,
@@ -940,17 +1116,23 @@ async function handleListResourcesRequest({
940
1116
  request,
941
1117
  client,
942
1118
  db,
943
- isReadOnlyMode
1119
+ isReadOnlyMode,
1120
+ signal
944
1121
  }) {
945
1122
  try {
1123
+ signal?.throwIfAborted();
946
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);
947
1133
  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
- }))
1134
+ resources: items,
1135
+ ...nextCursor ? { nextCursor } : {}
954
1136
  };
955
1137
  } catch (error) {
956
1138
  if (error instanceof Error) {
@@ -965,7 +1147,8 @@ async function handleListResourceTemplatesRequest({
965
1147
  request,
966
1148
  client,
967
1149
  db,
968
- isReadOnlyMode
1150
+ isReadOnlyMode,
1151
+ signal
969
1152
  }) {
970
1153
  return {
971
1154
  resourceTemplates: [
@@ -1011,13 +1194,15 @@ async function handleListToolsRequest({
1011
1194
  request,
1012
1195
  client,
1013
1196
  db,
1014
- isReadOnlyMode
1197
+ isReadOnlyMode,
1198
+ signal
1015
1199
  }) {
1016
1200
  return {
1017
1201
  tools: [
1018
1202
  {
1019
1203
  name: "query",
1020
1204
  description: "Execute a MongoDB query with optional execution plan analysis",
1205
+ execution: { taskSupport: "optional" },
1021
1206
  inputSchema: {
1022
1207
  type: "object",
1023
1208
  properties: {
@@ -1038,6 +1223,11 @@ async function handleListToolsRequest({
1038
1223
  description: "Maximum number of documents to return",
1039
1224
  default: 10
1040
1225
  },
1226
+ skip: {
1227
+ type: "number",
1228
+ description: "Number of documents to skip before returning results",
1229
+ default: 0
1230
+ },
1041
1231
  explain: {
1042
1232
  type: "string",
1043
1233
  description: "Optional: Get query execution information",
@@ -1056,6 +1246,7 @@ async function handleListToolsRequest({
1056
1246
  {
1057
1247
  name: "aggregate",
1058
1248
  description: "Execute a MongoDB aggregation pipeline with optional execution plan analysis",
1249
+ execution: { taskSupport: "optional" },
1059
1250
  inputSchema: {
1060
1251
  type: "object",
1061
1252
  properties: {
@@ -1088,6 +1279,7 @@ async function handleListToolsRequest({
1088
1279
  {
1089
1280
  name: "update",
1090
1281
  description: "Update documents in a MongoDB collection",
1282
+ execution: { taskSupport: "optional" },
1091
1283
  inputSchema: {
1092
1284
  type: "object",
1093
1285
  properties: {
@@ -1124,6 +1316,7 @@ async function handleListToolsRequest({
1124
1316
  {
1125
1317
  name: "serverInfo",
1126
1318
  description: "Get MongoDB server information including version, storage engine, and other details",
1319
+ execution: { taskSupport: "optional" },
1127
1320
  inputSchema: {
1128
1321
  type: "object",
1129
1322
  properties: {
@@ -1137,6 +1330,7 @@ async function handleListToolsRequest({
1137
1330
  {
1138
1331
  name: "insert",
1139
1332
  description: "Insert one or more documents into a MongoDB collection",
1333
+ execution: { taskSupport: "optional" },
1140
1334
  inputSchema: {
1141
1335
  type: "object",
1142
1336
  properties: {
@@ -1174,6 +1368,7 @@ async function handleListToolsRequest({
1174
1368
  {
1175
1369
  name: "createIndex",
1176
1370
  description: "Create one or more indexes on a MongoDB collection",
1371
+ execution: { taskSupport: "optional" },
1177
1372
  inputSchema: {
1178
1373
  type: "object",
1179
1374
  properties: {
@@ -1240,6 +1435,7 @@ async function handleListToolsRequest({
1240
1435
  {
1241
1436
  name: "count",
1242
1437
  description: "Count documents in a collection matching a query",
1438
+ execution: { taskSupport: "optional" },
1243
1439
  inputSchema: {
1244
1440
  type: "object",
1245
1441
  properties: {
@@ -1270,6 +1466,7 @@ async function handleListToolsRequest({
1270
1466
  {
1271
1467
  name: "listCollections",
1272
1468
  description: "List all collections in the MongoDB database",
1469
+ execution: { taskSupport: "optional" },
1273
1470
  inputSchema: {
1274
1471
  type: "object",
1275
1472
  properties: {
@@ -1281,6 +1478,16 @@ async function handleListToolsRequest({
1281
1478
  type: "object",
1282
1479
  description: "Filter for collections"
1283
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
+ },
1284
1491
  objectIdMode: {
1285
1492
  type: "string",
1286
1493
  description: "Control how 24-character hex strings are handled",
@@ -1295,11 +1502,24 @@ async function handleListToolsRequest({
1295
1502
  }
1296
1503
 
1297
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
+ }
1298
1513
  function createServer(client, db, isReadOnlyMode = false, options = {}) {
1514
+ const taskStore = new InMemoryTaskStore();
1515
+ const taskMessageQueue = new InMemoryTaskMessageQueue();
1299
1516
  const server = new Server(
1300
1517
  {
1301
1518
  name: "mongodb",
1302
- version: "2.0.2",
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",
1303
1523
  ...options
1304
1524
  },
1305
1525
  {
@@ -1307,46 +1527,111 @@ function createServer(client, db, isReadOnlyMode = false, options = {}) {
1307
1527
  completions: {},
1308
1528
  resources: {},
1309
1529
  tools: {},
1310
- prompts: {}
1530
+ prompts: {},
1531
+ tasks: {
1532
+ list: {},
1533
+ cancel: {},
1534
+ requests: { tools: { call: {} } }
1535
+ }
1311
1536
  },
1537
+ taskStore,
1538
+ taskMessageQueue,
1312
1539
  ...options
1313
1540
  }
1314
1541
  );
1315
1542
  server.setRequestHandler(
1316
1543
  PingRequestSchema,
1317
- (request) => handlePingRequest({ request, client, db, isReadOnlyMode })
1544
+ (request, extra) => handlePingRequest({
1545
+ request,
1546
+ client,
1547
+ db,
1548
+ isReadOnlyMode,
1549
+ signal: extra.signal
1550
+ })
1318
1551
  );
1319
1552
  server.setRequestHandler(
1320
1553
  ListResourcesRequestSchema,
1321
- (request) => handleListResourcesRequest({ request, client, db, isReadOnlyMode })
1554
+ (request, extra) => handleListResourcesRequest({
1555
+ request,
1556
+ client,
1557
+ db,
1558
+ isReadOnlyMode,
1559
+ signal: extra.signal
1560
+ })
1322
1561
  );
1323
1562
  server.setRequestHandler(
1324
1563
  ReadResourceRequestSchema,
1325
- (request) => handleReadResourceRequest({ request, client, db, isReadOnlyMode })
1564
+ (request, extra) => handleReadResourceRequest({
1565
+ request,
1566
+ client,
1567
+ db,
1568
+ isReadOnlyMode,
1569
+ signal: extra.signal,
1570
+ sendProgress: createSendProgress(extra)
1571
+ })
1326
1572
  );
1327
1573
  server.setRequestHandler(
1328
1574
  ListToolsRequestSchema,
1329
- (request) => handleListToolsRequest({ request, client, db, isReadOnlyMode })
1330
- );
1331
- server.setRequestHandler(
1332
- CallToolRequestSchema,
1333
- (request) => handleCallToolRequest({ request, client, db, isReadOnlyMode })
1575
+ (request, extra) => handleListToolsRequest({
1576
+ request,
1577
+ client,
1578
+ db,
1579
+ isReadOnlyMode,
1580
+ signal: extra.signal
1581
+ })
1334
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
+ });
1335
1595
  server.setRequestHandler(
1336
1596
  ListPromptsRequestSchema,
1337
- (request) => handleListPromptsRequest({ request, client, db, isReadOnlyMode })
1597
+ (request, extra) => handleListPromptsRequest({
1598
+ request,
1599
+ client,
1600
+ db,
1601
+ isReadOnlyMode,
1602
+ signal: extra.signal
1603
+ })
1338
1604
  );
1339
1605
  server.setRequestHandler(
1340
1606
  GetPromptRequestSchema,
1341
- (request) => handleGetPromptRequest({ request, client, db, isReadOnlyMode })
1607
+ (request, extra) => handleGetPromptRequest({
1608
+ request,
1609
+ client,
1610
+ db,
1611
+ isReadOnlyMode,
1612
+ signal: extra.signal,
1613
+ sendProgress: createSendProgress(extra)
1614
+ })
1342
1615
  );
1343
1616
  server.setRequestHandler(
1344
1617
  ListResourceTemplatesRequestSchema,
1345
- (request) => handleListResourceTemplatesRequest({ request, client, db, isReadOnlyMode })
1618
+ (request, extra) => handleListResourceTemplatesRequest({
1619
+ request,
1620
+ client,
1621
+ db,
1622
+ isReadOnlyMode,
1623
+ signal: extra.signal
1624
+ })
1346
1625
  );
1347
1626
  server.setRequestHandler(
1348
1627
  CompleteRequestSchema,
1349
- (request) => handleCompletionRequest({ request, client, db, isReadOnlyMode })
1628
+ (request, extra) => handleCompletionRequest({
1629
+ request,
1630
+ client,
1631
+ db,
1632
+ isReadOnlyMode,
1633
+ signal: extra.signal
1634
+ })
1350
1635
  );
1351
1636
  return server;
1352
1637
  }
@@ -1359,6 +1644,7 @@ async function main() {
1359
1644
  let readOnlyMode = process.env.MCP_MONGODB_READONLY === "true" || false;
1360
1645
  let transportMode = "stdio";
1361
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);
1362
1648
  for (let i = 0; i < args.length; i++) {
1363
1649
  if (args[i] === "--read-only" || args[i] === "-r") {
1364
1650
  readOnlyMode = true;
@@ -1375,6 +1661,10 @@ async function main() {
1375
1661
  console.error("Invalid port number.");
1376
1662
  process.exit(1);
1377
1663
  }
1664
+ } else if (args[i] === "--allowed-origins") {
1665
+ allowedOrigins.push(
1666
+ ...(args[++i] || "").split(",").map((origin) => origin.trim()).filter(Boolean)
1667
+ );
1378
1668
  } else if (!connectionUrl) {
1379
1669
  connectionUrl = args[i];
1380
1670
  }
@@ -1411,7 +1701,7 @@ async function main() {
1411
1701
  process.exit(1);
1412
1702
  }
1413
1703
  if (transportMode === "http") {
1414
- await startHttpServer(client, db, isReadOnlyMode, port);
1704
+ await startHttpServer(client, db, isReadOnlyMode, port, allowedOrigins);
1415
1705
  } else {
1416
1706
  await startStdioServer(client, db, isReadOnlyMode);
1417
1707
  }
@@ -1429,7 +1719,21 @@ async function startStdioServer(client, db, isReadOnlyMode) {
1429
1719
  await server.connect(transport);
1430
1720
  console.warn("Server connected successfully via stdio");
1431
1721
  }
1432
- async function startHttpServer(client, db, isReadOnlyMode, port) {
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) {
1433
1737
  const app = createMcpExpressApp({ host: "0.0.0.0" });
1434
1738
  app.use((req, res, next) => {
1435
1739
  const start = Date.now();
@@ -1448,6 +1752,21 @@ async function startHttpServer(client, db, isReadOnlyMode, port) {
1448
1752
  });
1449
1753
  next();
1450
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;
1767
+ }
1768
+ next();
1769
+ });
1451
1770
  app.post("/mcp", async (req, res) => {
1452
1771
  const server = createServer(client, db, isReadOnlyMode);
1453
1772
  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.0",
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
  }