mcp-mongo-server 3.0.2 → 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 +32 -3
  2. package/build/index.js +438 -109
  3. package/package.json +3 -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,13 +100,41 @@ 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
139
  **Server-side JavaScript.** The operators `$function`, `$where`, and
111
140
  `$accumulator` are blocked by default in aggregation pipelines and in the
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",
@@ -119,6 +267,7 @@ async function handleCallToolRequest({
119
267
  isReadOnlyMode,
120
268
  allowCrossDb = false,
121
269
  allowServerJs = false,
270
+ connections: connections2,
122
271
  signal
123
272
  }) {
124
273
  const { name, arguments: args = {} } = request.params;
@@ -134,8 +283,32 @@ async function handleCallToolRequest({
134
283
  args.sort = parseSort(args.sort);
135
284
  }
136
285
  Object.assign(args, filteredArgs);
137
- validateOperation(operation);
286
+ if (!connections2 || name !== "listConnections" && name !== "listDatabases") {
287
+ validateOperation(operation);
288
+ }
138
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
+ }
139
312
  checkReadOnlyMode(operation, isReadOnlyMode);
140
313
  let collection = null;
141
314
  if (COLLECTION_OPERATIONS.includes(operation)) {
@@ -161,7 +334,7 @@ async function handleCallToolRequest({
161
334
  signal
162
335
  );
163
336
  } catch (error) {
164
- if (error instanceof ProtocolError || signal?.aborted) {
337
+ if (error instanceof ProtocolError2 || signal?.aborted) {
165
338
  throw error;
166
339
  }
167
340
  return toolExecutionError(
@@ -169,6 +342,40 @@ async function handleCallToolRequest({
169
342
  );
170
343
  }
171
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
+ }
172
379
  async function executeOperation(operation, collection, db, isReadOnlyMode, allowCrossDb, allowServerJs, args, objectIdMode, signal) {
173
380
  switch (operation) {
174
381
  case "query":
@@ -190,7 +397,13 @@ async function executeOperation(operation, collection, db, isReadOnlyMode, allow
190
397
  signal
191
398
  );
192
399
  case "update":
193
- return handleUpdate(collection, args, objectIdMode, allowServerJs, signal);
400
+ return handleUpdate(
401
+ collection,
402
+ args,
403
+ objectIdMode,
404
+ allowServerJs,
405
+ signal
406
+ );
194
407
  case "serverInfo":
195
408
  return handleServerInfo(db, isReadOnlyMode, args, signal);
196
409
  case "insert":
@@ -210,7 +423,10 @@ async function executeOperation(operation, collection, db, isReadOnlyMode, allow
210
423
  case "convertTime":
211
424
  return handleConvertTime(args);
212
425
  default:
213
- throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Unknown tool: ${operation}`);
426
+ throw new ProtocolError2(
427
+ ProtocolErrorCode2.InvalidParams,
428
+ `Unknown tool: ${operation}`
429
+ );
214
430
  }
215
431
  }
216
432
  function validateOperation(operation) {
@@ -226,7 +442,10 @@ function validateOperation(operation) {
226
442
  "convertTime"
227
443
  ];
228
444
  if (!validOperations.includes(operation)) {
229
- throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Unknown tool: ${operation}`);
445
+ throw new ProtocolError2(
446
+ ProtocolErrorCode2.InvalidParams,
447
+ `Unknown tool: ${operation}`
448
+ );
230
449
  }
231
450
  }
232
451
  function validateCollection(collection) {
@@ -965,7 +1184,8 @@ async function handleListPromptsRequest({
965
1184
  client,
966
1185
  db,
967
1186
  isReadOnlyMode,
968
- signal
1187
+ signal,
1188
+ connectionSelection = false
969
1189
  }) {
970
1190
  return {
971
1191
  prompts: [
@@ -977,7 +1197,17 @@ async function handleListPromptsRequest({
977
1197
  name: "collection",
978
1198
  description: "Name of the collection to analyze",
979
1199
  required: true
980
- }
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
+ ] : []
981
1211
  ]
982
1212
  }
983
1213
  ]
@@ -989,7 +1219,9 @@ async function handleGetPromptRequest({
989
1219
  db,
990
1220
  isReadOnlyMode,
991
1221
  signal,
992
- sendProgress
1222
+ sendProgress,
1223
+ connectionId,
1224
+ showTarget = false
993
1225
  }) {
994
1226
  const { name, arguments: args = {} } = request.params;
995
1227
  if (name !== "analyze_collection") {
@@ -1021,7 +1253,9 @@ async function handleGetPromptRequest({
1021
1253
  content: {
1022
1254
  type: "text",
1023
1255
  text: `Please analyze the following MongoDB collection:
1024
- Collection: ${collectionName}
1256
+ Collection: ${collectionName}${showTarget ? `
1257
+ Connection: ${connectionId}
1258
+ Database: ${db.databaseName}` : ""}
1025
1259
 
1026
1260
  Schema:
1027
1261
  ${JSON.stringify(schemaSample, null, 2)}
@@ -1050,8 +1284,8 @@ ${JSON.stringify(sampleDocs, null, 2)}`
1050
1284
 
1051
1285
  // src/schemas/resource.ts
1052
1286
  import {
1053
- ProtocolError as ProtocolError2,
1054
- ProtocolErrorCode as ProtocolErrorCode2
1287
+ ProtocolError as ProtocolError3,
1288
+ ProtocolErrorCode as ProtocolErrorCode3
1055
1289
  } from "@modelcontextprotocol/server";
1056
1290
  import { ObjectId as ObjectId2 } from "mongodb";
1057
1291
  function detectMongoType(value) {
@@ -1154,28 +1388,28 @@ async function handleReadResourceRequest({
1154
1388
  try {
1155
1389
  const url = new URL(request.params.uri);
1156
1390
  if (url.protocol !== "mongodb:") {
1157
- throw new ProtocolError2(
1158
- ProtocolErrorCode2.InvalidParams,
1391
+ throw new ProtocolError3(
1392
+ ProtocolErrorCode3.InvalidParams,
1159
1393
  `Unsupported resource URI scheme '${url.protocol}': expected 'mongodb:'`
1160
1394
  );
1161
1395
  }
1162
1396
  collectionName = decodeURIComponent(url.pathname.replace(/^\//, ""));
1163
1397
  } catch (error) {
1164
- if (error instanceof ProtocolError2) throw error;
1165
- throw new ProtocolError2(
1166
- ProtocolErrorCode2.InvalidParams,
1398
+ if (error instanceof ProtocolError3) throw error;
1399
+ throw new ProtocolError3(
1400
+ ProtocolErrorCode3.InvalidParams,
1167
1401
  `Invalid resource URI: ${request.params.uri}`
1168
1402
  );
1169
1403
  }
1170
1404
  if (!collectionName) {
1171
- throw new ProtocolError2(
1172
- ProtocolErrorCode2.InvalidParams,
1405
+ throw new ProtocolError3(
1406
+ ProtocolErrorCode3.InvalidParams,
1173
1407
  "Resource URI is missing a collection name"
1174
1408
  );
1175
1409
  }
1176
1410
  if (collectionName.startsWith("system.")) {
1177
- throw new ProtocolError2(
1178
- ProtocolErrorCode2.InvalidParams,
1411
+ throw new ProtocolError3(
1412
+ ProtocolErrorCode3.InvalidParams,
1179
1413
  "Access to system collections is not allowed"
1180
1414
  );
1181
1415
  }
@@ -1339,9 +1573,10 @@ async function handleListToolsRequest({
1339
1573
  client,
1340
1574
  db,
1341
1575
  isReadOnlyMode,
1342
- signal
1576
+ signal,
1577
+ connectionSelection = false
1343
1578
  }) {
1344
- return {
1579
+ const result = {
1345
1580
  tools: [
1346
1581
  {
1347
1582
  name: "query",
@@ -1652,6 +1887,39 @@ async function handleListToolsRequest({
1652
1887
  }
1653
1888
  ]
1654
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;
1655
1923
  }
1656
1924
 
1657
1925
  // src/server.ts
@@ -1662,12 +1930,13 @@ function createSendProgress(progressToken, notify) {
1662
1930
  params: { progressToken, progress, total, message }
1663
1931
  });
1664
1932
  }
1665
- 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 };
1666
1935
  const server = new Server(
1667
1936
  {
1668
1937
  name: "mongodb",
1669
1938
  title: "MongoDB MCP Server",
1670
- version: "3.0.2",
1939
+ version: "3.1.0",
1671
1940
  description: "MCP server for MongoDB: query, aggregate, and manage collections with read-only mode, progress notifications, and cancellation",
1672
1941
  websiteUrl: "https://github.com/kiliczsh/mcp-mongo-server",
1673
1942
  ...options
@@ -1692,23 +1961,27 @@ function createServer(client, db, isReadOnlyMode = false, allowCrossDb = false,
1692
1961
  signal: ctx.mcpReq.signal
1693
1962
  })
1694
1963
  );
1695
- server.setRequestHandler(
1696
- "resources/list",
1697
- (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({
1698
1973
  request,
1699
1974
  client,
1700
1975
  db,
1701
1976
  isReadOnlyMode,
1702
1977
  signal: ctx.mcpReq.signal
1703
- })
1704
- );
1978
+ });
1979
+ });
1705
1980
  server.setRequestHandler(
1706
1981
  "resources/read",
1707
- (request, ctx) => handleReadResourceRequest({
1982
+ async (request, ctx) => handleReadResourceRequest({
1708
1983
  request,
1709
- client,
1710
- db,
1711
- isReadOnlyMode,
1984
+ ...connections2 ? await connections2.resolve(resourceSelectors(request.params.uri)) : defaultTarget,
1712
1985
  signal: ctx.mcpReq.signal,
1713
1986
  sendProgress: createSendProgress(
1714
1987
  request.params._meta?.progressToken,
@@ -1723,6 +1996,7 @@ function createServer(client, db, isReadOnlyMode = false, allowCrossDb = false,
1723
1996
  client,
1724
1997
  db,
1725
1998
  isReadOnlyMode,
1999
+ connectionSelection: !!connections2,
1726
2000
  signal: ctx.mcpReq.signal
1727
2001
  })
1728
2002
  );
@@ -1735,6 +2009,7 @@ function createServer(client, db, isReadOnlyMode = false, allowCrossDb = false,
1735
2009
  isReadOnlyMode,
1736
2010
  allowCrossDb,
1737
2011
  allowServerJs,
2012
+ connections: connections2,
1738
2013
  signal: ctx.mcpReq.signal
1739
2014
  })
1740
2015
  );
@@ -1745,16 +2020,16 @@ function createServer(client, db, isReadOnlyMode = false, allowCrossDb = false,
1745
2020
  client,
1746
2021
  db,
1747
2022
  isReadOnlyMode,
2023
+ connectionSelection: !!connections2,
1748
2024
  signal: ctx.mcpReq.signal
1749
2025
  })
1750
2026
  );
1751
2027
  server.setRequestHandler(
1752
2028
  "prompts/get",
1753
- (request, ctx) => handleGetPromptRequest({
2029
+ async (request, ctx) => handleGetPromptRequest({
1754
2030
  request,
1755
- client,
1756
- db,
1757
- isReadOnlyMode,
2031
+ ...connections2 ? await connections2.resolve(request.params.arguments) : defaultTarget,
2032
+ showTarget: !!connections2?.named,
1758
2033
  signal: ctx.mcpReq.signal,
1759
2034
  sendProgress: createSendProgress(
1760
2035
  request.params._meta?.progressToken,
@@ -1762,31 +2037,94 @@ function createServer(client, db, isReadOnlyMode = false, allowCrossDb = false,
1762
2037
  )
1763
2038
  })
1764
2039
  );
1765
- server.setRequestHandler(
1766
- "resources/templates/list",
1767
- (request, ctx) => handleListResourceTemplatesRequest({
2040
+ server.setRequestHandler("resources/templates/list", async (request, ctx) => {
2041
+ const result = await handleListResourceTemplatesRequest({
1768
2042
  request,
1769
2043
  client,
1770
2044
  db,
1771
2045
  isReadOnlyMode,
1772
2046
  signal: ctx.mcpReq.signal
1773
- })
1774
- );
1775
- server.setRequestHandler(
1776
- "completion/complete",
1777
- (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({
1778
2077
  request,
1779
- client,
1780
- db,
1781
- isReadOnlyMode,
2078
+ ...connections2 ? await connections2.resolve(selectors) : defaultTarget,
1782
2079
  signal: ctx.mcpReq.signal
1783
- })
1784
- );
2080
+ });
2081
+ });
1785
2082
  return server;
1786
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
+ }
1787
2125
 
1788
2126
  // src/index.ts
1789
- var mongoClient = null;
2127
+ var connections;
1790
2128
  async function main() {
1791
2129
  const args = process.argv.slice(2);
1792
2130
  let connectionUrl = "";
@@ -1835,34 +2173,13 @@ async function main() {
1835
2173
  if (!connectionUrl) {
1836
2174
  connectionUrl = process.env.MCP_MONGODB_URI || "";
1837
2175
  }
1838
- if (!connectionUrl) {
1839
- console.error(
1840
- "Please provide a MongoDB connection URL via command-line argument or MCP_MONGODB_URI environment variable"
1841
- );
1842
- console.error(
1843
- "Usage: command <mongodb-url> [--read-only|-r] [--transport stdio|http] [--port 3001]"
1844
- );
1845
- console.error(
1846
- " or: MCP_MONGODB_URI=<mongodb-url> [MCP_MONGODB_READONLY=true] command"
1847
- );
1848
- process.exit(1);
1849
- }
1850
- if (!connectionUrl.startsWith("mongodb://") && !connectionUrl.startsWith("mongodb+srv://")) {
1851
- console.error(
1852
- "Invalid MongoDB connection URL. URL must start with 'mongodb://' or 'mongodb+srv://'"
1853
- );
1854
- process.exit(1);
1855
- }
1856
2176
  try {
1857
- const { client, db, isConnected, isReadOnlyMode } = await connectToMongoDB(
2177
+ connections = new Connections(
1858
2178
  connectionUrl,
2179
+ process.env.MCP_MONGODB_CONNECTIONS,
1859
2180
  readOnlyMode
1860
2181
  );
1861
- mongoClient = client;
1862
- if (!isConnected || !client || !db) {
1863
- console.error("Failed to connect to MongoDB");
1864
- process.exit(1);
1865
- }
2182
+ const { client, db, isReadOnlyMode } = await connections.resolve();
1866
2183
  if (transportMode === "http") {
1867
2184
  await startHttpServer(
1868
2185
  client,
@@ -1870,6 +2187,7 @@ async function main() {
1870
2187
  isReadOnlyMode,
1871
2188
  allowCrossDb,
1872
2189
  allowServerJs,
2190
+ connections,
1873
2191
  port,
1874
2192
  allowedOrigins,
1875
2193
  jsonLimit,
@@ -1881,20 +2199,27 @@ async function main() {
1881
2199
  db,
1882
2200
  isReadOnlyMode,
1883
2201
  allowCrossDb,
1884
- allowServerJs
2202
+ allowServerJs,
2203
+ connections
1885
2204
  );
1886
2205
  }
1887
2206
  } catch (error) {
1888
2207
  console.error("Failed to connect to MongoDB:", error);
1889
- if (mongoClient) {
1890
- await mongoClient.close();
1891
- }
2208
+ await connections?.close();
1892
2209
  process.exit(1);
1893
2210
  }
1894
2211
  }
1895
- async function startStdioServer(client, db, isReadOnlyMode, allowCrossDb, allowServerJs) {
2212
+ async function startStdioServer(client, db, isReadOnlyMode, allowCrossDb, allowServerJs, connections2) {
1896
2213
  serveStdio(
1897
- () => createServer(client, db, isReadOnlyMode, allowCrossDb, allowServerJs)
2214
+ () => createServer(
2215
+ client,
2216
+ db,
2217
+ isReadOnlyMode,
2218
+ allowCrossDb,
2219
+ allowServerJs,
2220
+ {},
2221
+ connections2
2222
+ )
1898
2223
  );
1899
2224
  console.warn("Server connected successfully via stdio");
1900
2225
  }
@@ -1921,7 +2246,7 @@ function bearerTokenValid(authorization, expected) {
1921
2246
  const wanted = Buffer.from(expected);
1922
2247
  return provided.length === wanted.length && timingSafeEqual(provided, wanted);
1923
2248
  }
1924
- 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) {
1925
2250
  const app = createMcpExpressApp({ host: "0.0.0.0", jsonLimit });
1926
2251
  app.use((req, res, next) => {
1927
2252
  const start = Date.now();
@@ -1972,7 +2297,15 @@ async function startHttpServer(client, db, isReadOnlyMode, allowCrossDb, allowSe
1972
2297
  });
1973
2298
  }
1974
2299
  const mcpHandler = createMcpHandler(
1975
- (_ctx) => createServer(client, db, isReadOnlyMode, allowCrossDb, allowServerJs)
2300
+ (_ctx) => createServer(
2301
+ client,
2302
+ db,
2303
+ isReadOnlyMode,
2304
+ allowCrossDb,
2305
+ allowServerJs,
2306
+ {},
2307
+ connections2
2308
+ )
1976
2309
  );
1977
2310
  const nodeHandler = toNodeHandler(mcpHandler, {
1978
2311
  onerror: (error) => console.error("Error handling MCP request:", error)
@@ -1999,15 +2332,11 @@ async function startHttpServer(client, db, isReadOnlyMode, allowCrossDb, allowSe
1999
2332
  });
2000
2333
  }
2001
2334
  process.on("SIGINT", async () => {
2002
- if (mongoClient) {
2003
- await mongoClient.close();
2004
- }
2335
+ await connections?.close();
2005
2336
  process.exit(0);
2006
2337
  });
2007
2338
  process.on("SIGTERM", async () => {
2008
- if (mongoClient) {
2009
- await mongoClient.close();
2010
- }
2339
+ await connections?.close();
2011
2340
  process.exit(0);
2012
2341
  });
2013
2342
  main().catch((error) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-mongo-server",
3
- "version": "3.0.2",
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"