mcp-mongo-server 3.0.1 → 3.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 (3) hide show
  1. package/README.md +39 -10
  2. package/build/index.js +489 -143
  3. package/package.json +4 -1
package/README.md CHANGED
@@ -91,6 +91,7 @@ npx -y mcp-mongo-server "mongodb://..." --transport http --port 3001 --allowed-o
91
91
  | Variable | Description |
92
92
  |----------|-------------|
93
93
  | `MCP_MONGODB_URI` | MongoDB connection URI (alternative to the argument) |
94
+ | `MCP_MONGODB_CONNECTIONS` | JSON object of named MongoDB connections and their database permissions |
94
95
  | `MCP_MONGODB_READONLY` | Enable read-only mode (`"true"`) |
95
96
  | `MCP_MONGODB_ALLOW_CROSS_DB` | Allow cross-database aggregation stages (`"true"`) |
96
97
  | `MCP_MONGODB_ALLOW_SERVER_JS` | Allow server-side JavaScript operators (`"true"`) |
@@ -99,22 +100,50 @@ npx -y mcp-mongo-server "mongodb://..." --transport http --port 3001 --allowed-o
99
100
  | `MCP_HTTP_JSON_LIMIT` | Max HTTP request body size (default `10mb`) |
100
101
  | `MCP_HTTP_AUTH_TOKEN` | Bearer token required to access the HTTP endpoint |
101
102
 
103
+ ### Named connections
104
+
105
+ Set `MCP_MONGODB_CONNECTIONS` to a JSON string. This MCP client configuration
106
+ uses a local build and the two [Docker test databases](docs/development.md#local-mongodb-test-environment).
107
+
108
+ ```json
109
+ {
110
+ "mcpServers": {
111
+ "mongodb": {
112
+ "command": "node",
113
+ "args": [
114
+ "/absolute/path/to/mcp-mongo-server/build/index.js"
115
+ ],
116
+ "env": {
117
+ "MCP_MONGODB_CONNECTIONS": "{\"default\":{\"uri\":\"mongodb://127.0.0.1:37017/mcp_test\"},\"analytics\":{\"uri\":\"mongodb://127.0.0.1:37018/mcp_test?directConnection=true\",\"databases\":[\"mcp_test\",\"mcp_test_secondary\"],\"readOnly\":true}}"
118
+ }
119
+ }
120
+ }
121
+ }
122
+ ```
123
+
124
+ - Discover targets with `listConnections` and `listDatabases`. Pass `connectionId` and `database` to database tools and the collection analysis prompt. Omitted selectors use the `default` connection and its URI database.
125
+ - Omit `databases` to allow only the URI database. If supplied, the list must include the URI database. Global read-only mode applies to every connection.
126
+ - A positional URI or `MCP_MONGODB_URI` can supply `default` when the JSON contains only secondary connections. Named connections default to a 5-second server selection timeout. Set `serverSelectionTimeoutMS` in the URI to override it.
127
+ - If a connection is offline, `resources/list` reports its ID and fails. Tools targeting other connections remain usable.
128
+
102
129
  ## Security
103
130
 
104
- **Database scope.** The server operates on the database in your connection
105
- string. Aggregation stages that reach another database (`$out`, `$merge`,
131
+ **Database scope.** The server operates on the selected database.
132
+ Named connections restrict selection to their configured databases.
133
+ Aggregation stages that reach another database (`$out`, `$merge`,
106
134
  `$lookup` with an explicit `db`) are rejected by default, so a pipeline can't
107
135
  quietly read from or write to databases you didn't point it at. Enable them
108
- with `--allow-cross-db` if you need them.
136
+ with `--allow-cross-db` if you need them. Named connections still enforce their
137
+ database allowlist.
109
138
 
110
- **Server-side JavaScript.** The aggregation operators `$function`, `$where`,
111
- and `$accumulator` run arbitrary JavaScript on the MongoDB server. They are
112
- rejected by default; enable them with `--allow-server-js` if you trust the
113
- pipelines being run.
139
+ **Server-side JavaScript.** The operators `$function`, `$where`, and
140
+ `$accumulator` are blocked by default in aggregation pipelines and in the
141
+ filters for `query`, `count`, and `update`. In read-write mode,
142
+ `--allow-server-js` enables these operators.
114
143
 
115
- **Read-only mode** blocks every write path, including aggregation stages that
116
- write or run server-side JavaScript (`$out`, `$merge`, `$function`, `$where`,
117
- `$accumulator`).
144
+ **Read-only mode** blocks writes, including the `$out` and `$merge`
145
+ aggregation stages. It also blocks server-side JavaScript even when
146
+ `--allow-server-js` is set.
118
147
 
119
148
  **Least privilege.** Application-level checks only go so far — the strongest
120
149
  guarantee comes from the database. Connect with a MongoDB user scoped to just
package/build/index.js CHANGED
@@ -7,36 +7,184 @@ import { createMcpHandler } from "@modelcontextprotocol/server";
7
7
  import { serveStdio } from "@modelcontextprotocol/server/stdio";
8
8
  import { createMcpExpressApp } from "@modelcontextprotocol/express";
9
9
 
10
- // src/mongo.ts
10
+ // src/connections.ts
11
+ import { randomUUID } from "crypto";
12
+ import { ProtocolError, ProtocolErrorCode } from "@modelcontextprotocol/server";
11
13
  import { MongoClient } from "mongodb";
12
- async function connectToMongoDB(url, readOnly) {
13
- try {
14
- const client = new MongoClient(url);
15
- await client.connect();
16
- const db = client.db();
17
- console.warn(`Connected to MongoDB database: ${db.databaseName}`);
14
+ function isRecord(value) {
15
+ return !!value && typeof value === "object" && !Array.isArray(value);
16
+ }
17
+ var Connections = class {
18
+ cursorScope = randomUUID();
19
+ named;
20
+ entries = /* @__PURE__ */ new Map();
21
+ closing;
22
+ constructor(legacyUri, configuration, readOnly) {
23
+ this.named = configuration !== void 0;
24
+ let config = {};
25
+ if (configuration !== void 0) {
26
+ try {
27
+ config = JSON.parse(configuration);
28
+ } catch {
29
+ throw new Error("MCP_MONGODB_CONNECTIONS must be valid JSON");
30
+ }
31
+ }
32
+ if (!isRecord(config))
33
+ throw new Error("MCP_MONGODB_CONNECTIONS must be an object");
34
+ if (legacyUri && Object.hasOwn(config, "default")) {
35
+ throw new Error("The legacy URI and named default connection conflict");
36
+ }
37
+ const entries = legacyUri ? { ...config, default: { uri: legacyUri } } : config;
38
+ if (!Object.hasOwn(entries, "default"))
39
+ throw new Error("A default MongoDB connection is required");
40
+ for (const [id, entry] of Object.entries(entries)) {
41
+ if (!/^[A-Za-z0-9_-]+$/.test(id))
42
+ throw new Error("Invalid connection ID");
43
+ if (!isRecord(entry) || Object.keys(entry).some(
44
+ (key) => !["uri", "databases", "readOnly"].includes(key)
45
+ )) {
46
+ throw new Error(`Invalid configuration for connection '${id}'`);
47
+ }
48
+ const { uri, databases, readOnly: localReadOnly } = entry;
49
+ if (typeof uri !== "string" || !/^mongodb(?:\+srv)?:\/\//.test(uri)) {
50
+ throw new Error(`Invalid MongoDB URI for connection '${id}'`);
51
+ }
52
+ if (localReadOnly !== void 0 && typeof localReadOnly !== "boolean") {
53
+ throw new Error(`Invalid readOnly setting for connection '${id}'`);
54
+ }
55
+ let client;
56
+ const query = uri.indexOf("?");
57
+ const uriOptions = new URLSearchParams(
58
+ query < 0 ? "" : uri.slice(query + 1)
59
+ );
60
+ const hasSelectionTimeout = [...uriOptions.keys()].some(
61
+ (key) => key.toLowerCase() === "serverselectiontimeoutms"
62
+ );
63
+ const selectionTimeout = this.named && !hasSelectionTimeout ? 5e3 : void 0;
64
+ try {
65
+ client = new MongoClient(uri, {
66
+ serverSelectionTimeoutMS: selectionTimeout
67
+ });
68
+ } catch {
69
+ throw new Error(`Invalid MongoDB URI for connection '${id}'`);
70
+ }
71
+ const defaultDatabase = client.db().databaseName;
72
+ if (databases !== void 0 && (!Array.isArray(databases) || !databases.includes(defaultDatabase) || databases.some(
73
+ (name) => typeof name !== "string" || !name.trim() || /[/\\. "$]/.test(name) || name.includes("\0")
74
+ ))) {
75
+ throw new Error(`Invalid database allowlist for connection '${id}'`);
76
+ }
77
+ this.entries.set(id, {
78
+ connectionId: id,
79
+ uri,
80
+ selectionTimeout,
81
+ client,
82
+ defaultDatabase,
83
+ databases: databases === void 0 ? [defaultDatabase] : [...new Set(databases)].sort(),
84
+ readOnly: readOnly || localReadOnly === true
85
+ });
86
+ }
87
+ }
88
+ connection(id = "default") {
89
+ const entry = typeof id === "string" ? this.entries.get(id) : void 0;
90
+ if (!entry) throw new Error("Unknown or invalid MongoDB connection ID");
91
+ return entry;
92
+ }
93
+ list() {
94
+ return [...this.entries.keys()].sort().map((id) => {
95
+ const { connectionId, defaultDatabase, readOnly } = this.connection(id);
96
+ return { connectionId, defaultDatabase, readOnly };
97
+ });
98
+ }
99
+ allowedDatabases(id) {
100
+ return this.connection(id).databases;
101
+ }
102
+ async resolve({ connectionId, database } = {}) {
103
+ const entry = this.connection(connectionId);
104
+ const name = database === void 0 ? entry.defaultDatabase : database;
105
+ if (typeof name !== "string" || !entry.databases.includes(name)) {
106
+ throw new Error(
107
+ `Database is invalid or not allowed on connection '${entry.connectionId}'`
108
+ );
109
+ }
110
+ if (this.closing) throw new Error("MongoDB connections are shutting down");
111
+ if (!entry.pending) {
112
+ const client2 = entry.client ?? new MongoClient(entry.uri, {
113
+ serverSelectionTimeoutMS: entry.selectionTimeout
114
+ });
115
+ entry.client = client2;
116
+ entry.pending = client2.connect().catch(async () => {
117
+ await client2.close().catch(() => void 0);
118
+ entry.client = void 0;
119
+ entry.pending = void 0;
120
+ throw new Error(
121
+ `Failed to connect to MongoDB connection '${entry.connectionId}'`
122
+ );
123
+ });
124
+ }
125
+ const client = await entry.pending;
18
126
  return {
19
127
  client,
20
- db,
21
- isConnected: true,
22
- isReadOnlyMode: readOnly
23
- };
24
- } catch (error) {
25
- console.error("Failed to connect to MongoDB:", error);
26
- return {
27
- client: null,
28
- db: null,
29
- isConnected: false,
30
- isReadOnlyMode: readOnly
128
+ db: client.db(name),
129
+ isReadOnlyMode: entry.readOnly,
130
+ connectionId: entry.connectionId
31
131
  };
32
132
  }
133
+ async listDatabases(connectionId) {
134
+ const { db } = await this.resolve({ connectionId });
135
+ const allowed = this.allowedDatabases(connectionId);
136
+ const { databases } = await db.admin().listDatabases({
137
+ nameOnly: true,
138
+ authorizedDatabases: true,
139
+ filter: { name: { $in: [...allowed] } }
140
+ });
141
+ return databases.map(({ name }) => name).filter((name) => allowed.includes(name)).sort();
142
+ }
143
+ async close() {
144
+ this.closing ??= Promise.allSettled(
145
+ [...this.entries.values()].map(async ({ client, pending }) => {
146
+ await pending?.catch(() => void 0);
147
+ await client?.close();
148
+ })
149
+ ).then(() => void 0);
150
+ await this.closing;
151
+ }
152
+ };
153
+ function resourceSelectors(uri) {
154
+ let url;
155
+ try {
156
+ url = new URL(uri);
157
+ if (url.protocol !== "mongodb:") throw new Error("Invalid scheme");
158
+ decodeURIComponent(url.search);
159
+ } catch {
160
+ throw new ProtocolError(
161
+ ProtocolErrorCode.InvalidParams,
162
+ "Invalid MongoDB resource URI"
163
+ );
164
+ }
165
+ const selectors = {};
166
+ for (const key of ["connectionId", "database"]) {
167
+ const values = url.searchParams.getAll(key);
168
+ if (values.length > 1) {
169
+ throw new ProtocolError(
170
+ ProtocolErrorCode.InvalidParams,
171
+ `Duplicate resource selector '${key}'`
172
+ );
173
+ }
174
+ if (values.length === 1) selectors[key] = values[0];
175
+ }
176
+ return selectors;
33
177
  }
34
178
 
35
179
  // src/server.ts
36
- import { Server } from "@modelcontextprotocol/server";
180
+ import {
181
+ ProtocolError as ProtocolError4,
182
+ ProtocolErrorCode as ProtocolErrorCode4,
183
+ Server
184
+ } from "@modelcontextprotocol/server";
37
185
 
38
186
  // src/schemas/call.ts
39
- import { ProtocolError, ProtocolErrorCode } from "@modelcontextprotocol/server";
187
+ import { ProtocolError as ProtocolError2, ProtocolErrorCode as ProtocolErrorCode2 } from "@modelcontextprotocol/server";
40
188
  import { ObjectId } from "mongodb";
41
189
  var COLLECTION_OPERATIONS = [
42
190
  "query",
@@ -82,6 +230,14 @@ function findAggOperator(value, operators, depth = 0) {
82
230
  }
83
231
  return null;
84
232
  }
233
+ function assertNoServerJs(value) {
234
+ const operator = findAggOperator(value, SERVER_JS_AGG_OPERATORS);
235
+ if (operator) {
236
+ throw new Error(
237
+ `Server-side JavaScript operator '${operator}' is disabled by the current server policy.`
238
+ );
239
+ }
240
+ }
85
241
  function findCrossDbTarget(pipeline, connectedDb) {
86
242
  const foreignDb = (target) => {
87
243
  if (target && typeof target === "object" && "db" in target) {
@@ -111,6 +267,7 @@ async function handleCallToolRequest({
111
267
  isReadOnlyMode,
112
268
  allowCrossDb = false,
113
269
  allowServerJs = false,
270
+ connections: connections2,
114
271
  signal
115
272
  }) {
116
273
  const { name, arguments: args = {} } = request.params;
@@ -126,8 +283,32 @@ async function handleCallToolRequest({
126
283
  args.sort = parseSort(args.sort);
127
284
  }
128
285
  Object.assign(args, filteredArgs);
129
- validateOperation(operation);
286
+ if (!connections2 || name !== "listConnections" && name !== "listDatabases") {
287
+ validateOperation(operation);
288
+ }
130
289
  try {
290
+ signal?.throwIfAborted();
291
+ if (connections2 && name === "listConnections") {
292
+ return formatResponse({ connections: connections2.list() });
293
+ }
294
+ if (connections2 && name === "listDatabases") {
295
+ return formatResponse({
296
+ databases: await connections2.listDatabases(args.connectionId)
297
+ });
298
+ }
299
+ if (connections2 && operation !== "convertTime") {
300
+ const target = await connections2.resolve(args);
301
+ db = target.db;
302
+ isReadOnlyMode = target.isReadOnlyMode;
303
+ if (connections2.named && operation === "aggregate" && Array.isArray(args.pipeline)) {
304
+ const allowed = connections2.allowedDatabases(target.connectionId);
305
+ checkNamedAggregationDatabases(
306
+ args.pipeline,
307
+ db.databaseName,
308
+ allowCrossDb ? allowed : [db.databaseName]
309
+ );
310
+ }
311
+ }
131
312
  checkReadOnlyMode(operation, isReadOnlyMode);
132
313
  let collection = null;
133
314
  if (COLLECTION_OPERATIONS.includes(operation)) {
@@ -153,7 +334,7 @@ async function handleCallToolRequest({
153
334
  signal
154
335
  );
155
336
  } catch (error) {
156
- if (error instanceof ProtocolError || signal?.aborted) {
337
+ if (error instanceof ProtocolError2 || signal?.aborted) {
157
338
  throw error;
158
339
  }
159
340
  return toolExecutionError(
@@ -161,10 +342,50 @@ async function handleCallToolRequest({
161
342
  );
162
343
  }
163
344
  }
345
+ function checkNamedAggregationDatabases(pipeline, database, allowed, depth = 0) {
346
+ if (depth > MAX_OBJECT_DEPTH)
347
+ throw new Error("Aggregation pipeline nesting is too deep");
348
+ for (const stage of pipeline) {
349
+ if (!stage || typeof stage !== "object") continue;
350
+ const { $out, $merge, $lookup, $unionWith, $facet } = stage;
351
+ for (const target of [$out, $merge, $lookup, $unionWith]) {
352
+ if (!target || typeof target !== "object") continue;
353
+ const value = target;
354
+ for (const reference of [value, value.into, value.from]) {
355
+ if (!reference || typeof reference !== "object" || !("db" in reference))
356
+ continue;
357
+ if (typeof reference.db !== "string" || !allowed.includes(reference.db)) {
358
+ throw new Error(
359
+ `Aggregation references a database outside the allowed scope of '${database}'`
360
+ );
361
+ }
362
+ }
363
+ if (Array.isArray(value.pipeline))
364
+ checkNamedAggregationDatabases(
365
+ value.pipeline,
366
+ database,
367
+ allowed,
368
+ depth + 1
369
+ );
370
+ }
371
+ if ($facet && typeof $facet === "object") {
372
+ for (const nested of Object.values($facet)) {
373
+ if (Array.isArray(nested))
374
+ checkNamedAggregationDatabases(nested, database, allowed, depth + 1);
375
+ }
376
+ }
377
+ }
378
+ }
164
379
  async function executeOperation(operation, collection, db, isReadOnlyMode, allowCrossDb, allowServerJs, args, objectIdMode, signal) {
165
380
  switch (operation) {
166
381
  case "query":
167
- return handleQuery(collection, args, objectIdMode, signal);
382
+ return handleQuery(
383
+ collection,
384
+ args,
385
+ objectIdMode,
386
+ allowServerJs && !isReadOnlyMode,
387
+ signal
388
+ );
168
389
  case "aggregate":
169
390
  return handleAggregate(
170
391
  collection,
@@ -176,7 +397,13 @@ async function executeOperation(operation, collection, db, isReadOnlyMode, allow
176
397
  signal
177
398
  );
178
399
  case "update":
179
- return handleUpdate(collection, args, objectIdMode, signal);
400
+ return handleUpdate(
401
+ collection,
402
+ args,
403
+ objectIdMode,
404
+ allowServerJs,
405
+ signal
406
+ );
180
407
  case "serverInfo":
181
408
  return handleServerInfo(db, isReadOnlyMode, args, signal);
182
409
  case "insert":
@@ -184,13 +411,22 @@ async function executeOperation(operation, collection, db, isReadOnlyMode, allow
184
411
  case "createIndex":
185
412
  return handleCreateIndex(collection, args, objectIdMode, signal);
186
413
  case "count":
187
- return handleCount(collection, args, objectIdMode, signal);
414
+ return handleCount(
415
+ collection,
416
+ args,
417
+ objectIdMode,
418
+ allowServerJs && !isReadOnlyMode,
419
+ signal
420
+ );
188
421
  case "listCollections":
189
422
  return handleListCollections(db, args, objectIdMode, signal);
190
423
  case "convertTime":
191
424
  return handleConvertTime(args);
192
425
  default:
193
- throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Unknown tool: ${operation}`);
426
+ throw new ProtocolError2(
427
+ ProtocolErrorCode2.InvalidParams,
428
+ `Unknown tool: ${operation}`
429
+ );
194
430
  }
195
431
  }
196
432
  function validateOperation(operation) {
@@ -206,7 +442,10 @@ function validateOperation(operation) {
206
442
  "convertTime"
207
443
  ];
208
444
  if (!validOperations.includes(operation)) {
209
- throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Unknown tool: ${operation}`);
445
+ throw new ProtocolError2(
446
+ ProtocolErrorCode2.InvalidParams,
447
+ `Unknown tool: ${operation}`
448
+ );
210
449
  }
211
450
  }
212
451
  function validateCollection(collection) {
@@ -377,7 +616,7 @@ function handleError(error, operation, collectionName) {
377
616
  const message = error instanceof Error ? error.message : "Unknown error";
378
617
  return toolExecutionError(`Failed to ${operation} ${context}: ${message}`);
379
618
  }
380
- async function handleQuery(collection, args, objectIdMode = "auto", signal) {
619
+ async function handleQuery(collection, args, objectIdMode = "auto", allowServerJs = false, signal) {
381
620
  if (!collection) {
382
621
  throw new Error("Collection is required for query operation");
383
622
  }
@@ -385,6 +624,9 @@ async function handleQuery(collection, args, objectIdMode = "auto", signal) {
385
624
  const limit = args.limit || 10;
386
625
  const skip = args.skip || 0;
387
626
  const queryFilter = parseFilter(filter, objectIdMode);
627
+ if (!allowServerJs) {
628
+ assertNoServerJs(queryFilter);
629
+ }
388
630
  try {
389
631
  if (explain) {
390
632
  signal?.throwIfAborted();
@@ -440,12 +682,7 @@ async function handleAggregate(collection, args, objectIdMode = "auto", isReadOn
440
682
  }
441
683
  }
442
684
  if (!allowServerJs) {
443
- const js = findAggOperator(pipeline, SERVER_JS_AGG_OPERATORS);
444
- if (js) {
445
- throw new Error(
446
- `Server-side JavaScript operator '${js}' is not allowed. Enable it with --allow-server-js or MCP_MONGODB_ALLOW_SERVER_JS=true.`
447
- );
448
- }
685
+ assertNoServerJs(pipeline);
449
686
  }
450
687
  if (!allowCrossDb) {
451
688
  const foreignDb = findCrossDbTarget(pipeline, collection.dbName);
@@ -486,12 +723,15 @@ async function handleAggregate(collection, args, objectIdMode = "auto", isReadOn
486
723
  return handleError(error, "aggregate", collection.collectionName);
487
724
  }
488
725
  }
489
- async function handleUpdate(collection, args, objectIdMode = "auto", signal) {
726
+ async function handleUpdate(collection, args, objectIdMode = "auto", allowServerJs = false, signal) {
490
727
  if (!collection) {
491
728
  throw new Error("Collection is required for update operation");
492
729
  }
493
730
  const { filter, update, upsert, multi } = args;
494
731
  const queryFilter = parseFilter(filter, objectIdMode);
732
+ if (!allowServerJs) {
733
+ assertNoServerJs(queryFilter);
734
+ }
495
735
  let processedUpdate = update;
496
736
  if (update && typeof update === "object" && !Array.isArray(update)) {
497
737
  processedUpdate = processObjectIdInFilter(
@@ -652,7 +892,7 @@ async function handleCreateIndex(collection, args, objectIdMode = "auto", signal
652
892
  "Write concern must be a valid MongoDB write concern object"
653
893
  );
654
894
  }
655
- if (commitQuorum && typeof commitQuorum !== "string" && typeof commitQuorum !== "number") {
895
+ if (commitQuorum !== void 0 && typeof commitQuorum !== "string" && typeof commitQuorum !== "number") {
656
896
  throw new Error("Commit quorum must be a string or number");
657
897
  }
658
898
  const processedIndexes = indexes.map((index) => {
@@ -666,7 +906,7 @@ async function handleCreateIndex(collection, args, objectIdMode = "auto", signal
666
906
  });
667
907
  try {
668
908
  const indexOptions = {
669
- commitQuorum: typeof commitQuorum === "number" ? commitQuorum : void 0
909
+ commitQuorum
670
910
  };
671
911
  signal?.throwIfAborted();
672
912
  const createdIndexes = await collection.createIndexes(
@@ -682,12 +922,15 @@ async function handleCreateIndex(collection, args, objectIdMode = "auto", signal
682
922
  return handleError(error, "create indexes", collection.collectionName);
683
923
  }
684
924
  }
685
- async function handleCount(collection, args, objectIdMode = "auto", signal) {
925
+ async function handleCount(collection, args, objectIdMode = "auto", allowServerJs = false, signal) {
686
926
  if (!collection) {
687
927
  throw new Error("Collection is required for count operation");
688
928
  }
689
929
  const { query, limit, skip, hint, readConcern, maxTimeMS, collation } = args;
690
930
  const countQuery = parseFilter(query, objectIdMode);
931
+ if (!allowServerJs) {
932
+ assertNoServerJs(countQuery);
933
+ }
691
934
  try {
692
935
  const options = {
693
936
  limit: typeof limit === "number" ? limit : void 0,
@@ -758,17 +1001,10 @@ async function handleListCollections(db, args, objectIdMode = "auto", signal) {
758
1001
  const { nameOnly, filter } = args;
759
1002
  const skip = args.skip || 0;
760
1003
  const limit = args.limit || 20;
761
- let processedFilter = filter;
762
- if (filter && typeof filter === "object") {
763
- processedFilter = processObjectIdInFilter(
764
- filter,
765
- objectIdMode
766
- );
767
- }
768
1004
  try {
769
- const options = processedFilter ? { filter: processedFilter } : {};
1005
+ const processedFilter = parseFilter(filter, objectIdMode);
770
1006
  signal?.throwIfAborted();
771
- const collections = await db.listCollections(options).toArray();
1007
+ const collections = await db.listCollections(processedFilter).toArray();
772
1008
  const allResults = nameOnly ? collections.map((collection) => collection.name) : collections;
773
1009
  const total = allResults.length;
774
1010
  const paged = allResults.slice(skip, skip + limit);
@@ -948,7 +1184,8 @@ async function handleListPromptsRequest({
948
1184
  client,
949
1185
  db,
950
1186
  isReadOnlyMode,
951
- signal
1187
+ signal,
1188
+ connectionSelection = false
952
1189
  }) {
953
1190
  return {
954
1191
  prompts: [
@@ -960,7 +1197,17 @@ async function handleListPromptsRequest({
960
1197
  name: "collection",
961
1198
  description: "Name of the collection to analyze",
962
1199
  required: true
963
- }
1200
+ },
1201
+ ...connectionSelection ? [
1202
+ {
1203
+ name: "connectionId",
1204
+ description: "Configured MongoDB connection ID"
1205
+ },
1206
+ {
1207
+ name: "database",
1208
+ description: "Allowed MongoDB database name"
1209
+ }
1210
+ ] : []
964
1211
  ]
965
1212
  }
966
1213
  ]
@@ -972,7 +1219,9 @@ async function handleGetPromptRequest({
972
1219
  db,
973
1220
  isReadOnlyMode,
974
1221
  signal,
975
- sendProgress
1222
+ sendProgress,
1223
+ connectionId,
1224
+ showTarget = false
976
1225
  }) {
977
1226
  const { name, arguments: args = {} } = request.params;
978
1227
  if (name !== "analyze_collection") {
@@ -1004,7 +1253,9 @@ async function handleGetPromptRequest({
1004
1253
  content: {
1005
1254
  type: "text",
1006
1255
  text: `Please analyze the following MongoDB collection:
1007
- Collection: ${collectionName}
1256
+ Collection: ${collectionName}${showTarget ? `
1257
+ Connection: ${connectionId}
1258
+ Database: ${db.databaseName}` : ""}
1008
1259
 
1009
1260
  Schema:
1010
1261
  ${JSON.stringify(schemaSample, null, 2)}
@@ -1033,8 +1284,8 @@ ${JSON.stringify(sampleDocs, null, 2)}`
1033
1284
 
1034
1285
  // src/schemas/resource.ts
1035
1286
  import {
1036
- ProtocolError as ProtocolError2,
1037
- ProtocolErrorCode as ProtocolErrorCode2
1287
+ ProtocolError as ProtocolError3,
1288
+ ProtocolErrorCode as ProtocolErrorCode3
1038
1289
  } from "@modelcontextprotocol/server";
1039
1290
  import { ObjectId as ObjectId2 } from "mongodb";
1040
1291
  function detectMongoType(value) {
@@ -1137,28 +1388,28 @@ async function handleReadResourceRequest({
1137
1388
  try {
1138
1389
  const url = new URL(request.params.uri);
1139
1390
  if (url.protocol !== "mongodb:") {
1140
- throw new ProtocolError2(
1141
- ProtocolErrorCode2.InvalidParams,
1391
+ throw new ProtocolError3(
1392
+ ProtocolErrorCode3.InvalidParams,
1142
1393
  `Unsupported resource URI scheme '${url.protocol}': expected 'mongodb:'`
1143
1394
  );
1144
1395
  }
1145
- collectionName = url.pathname.replace(/^\//, "");
1396
+ collectionName = decodeURIComponent(url.pathname.replace(/^\//, ""));
1146
1397
  } catch (error) {
1147
- if (error instanceof ProtocolError2) throw error;
1148
- throw new ProtocolError2(
1149
- ProtocolErrorCode2.InvalidParams,
1398
+ if (error instanceof ProtocolError3) throw error;
1399
+ throw new ProtocolError3(
1400
+ ProtocolErrorCode3.InvalidParams,
1150
1401
  `Invalid resource URI: ${request.params.uri}`
1151
1402
  );
1152
1403
  }
1153
1404
  if (!collectionName) {
1154
- throw new ProtocolError2(
1155
- ProtocolErrorCode2.InvalidParams,
1405
+ throw new ProtocolError3(
1406
+ ProtocolErrorCode3.InvalidParams,
1156
1407
  "Resource URI is missing a collection name"
1157
1408
  );
1158
1409
  }
1159
1410
  if (collectionName.startsWith("system.")) {
1160
- throw new ProtocolError2(
1161
- ProtocolErrorCode2.InvalidParams,
1411
+ throw new ProtocolError3(
1412
+ ProtocolErrorCode3.InvalidParams,
1162
1413
  "Access to system collections is not allowed"
1163
1414
  );
1164
1415
  }
@@ -1250,7 +1501,7 @@ async function handleListResourcesRequest({
1250
1501
  signal?.throwIfAborted();
1251
1502
  const collections = await db.listCollections().toArray();
1252
1503
  const allResources = collections.map((collection) => ({
1253
- uri: `mongodb:///${collection.name}`,
1504
+ uri: `mongodb:///${encodeURIComponent(collection.name)}`,
1254
1505
  mimeType: "application/json",
1255
1506
  name: collection.name,
1256
1507
  description: `MongoDB collection: ${collection.name}`
@@ -1322,13 +1573,14 @@ async function handleListToolsRequest({
1322
1573
  client,
1323
1574
  db,
1324
1575
  isReadOnlyMode,
1325
- signal
1576
+ signal,
1577
+ connectionSelection = false
1326
1578
  }) {
1327
- return {
1579
+ const result = {
1328
1580
  tools: [
1329
1581
  {
1330
1582
  name: "query",
1331
- description: "Execute a MongoDB query with optional execution plan analysis",
1583
+ description: "Find documents in one collection using a filter, projection, or sort. Use aggregate for grouping or joins. Request an execution plan to inspect the query.",
1332
1584
  inputSchema: {
1333
1585
  type: "object",
1334
1586
  properties: {
@@ -1375,7 +1627,7 @@ async function handleListToolsRequest({
1375
1627
  },
1376
1628
  {
1377
1629
  name: "aggregate",
1378
- description: "Execute a MongoDB aggregation pipeline with optional execution plan analysis",
1630
+ description: "Run a MongoDB aggregation pipeline for grouping, joins, or multi-stage transformations. Request an execution plan to inspect the pipeline.",
1379
1631
  inputSchema: {
1380
1632
  type: "object",
1381
1633
  properties: {
@@ -1407,7 +1659,7 @@ async function handleListToolsRequest({
1407
1659
  },
1408
1660
  {
1409
1661
  name: "update",
1410
- description: "Update documents in a MongoDB collection",
1662
+ description: "Modify matching documents using operators such as $set, $unset, or $inc. Enable multi to update all matches or upsert to create a document when the filter matches none.",
1411
1663
  inputSchema: {
1412
1664
  type: "object",
1413
1665
  properties: {
@@ -1443,7 +1695,7 @@ async function handleListToolsRequest({
1443
1695
  },
1444
1696
  {
1445
1697
  name: "serverInfo",
1446
- description: "Get MongoDB server information including version, storage engine, and other details",
1698
+ description: "Inspect MongoDB server metadata such as version and storage engine. Enable includeDebugInfo for additional diagnostics.",
1447
1699
  inputSchema: {
1448
1700
  type: "object",
1449
1701
  properties: {
@@ -1456,7 +1708,7 @@ async function handleListToolsRequest({
1456
1708
  },
1457
1709
  {
1458
1710
  name: "insert",
1459
- description: "Insert one or more documents into a MongoDB collection",
1711
+ description: "Insert new documents into a MongoDB collection. Use update to modify existing records.",
1460
1712
  inputSchema: {
1461
1713
  type: "object",
1462
1714
  properties: {
@@ -1493,7 +1745,7 @@ async function handleListToolsRequest({
1493
1745
  },
1494
1746
  {
1495
1747
  name: "createIndex",
1496
- description: "Create one or more indexes on a MongoDB collection",
1748
+ description: "Create indexes to improve query performance, enforce uniqueness, or expire documents with a TTL index.",
1497
1749
  inputSchema: {
1498
1750
  type: "object",
1499
1751
  properties: {
@@ -1544,8 +1796,8 @@ async function handleListToolsRequest({
1544
1796
  description: "Write concern for index creation"
1545
1797
  },
1546
1798
  commitQuorum: {
1547
- type: "string",
1548
- description: "Number of members required to create the index"
1799
+ type: ["string", "number"],
1800
+ description: "Replica-set commit quorum as a member count or a mode such as majority"
1549
1801
  },
1550
1802
  objectIdMode: {
1551
1803
  type: "string",
@@ -1559,7 +1811,7 @@ async function handleListToolsRequest({
1559
1811
  },
1560
1812
  {
1561
1813
  name: "count",
1562
- description: "Count documents in a collection matching a query",
1814
+ description: "Count documents matching a filter in one collection. Use aggregate for counts grouped by field.",
1563
1815
  inputSchema: {
1564
1816
  type: "object",
1565
1817
  properties: {
@@ -1589,7 +1841,7 @@ async function handleListToolsRequest({
1589
1841
  },
1590
1842
  {
1591
1843
  name: "listCollections",
1592
- description: "List all collections in the MongoDB database",
1844
+ description: "List collections in the connected database. Filter by name or other collection properties and request names only for a compact result.",
1593
1845
  inputSchema: {
1594
1846
  type: "object",
1595
1847
  properties: {
@@ -1635,6 +1887,39 @@ async function handleListToolsRequest({
1635
1887
  }
1636
1888
  ]
1637
1889
  };
1890
+ const { tools } = result;
1891
+ if (connectionSelection) {
1892
+ const connectionId = {
1893
+ type: "string",
1894
+ description: "Configured connection ID. Defaults to default."
1895
+ };
1896
+ const database = {
1897
+ type: "string",
1898
+ description: "Allowed database name. Defaults to the database in the connection URI."
1899
+ };
1900
+ for (const tool of tools) {
1901
+ if (tool.name !== "convertTime") {
1902
+ tool.inputSchema.properties = {
1903
+ ...tool.inputSchema.properties,
1904
+ connectionId,
1905
+ database
1906
+ };
1907
+ }
1908
+ }
1909
+ tools.push(
1910
+ {
1911
+ name: "listConnections",
1912
+ description: "List configured connection IDs, default databases, and read-only policies.",
1913
+ inputSchema: { type: "object", properties: {} }
1914
+ },
1915
+ {
1916
+ name: "listDatabases",
1917
+ description: "List existing authorized databases on a configured connection.",
1918
+ inputSchema: { type: "object", properties: { connectionId } }
1919
+ }
1920
+ );
1921
+ }
1922
+ return result;
1638
1923
  }
1639
1924
 
1640
1925
  // src/server.ts
@@ -1645,12 +1930,13 @@ function createSendProgress(progressToken, notify) {
1645
1930
  params: { progressToken, progress, total, message }
1646
1931
  });
1647
1932
  }
1648
- function createServer(client, db, isReadOnlyMode = false, allowCrossDb = false, allowServerJs = false, options = {}) {
1933
+ function createServer(client, db, isReadOnlyMode = false, allowCrossDb = false, allowServerJs = false, options = {}, connections2) {
1934
+ const defaultTarget = { client, db, isReadOnlyMode };
1649
1935
  const server = new Server(
1650
1936
  {
1651
1937
  name: "mongodb",
1652
1938
  title: "MongoDB MCP Server",
1653
- version: "3.0.1",
1939
+ version: "3.1.0",
1654
1940
  description: "MCP server for MongoDB: query, aggregate, and manage collections with read-only mode, progress notifications, and cancellation",
1655
1941
  websiteUrl: "https://github.com/kiliczsh/mcp-mongo-server",
1656
1942
  ...options
@@ -1675,23 +1961,27 @@ function createServer(client, db, isReadOnlyMode = false, allowCrossDb = false,
1675
1961
  signal: ctx.mcpReq.signal
1676
1962
  })
1677
1963
  );
1678
- server.setRequestHandler(
1679
- "resources/list",
1680
- (request, ctx) => handleListResourcesRequest({
1964
+ server.setRequestHandler("resources/list", (request, ctx) => {
1965
+ if (connections2?.named) {
1966
+ return listNamedResources(
1967
+ connections2,
1968
+ request.params?.cursor,
1969
+ ctx.mcpReq.signal
1970
+ );
1971
+ }
1972
+ return handleListResourcesRequest({
1681
1973
  request,
1682
1974
  client,
1683
1975
  db,
1684
1976
  isReadOnlyMode,
1685
1977
  signal: ctx.mcpReq.signal
1686
- })
1687
- );
1978
+ });
1979
+ });
1688
1980
  server.setRequestHandler(
1689
1981
  "resources/read",
1690
- (request, ctx) => handleReadResourceRequest({
1982
+ async (request, ctx) => handleReadResourceRequest({
1691
1983
  request,
1692
- client,
1693
- db,
1694
- isReadOnlyMode,
1984
+ ...connections2 ? await connections2.resolve(resourceSelectors(request.params.uri)) : defaultTarget,
1695
1985
  signal: ctx.mcpReq.signal,
1696
1986
  sendProgress: createSendProgress(
1697
1987
  request.params._meta?.progressToken,
@@ -1706,6 +1996,7 @@ function createServer(client, db, isReadOnlyMode = false, allowCrossDb = false,
1706
1996
  client,
1707
1997
  db,
1708
1998
  isReadOnlyMode,
1999
+ connectionSelection: !!connections2,
1709
2000
  signal: ctx.mcpReq.signal
1710
2001
  })
1711
2002
  );
@@ -1718,6 +2009,7 @@ function createServer(client, db, isReadOnlyMode = false, allowCrossDb = false,
1718
2009
  isReadOnlyMode,
1719
2010
  allowCrossDb,
1720
2011
  allowServerJs,
2012
+ connections: connections2,
1721
2013
  signal: ctx.mcpReq.signal
1722
2014
  })
1723
2015
  );
@@ -1728,16 +2020,16 @@ function createServer(client, db, isReadOnlyMode = false, allowCrossDb = false,
1728
2020
  client,
1729
2021
  db,
1730
2022
  isReadOnlyMode,
2023
+ connectionSelection: !!connections2,
1731
2024
  signal: ctx.mcpReq.signal
1732
2025
  })
1733
2026
  );
1734
2027
  server.setRequestHandler(
1735
2028
  "prompts/get",
1736
- (request, ctx) => handleGetPromptRequest({
2029
+ async (request, ctx) => handleGetPromptRequest({
1737
2030
  request,
1738
- client,
1739
- db,
1740
- isReadOnlyMode,
2031
+ ...connections2 ? await connections2.resolve(request.params.arguments) : defaultTarget,
2032
+ showTarget: !!connections2?.named,
1741
2033
  signal: ctx.mcpReq.signal,
1742
2034
  sendProgress: createSendProgress(
1743
2035
  request.params._meta?.progressToken,
@@ -1745,31 +2037,94 @@ function createServer(client, db, isReadOnlyMode = false, allowCrossDb = false,
1745
2037
  )
1746
2038
  })
1747
2039
  );
1748
- server.setRequestHandler(
1749
- "resources/templates/list",
1750
- (request, ctx) => handleListResourceTemplatesRequest({
2040
+ server.setRequestHandler("resources/templates/list", async (request, ctx) => {
2041
+ const result = await handleListResourceTemplatesRequest({
1751
2042
  request,
1752
2043
  client,
1753
2044
  db,
1754
2045
  isReadOnlyMode,
1755
2046
  signal: ctx.mcpReq.signal
1756
- })
1757
- );
1758
- server.setRequestHandler(
1759
- "completion/complete",
1760
- (request, ctx) => handleCompletionRequest({
2047
+ });
2048
+ if (connections2?.named) {
2049
+ result.resourceTemplates.push({
2050
+ ...result.resourceTemplates[0],
2051
+ name: "mongodb_connection_collection",
2052
+ uriTemplate: "mongodb:///{collection}{?connectionId,database}"
2053
+ });
2054
+ }
2055
+ return result;
2056
+ });
2057
+ server.setRequestHandler("completion/complete", async (request, ctx) => {
2058
+ const { ref, argument, context } = request.params;
2059
+ const selectors = {
2060
+ ...context?.arguments,
2061
+ ...ref.type === "ref/resource" ? resourceSelectors(ref.uri) : {}
2062
+ };
2063
+ if (connections2 && (argument.name === "connectionId" || argument.name === "database")) {
2064
+ const candidates = argument.name === "connectionId" ? connections2.list().map(({ connectionId }) => connectionId) : await connections2.listDatabases(selectors.connectionId);
2065
+ const matches = candidates.filter(
2066
+ (value) => value.startsWith(argument.value)
2067
+ );
2068
+ return {
2069
+ completion: {
2070
+ values: matches.slice(0, 100),
2071
+ total: matches.length,
2072
+ hasMore: matches.length > 100
2073
+ }
2074
+ };
2075
+ }
2076
+ return handleCompletionRequest({
1761
2077
  request,
1762
- client,
1763
- db,
1764
- isReadOnlyMode,
2078
+ ...connections2 ? await connections2.resolve(selectors) : defaultTarget,
1765
2079
  signal: ctx.mcpReq.signal
1766
- })
1767
- );
2080
+ });
2081
+ });
1768
2082
  return server;
1769
2083
  }
2084
+ async function listNamedResources(connections2, cursor, signal) {
2085
+ const prefix = `${connections2.cursorScope}.`;
2086
+ const offsetCursor = cursor?.slice(prefix.length);
2087
+ const offset = decodeCursor(offsetCursor);
2088
+ if (cursor !== void 0 && (!cursor.startsWith(prefix) || !Number.isSafeInteger(offset) || encodeCursor(offset) !== offsetCursor)) {
2089
+ throw new ProtocolError4(
2090
+ ProtocolErrorCode4.InvalidParams,
2091
+ "Invalid resource cursor for the current connection configuration"
2092
+ );
2093
+ }
2094
+ const resources = [];
2095
+ for (const { connectionId, defaultDatabase } of connections2.list()) {
2096
+ try {
2097
+ signal?.throwIfAborted();
2098
+ for (const database of await connections2.listDatabases(connectionId)) {
2099
+ const { db } = await connections2.resolve({ connectionId, database });
2100
+ const collections = await db.listCollections({}, { nameOnly: true }).toArray();
2101
+ for (const name of collections.map(({ name: name2 }) => name2).sort()) {
2102
+ if (name.startsWith("system.")) continue;
2103
+ const query = connectionId === "default" && database === defaultDatabase ? "" : `?${new URLSearchParams({ connectionId, database })}`;
2104
+ resources.push({
2105
+ uri: `mongodb:///${encodeURIComponent(name)}${query}`,
2106
+ name,
2107
+ mimeType: "application/json",
2108
+ description: `MongoDB collection: ${connectionId}/${database}/${name}`
2109
+ });
2110
+ }
2111
+ }
2112
+ } catch (error) {
2113
+ if (signal?.aborted) throw error;
2114
+ throw new Error(
2115
+ `Failed to list resources for MongoDB connection '${connectionId}'`
2116
+ );
2117
+ }
2118
+ }
2119
+ const { items, nextCursor } = paginate(resources, offsetCursor);
2120
+ return {
2121
+ resources: items,
2122
+ ...nextCursor ? { nextCursor: prefix + nextCursor } : {}
2123
+ };
2124
+ }
1770
2125
 
1771
2126
  // src/index.ts
1772
- var mongoClient = null;
2127
+ var connections;
1773
2128
  async function main() {
1774
2129
  const args = process.argv.slice(2);
1775
2130
  let connectionUrl = "";
@@ -1818,34 +2173,13 @@ async function main() {
1818
2173
  if (!connectionUrl) {
1819
2174
  connectionUrl = process.env.MCP_MONGODB_URI || "";
1820
2175
  }
1821
- if (!connectionUrl) {
1822
- console.error(
1823
- "Please provide a MongoDB connection URL via command-line argument or MCP_MONGODB_URI environment variable"
1824
- );
1825
- console.error(
1826
- "Usage: command <mongodb-url> [--read-only|-r] [--transport stdio|http] [--port 3001]"
1827
- );
1828
- console.error(
1829
- " or: MCP_MONGODB_URI=<mongodb-url> [MCP_MONGODB_READONLY=true] command"
1830
- );
1831
- process.exit(1);
1832
- }
1833
- if (!connectionUrl.startsWith("mongodb://") && !connectionUrl.startsWith("mongodb+srv://")) {
1834
- console.error(
1835
- "Invalid MongoDB connection URL. URL must start with 'mongodb://' or 'mongodb+srv://'"
1836
- );
1837
- process.exit(1);
1838
- }
1839
2176
  try {
1840
- const { client, db, isConnected, isReadOnlyMode } = await connectToMongoDB(
2177
+ connections = new Connections(
1841
2178
  connectionUrl,
2179
+ process.env.MCP_MONGODB_CONNECTIONS,
1842
2180
  readOnlyMode
1843
2181
  );
1844
- mongoClient = client;
1845
- if (!isConnected || !client || !db) {
1846
- console.error("Failed to connect to MongoDB");
1847
- process.exit(1);
1848
- }
2182
+ const { client, db, isReadOnlyMode } = await connections.resolve();
1849
2183
  if (transportMode === "http") {
1850
2184
  await startHttpServer(
1851
2185
  client,
@@ -1853,6 +2187,7 @@ async function main() {
1853
2187
  isReadOnlyMode,
1854
2188
  allowCrossDb,
1855
2189
  allowServerJs,
2190
+ connections,
1856
2191
  port,
1857
2192
  allowedOrigins,
1858
2193
  jsonLimit,
@@ -1864,20 +2199,27 @@ async function main() {
1864
2199
  db,
1865
2200
  isReadOnlyMode,
1866
2201
  allowCrossDb,
1867
- allowServerJs
2202
+ allowServerJs,
2203
+ connections
1868
2204
  );
1869
2205
  }
1870
2206
  } catch (error) {
1871
2207
  console.error("Failed to connect to MongoDB:", error);
1872
- if (mongoClient) {
1873
- await mongoClient.close();
1874
- }
2208
+ await connections?.close();
1875
2209
  process.exit(1);
1876
2210
  }
1877
2211
  }
1878
- async function startStdioServer(client, db, isReadOnlyMode, allowCrossDb, allowServerJs) {
2212
+ async function startStdioServer(client, db, isReadOnlyMode, allowCrossDb, allowServerJs, connections2) {
1879
2213
  serveStdio(
1880
- () => createServer(client, db, isReadOnlyMode, allowCrossDb, allowServerJs)
2214
+ () => createServer(
2215
+ client,
2216
+ db,
2217
+ isReadOnlyMode,
2218
+ allowCrossDb,
2219
+ allowServerJs,
2220
+ {},
2221
+ connections2
2222
+ )
1881
2223
  );
1882
2224
  console.warn("Server connected successfully via stdio");
1883
2225
  }
@@ -1904,7 +2246,7 @@ function bearerTokenValid(authorization, expected) {
1904
2246
  const wanted = Buffer.from(expected);
1905
2247
  return provided.length === wanted.length && timingSafeEqual(provided, wanted);
1906
2248
  }
1907
- async function startHttpServer(client, db, isReadOnlyMode, allowCrossDb, allowServerJs, port, allowedOrigins, jsonLimit, authToken) {
2249
+ async function startHttpServer(client, db, isReadOnlyMode, allowCrossDb, allowServerJs, connections2, port, allowedOrigins, jsonLimit, authToken) {
1908
2250
  const app = createMcpExpressApp({ host: "0.0.0.0", jsonLimit });
1909
2251
  app.use((req, res, next) => {
1910
2252
  const start = Date.now();
@@ -1955,7 +2297,15 @@ async function startHttpServer(client, db, isReadOnlyMode, allowCrossDb, allowSe
1955
2297
  });
1956
2298
  }
1957
2299
  const mcpHandler = createMcpHandler(
1958
- (_ctx) => createServer(client, db, isReadOnlyMode, allowCrossDb, allowServerJs)
2300
+ (_ctx) => createServer(
2301
+ client,
2302
+ db,
2303
+ isReadOnlyMode,
2304
+ allowCrossDb,
2305
+ allowServerJs,
2306
+ {},
2307
+ connections2
2308
+ )
1959
2309
  );
1960
2310
  const nodeHandler = toNodeHandler(mcpHandler, {
1961
2311
  onerror: (error) => console.error("Error handling MCP request:", error)
@@ -1982,15 +2332,11 @@ async function startHttpServer(client, db, isReadOnlyMode, allowCrossDb, allowSe
1982
2332
  });
1983
2333
  }
1984
2334
  process.on("SIGINT", async () => {
1985
- if (mongoClient) {
1986
- await mongoClient.close();
1987
- }
2335
+ await connections?.close();
1988
2336
  process.exit(0);
1989
2337
  });
1990
2338
  process.on("SIGTERM", async () => {
1991
- if (mongoClient) {
1992
- await mongoClient.close();
1993
- }
2339
+ await connections?.close();
1994
2340
  process.exit(0);
1995
2341
  });
1996
2342
  main().catch((error) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-mongo-server",
3
- "version": "3.0.1",
3
+ "version": "3.1.0",
4
4
  "description": "A Model Context Protocol server for MongoDB connections",
5
5
  "private": false,
6
6
  "type": "module",
@@ -21,6 +21,8 @@
21
21
  "format": "biome format --write .",
22
22
  "format:check": "biome format .",
23
23
  "lint": "biome lint .",
24
+ "test:connections": "bun test tests/connections.lifecycle.test.ts && node --test tests/connections.e2e.test.mjs",
25
+ "test:connections:legacy": "MCP_TEST_PROTOCOL=legacy node --test tests/connections.e2e.test.mjs",
24
26
  "lint:fix": "biome lint --write .",
25
27
  "check": "biome check --write .",
26
28
  "inspector": "mcp-inspector --config inspector-config.json --server mongodb"
@@ -35,6 +37,7 @@
35
37
  "@biomejs/biome": "^2.5.5",
36
38
  "@modelcontextprotocol/client": "^2.0.0",
37
39
  "@modelcontextprotocol/inspector": "^1.0.0",
40
+ "@types/express": "5",
38
41
  "@types/node": "^26.1.1",
39
42
  "tsup": "^8.5.1",
40
43
  "typescript": "^5.9.3"