mcp-mongo-server 2.1.1 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +97 -18
  2. package/build/index.js +314 -191
  3. package/package.json +10 -5
package/README.md CHANGED
@@ -5,51 +5,130 @@
5
5
  ![NPM Downloads](https://img.shields.io/npm/dm/mcp-mongo-server)
6
6
  ![NPM License](https://img.shields.io/npm/l/mcp-mongo-server)
7
7
 
8
- A Model Context Protocol server that enables LLMs to interact with MongoDB databases. This server provides capabilities for inspecting collection schemas and executing MongoDB operations through a standardized interface.
8
+ A Model Context Protocol (MCP) server that lets AI assistants work with your MongoDB databases. It exposes your collections, infers their schemas, and runs queries, aggregations, and writes through a standard interface — so tools like Claude Desktop and Cursor can read and reason about your data.
9
9
 
10
10
  ## Demo
11
11
 
12
12
  [![MCP MongoDB Server Demo | Claude Desktop](https://img.youtube.com/vi/FI-oE_voCpA/0.jpg)](https://www.youtube.com/watch?v=FI-oE_voCpA)
13
13
 
14
+ ## Why use it
15
+
16
+ - **Talk to your database in plain language** — the assistant discovers your collections and their shape automatically.
17
+ - **Safe by default** — turn on read-only mode to let an assistant explore without any risk of changing data.
18
+ - **Works everywhere** — connects to standalone, replica set, sharded, and Atlas deployments, over plain or TLS connections.
19
+
14
20
  ## Key Features
15
21
 
16
- - **Smart ObjectId Handling** - Configurable auto/none/force modes for string-to-ObjectId conversion
17
- - **Read-Only Mode** - Protection against write operations, uses secondary read preference
18
- - **Schema Inference** - Automatic collection schema detection from document samples
19
- - **Query & Aggregation** - Full MongoDB query and aggregation pipeline support with optional explain plans
20
- - **Write Operations** - Insert, update, and index creation (when not in read-only mode)
21
- - **Collection Completions** - Auto-complete collection names for LLM integration
22
+ - **Read-Only Mode** — blocks every write path (insert, update, index creation, and aggregation stages like `$out`/`$merge` that could modify data).
23
+ - **Smart ObjectId Handling** — configurable `auto`/`none`/`force` conversion of 24-character hex strings to ObjectIds.
24
+ - **Schema Inference** — automatic collection schema detection from document samples.
25
+ - **Query & Aggregation** — full query and aggregation pipeline support, with optional `explain` plans.
26
+ - **Write Operations** — insert, update, and index creation (when read-only mode is off).
27
+ - **Time Conversion** — a `convertTime` helper turns Unix timestamps and date strings into UTC/GMT/ISO, so date queries stay unambiguous across timezones.
28
+ - **Progress & Cancellation** — long operations report progress and can be cancelled mid-flight.
29
+ - **Two Transports** — run locally over stdio, or expose an HTTP endpoint for remote access.
30
+
31
+ ## Requirements
22
32
 
23
- ## Installation
33
+ - Node.js 20 or newer
34
+
35
+ ## Quick Start
36
+
37
+ Point the server at your database — no install step needed:
24
38
 
25
39
  ```bash
26
- npx -y mcp-mongo-server mongodb://localhost:27017/database
40
+ npx -y mcp-mongo-server mongodb://localhost:27017/mydatabase
41
+ ```
42
+
43
+ Explore safely, without any chance of changing data:
44
+
45
+ ```bash
46
+ npx -y mcp-mongo-server mongodb://localhost:27017/mydatabase --read-only
27
47
  ```
28
48
 
29
49
  ## Usage
30
50
 
51
+ ### Local (stdio)
52
+
53
+ This is the default, used by Claude Desktop, Cursor, and other local clients:
54
+
31
55
  ```bash
32
- # Start server with MongoDB URI
33
- npx -y mcp-mongo-server mongodb://muhammed:kilic@localhost:27017/database
56
+ npx -y mcp-mongo-server "mongodb://user:pass@localhost:27017/mydatabase"
57
+ ```
58
+
59
+ ### Remote (HTTP)
60
+
61
+ Expose an HTTP endpoint at `/mcp` for remote or multi-client access:
62
+
63
+ ```bash
64
+ npx -y mcp-mongo-server "mongodb://user:pass@localhost:27017/mydatabase" --transport http --port 3001
65
+ ```
66
+
67
+ By default, only requests without a browser `Origin` (CLIs, IDEs) and requests
68
+ from `localhost` are accepted; everything else is rejected with `403` to guard
69
+ against DNS-rebinding attacks. Allow specific browser origins with
70
+ `--allowed-origins`:
34
71
 
35
- # Connect in read-only mode
36
- npx -y mcp-mongo-server mongodb://muhammed:kilic@localhost:27017/database --read-only
72
+ ```bash
73
+ npx -y mcp-mongo-server "mongodb://..." --transport http --port 3001 --allowed-origins "https://app.example.com"
37
74
  ```
38
75
 
76
+ ### Options
77
+
78
+ | Flag | Description |
79
+ |------|-------------|
80
+ | `--read-only`, `-r` | Block all write operations |
81
+ | `--allow-cross-db` | Allow aggregation `$out`/`$merge`/`$lookup` to target other databases (off by default) |
82
+ | `--allow-server-js` | Allow server-side JavaScript operators `$function`/`$where`/`$accumulator` (off by default) |
83
+ | `--transport`, `-t` | `stdio` (default) or `http` |
84
+ | `--port`, `-p` | HTTP port (default `3001`) |
85
+ | `--allowed-origins` | Comma-separated browser origins to allow in HTTP mode |
86
+ | `--json-limit` | Max HTTP request body size (default `10mb`) |
87
+ | `--auth-token` | Require `Authorization: Bearer <token>` on the HTTP endpoint |
88
+
39
89
  ### Environment Variables
40
90
 
41
91
  | Variable | Description |
42
92
  |----------|-------------|
43
- | `MCP_MONGODB_URI` | MongoDB connection URI |
93
+ | `MCP_MONGODB_URI` | MongoDB connection URI (alternative to the argument) |
44
94
  | `MCP_MONGODB_READONLY` | Enable read-only mode (`"true"`) |
95
+ | `MCP_MONGODB_ALLOW_CROSS_DB` | Allow cross-database aggregation stages (`"true"`) |
96
+ | `MCP_MONGODB_ALLOW_SERVER_JS` | Allow server-side JavaScript operators (`"true"`) |
97
+ | `MCP_PORT` | HTTP port |
98
+ | `MCP_HTTP_ALLOWED_ORIGINS` | Comma-separated browser origins to allow in HTTP mode |
99
+ | `MCP_HTTP_JSON_LIMIT` | Max HTTP request body size (default `10mb`) |
100
+ | `MCP_HTTP_AUTH_TOKEN` | Bearer token required to access the HTTP endpoint |
101
+
102
+ ## Security
103
+
104
+ **Database scope.** The server operates on the database in your connection
105
+ string. Aggregation stages that reach another database (`$out`, `$merge`,
106
+ `$lookup` with an explicit `db`) are rejected by default, so a pipeline can't
107
+ quietly read from or write to databases you didn't point it at. Enable them
108
+ with `--allow-cross-db` if you need them.
109
+
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.
114
+
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`).
118
+
119
+ **Least privilege.** Application-level checks only go so far — the strongest
120
+ guarantee comes from the database. Connect with a MongoDB user scoped to just
121
+ the database you need, with read-only permissions when the assistant only needs
122
+ to explore. That way the database itself enforces the boundary, as defense in
123
+ depth.
45
124
 
46
125
  ## Documentation
47
126
 
48
- - [Integration Guide](docs/integration.md) - Claude Desktop, Windsurf, Cursor, Docker
49
- - [Available Tools](docs/tools.md) - Query, aggregate, update, insert, and more
50
- - [Development](docs/development.md) - Setup, scripts, and debugging
127
+ - [Integration Guide](docs/integration.md) — Claude Desktop, Windsurf, Cursor, Docker
128
+ - [Available Tools](docs/tools.md) — query, aggregate, update, insert, and more
129
+ - [Development](docs/development.md) — setup, scripts, and debugging
51
130
  - [Contributing](CONTRIBUTING.md)
52
131
 
53
132
  ## License
54
133
 
55
- MIT - see [LICENSE](LICENSE) for details.
134
+ MIT — see [LICENSE](LICENSE) for details.
package/build/index.js CHANGED
@@ -1,16 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
5
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
- import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
4
+ import { timingSafeEqual } from "crypto";
5
+ import { toNodeHandler } from "@modelcontextprotocol/node";
6
+ import { createMcpHandler } from "@modelcontextprotocol/server";
7
+ import { serveStdio } from "@modelcontextprotocol/server/stdio";
8
+ import { createMcpExpressApp } from "@modelcontextprotocol/express";
7
9
 
8
10
  // src/mongo.ts
9
- import { MongoClient, ReadPreference } from "mongodb";
11
+ import { MongoClient } from "mongodb";
10
12
  async function connectToMongoDB(url, readOnly) {
11
13
  try {
12
- const options = readOnly ? { readPreference: ReadPreference.SECONDARY } : {};
13
- const client = new MongoClient(url, options);
14
+ const client = new MongoClient(url);
14
15
  await client.connect();
15
16
  const db = client.db();
16
17
  console.warn(`Connected to MongoDB database: ${db.databaseName}`);
@@ -32,28 +33,10 @@ async function connectToMongoDB(url, readOnly) {
32
33
  }
33
34
 
34
35
  // src/server.ts
35
- import {
36
- InMemoryTaskMessageQueue,
37
- InMemoryTaskStore
38
- } from "@modelcontextprotocol/sdk/experimental/tasks";
39
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
40
- import {
41
- CallToolRequestSchema,
42
- CompleteRequestSchema,
43
- GetPromptRequestSchema,
44
- ListPromptsRequestSchema,
45
- ListResourcesRequestSchema,
46
- ListResourceTemplatesRequestSchema,
47
- ListToolsRequestSchema,
48
- PingRequestSchema,
49
- ReadResourceRequestSchema
50
- } from "@modelcontextprotocol/sdk/types.js";
36
+ import { Server } from "@modelcontextprotocol/server";
51
37
 
52
38
  // src/schemas/call.ts
53
- import {
54
- ErrorCode,
55
- McpError
56
- } from "@modelcontextprotocol/sdk/types.js";
39
+ import { ProtocolError, ProtocolErrorCode } from "@modelcontextprotocol/server";
57
40
  import { ObjectId } from "mongodb";
58
41
  var COLLECTION_OPERATIONS = [
59
42
  "query",
@@ -64,40 +47,71 @@ var COLLECTION_OPERATIONS = [
64
47
  "count"
65
48
  ];
66
49
  var WRITE_OPERATIONS = ["update", "insert", "createIndex"];
67
- var READONLY_FORBIDDEN_AGG_OPERATORS = /* @__PURE__ */ new Set([
68
- "$out",
69
- "$merge",
50
+ var MAX_OBJECT_DEPTH = 100;
51
+ var WRITE_AGG_STAGES = /* @__PURE__ */ new Set(["$out", "$merge"]);
52
+ var SERVER_JS_AGG_OPERATORS = /* @__PURE__ */ new Set([
70
53
  "$function",
71
54
  "$accumulator",
72
55
  "$where"
73
56
  ]);
74
- function findForbiddenAggOperator(value) {
57
+ var READONLY_FORBIDDEN_AGG_OPERATORS = /* @__PURE__ */ new Set([
58
+ ...WRITE_AGG_STAGES,
59
+ ...SERVER_JS_AGG_OPERATORS
60
+ ]);
61
+ function findAggOperator(value, operators, depth = 0) {
62
+ if (depth > MAX_OBJECT_DEPTH) {
63
+ throw new Error(
64
+ `Object nesting exceeds the maximum depth of ${MAX_OBJECT_DEPTH}`
65
+ );
66
+ }
75
67
  if (Array.isArray(value)) {
76
68
  for (const item of value) {
77
- const found = findForbiddenAggOperator(item);
69
+ const found = findAggOperator(item, operators, depth + 1);
78
70
  if (found) return found;
79
71
  }
80
72
  return null;
81
73
  }
82
74
  if (value && typeof value === "object") {
83
75
  for (const [key, nested] of Object.entries(value)) {
84
- if (READONLY_FORBIDDEN_AGG_OPERATORS.has(key)) {
76
+ if (operators.has(key)) {
85
77
  return key;
86
78
  }
87
- const found = findForbiddenAggOperator(nested);
79
+ const found = findAggOperator(nested, operators, depth + 1);
88
80
  if (found) return found;
89
81
  }
90
82
  }
91
83
  return null;
92
84
  }
85
+ function findCrossDbTarget(pipeline, connectedDb) {
86
+ const foreignDb = (target) => {
87
+ if (target && typeof target === "object" && "db" in target) {
88
+ const db = target.db;
89
+ if (typeof db === "string" && db && db !== connectedDb) return db;
90
+ }
91
+ return null;
92
+ };
93
+ for (const stage of pipeline) {
94
+ if (!stage || typeof stage !== "object") continue;
95
+ const s = stage;
96
+ const out = foreignDb(s.$out);
97
+ if (out) return out;
98
+ const merge = s.$merge;
99
+ const mergeInto = foreignDb(merge?.into ?? s.$merge);
100
+ if (mergeInto) return mergeInto;
101
+ const lookup = s.$lookup;
102
+ const lookupFrom = foreignDb(lookup?.from);
103
+ if (lookupFrom) return lookupFrom;
104
+ }
105
+ return null;
106
+ }
93
107
  async function handleCallToolRequest({
94
108
  request,
95
109
  client,
96
110
  db,
97
111
  isReadOnlyMode,
98
- signal,
99
- taskStore,
100
- taskTtl
112
+ allowCrossDb = false,
113
+ allowServerJs = false,
114
+ signal
101
115
  }) {
102
116
  const { name, arguments: args = {} } = request.params;
103
117
  const operation = name;
@@ -127,46 +141,19 @@ async function handleCallToolRequest({
127
141
  validateCollection(collection);
128
142
  }
129
143
  signal?.throwIfAborted();
130
- if (taskStore) {
131
- const task = await taskStore.createTask({
132
- ttl: taskTtl ?? void 0
133
- });
134
- (async () => {
135
- try {
136
- const result = await executeOperation(
137
- operation,
138
- collection,
139
- db,
140
- isReadOnlyMode,
141
- args,
142
- objectIdMode,
143
- signal
144
- );
145
- await taskStore.storeTaskResult(task.taskId, "completed", result);
146
- } catch (error) {
147
- try {
148
- const message = error instanceof Error ? error.message : "Unknown error";
149
- await taskStore.storeTaskResult(task.taskId, "failed", {
150
- content: [{ type: "text", text: message }],
151
- isError: true
152
- });
153
- } catch {
154
- }
155
- }
156
- })();
157
- return { task };
158
- }
159
144
  return await executeOperation(
160
145
  operation,
161
146
  collection,
162
147
  db,
163
148
  isReadOnlyMode,
149
+ allowCrossDb,
150
+ allowServerJs,
164
151
  args,
165
152
  objectIdMode,
166
153
  signal
167
154
  );
168
155
  } catch (error) {
169
- if (error instanceof McpError || signal?.aborted) {
156
+ if (error instanceof ProtocolError || signal?.aborted) {
170
157
  throw error;
171
158
  }
172
159
  return toolExecutionError(
@@ -174,7 +161,7 @@ async function handleCallToolRequest({
174
161
  );
175
162
  }
176
163
  }
177
- async function executeOperation(operation, collection, db, isReadOnlyMode, args, objectIdMode, signal) {
164
+ async function executeOperation(operation, collection, db, isReadOnlyMode, allowCrossDb, allowServerJs, args, objectIdMode, signal) {
178
165
  switch (operation) {
179
166
  case "query":
180
167
  return handleQuery(collection, args, objectIdMode, signal);
@@ -184,6 +171,8 @@ async function executeOperation(operation, collection, db, isReadOnlyMode, args,
184
171
  args,
185
172
  objectIdMode,
186
173
  isReadOnlyMode,
174
+ allowCrossDb,
175
+ allowServerJs,
187
176
  signal
188
177
  );
189
178
  case "update":
@@ -198,8 +187,10 @@ async function executeOperation(operation, collection, db, isReadOnlyMode, args,
198
187
  return handleCount(collection, args, objectIdMode, signal);
199
188
  case "listCollections":
200
189
  return handleListCollections(db, args, objectIdMode, signal);
190
+ case "convertTime":
191
+ return handleConvertTime(args);
201
192
  default:
202
- throw new McpError(ErrorCode.InvalidParams, `Unknown tool: ${operation}`);
193
+ throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Unknown tool: ${operation}`);
203
194
  }
204
195
  }
205
196
  function validateOperation(operation) {
@@ -211,17 +202,18 @@ function validateOperation(operation) {
211
202
  "insert",
212
203
  "createIndex",
213
204
  "count",
214
- "listCollections"
205
+ "listCollections",
206
+ "convertTime"
215
207
  ];
216
208
  if (!validOperations.includes(operation)) {
217
- throw new McpError(ErrorCode.InvalidParams, `Unknown tool: ${operation}`);
209
+ throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Unknown tool: ${operation}`);
218
210
  }
219
211
  }
220
212
  function validateCollection(collection) {
221
213
  if (!collection.collectionName) {
222
214
  throw new Error("Collection name cannot be empty");
223
215
  }
224
- if (collection.collectionName.startsWith("system.")) {
216
+ if (collection.collectionName.toLowerCase().startsWith("system.")) {
225
217
  throw new Error("Access to system collections is not allowed");
226
218
  }
227
219
  }
@@ -268,7 +260,12 @@ function isObjectIdField(fieldName) {
268
260
  const lowerFieldName = fieldName.toLowerCase();
269
261
  return lowerFieldName === "_id" || lowerFieldName === "id" || lowerFieldName.endsWith("id") || lowerFieldName.endsWith("_id");
270
262
  }
271
- function processObjectIdInFilter(filter, objectIdMode = "auto") {
263
+ function processObjectIdInFilter(filter, objectIdMode = "auto", depth = 0) {
264
+ if (depth > MAX_OBJECT_DEPTH) {
265
+ throw new Error(
266
+ `Object nesting exceeds the maximum depth of ${MAX_OBJECT_DEPTH}`
267
+ );
268
+ }
272
269
  if (objectIdMode === "none") {
273
270
  const result2 = {};
274
271
  for (const [key, value] of Object.entries(filter)) {
@@ -295,7 +292,8 @@ function processObjectIdInFilter(filter, objectIdMode = "auto") {
295
292
  } else {
296
293
  result2[key] = processObjectIdInFilter(
297
294
  value,
298
- "none"
295
+ "none",
296
+ depth + 1
299
297
  );
300
298
  }
301
299
  } else {
@@ -337,7 +335,8 @@ function processObjectIdInFilter(filter, objectIdMode = "auto") {
337
335
  } else {
338
336
  result[key] = processObjectIdInFilter(
339
337
  value,
340
- objectIdMode
338
+ objectIdMode,
339
+ depth + 1
341
340
  );
342
341
  }
343
342
  } else {
@@ -350,7 +349,9 @@ function isObjectIdString(str) {
350
349
  return /^[0-9a-fA-F]{24}$/.test(str);
351
350
  }
352
351
  function isISODateString(str) {
353
- return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,3})?Z$/.test(str);
352
+ return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,3})?(Z|[+-]\d{2}:\d{2})$/.test(
353
+ str
354
+ );
354
355
  }
355
356
  function formatResponse(data) {
356
357
  return {
@@ -419,7 +420,7 @@ async function handleQuery(collection, args, objectIdMode = "auto", signal) {
419
420
  return handleError(error, "query", collection.collectionName);
420
421
  }
421
422
  }
422
- async function handleAggregate(collection, args, objectIdMode = "auto", isReadOnlyMode = false, signal) {
423
+ async function handleAggregate(collection, args, objectIdMode = "auto", isReadOnlyMode = false, allowCrossDb = false, allowServerJs = false, signal) {
423
424
  if (!collection) {
424
425
  throw new Error("Collection is required for aggregate operation");
425
426
  }
@@ -428,13 +429,32 @@ async function handleAggregate(collection, args, objectIdMode = "auto", isReadOn
428
429
  throw new Error("Pipeline must be an array");
429
430
  }
430
431
  if (isReadOnlyMode) {
431
- const forbidden = findForbiddenAggOperator(pipeline);
432
+ const forbidden = findAggOperator(
433
+ pipeline,
434
+ READONLY_FORBIDDEN_AGG_OPERATORS
435
+ );
432
436
  if (forbidden) {
433
437
  throw new Error(
434
438
  `ReadonlyError: Aggregation operator '${forbidden}' is not allowed in read-only mode`
435
439
  );
436
440
  }
437
441
  }
442
+ 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
+ }
449
+ }
450
+ if (!allowCrossDb) {
451
+ const foreignDb = findCrossDbTarget(pipeline, collection.dbName);
452
+ if (foreignDb) {
453
+ throw new Error(
454
+ `Cross-database access to '${foreignDb}' is not allowed (connected database is '${collection.dbName}'). Enable it with --allow-cross-db or MCP_MONGODB_ALLOW_CROSS_DB=true.`
455
+ );
456
+ }
457
+ }
438
458
  const processedPipeline = pipeline.map((stage) => {
439
459
  if (typeof stage === "object" && stage !== null) {
440
460
  return processObjectIdInFilter(
@@ -536,8 +556,7 @@ async function handleServerInfo(db, isReadOnlyMode, args, signal) {
536
556
  bits: buildInfo.bits,
537
557
  ok: buildInfo.ok,
538
558
  connectionInfo: {
539
- readOnlyMode: isReadOnlyMode,
540
- readPreference: isReadOnlyMode ? "secondary" : "primary"
559
+ readOnlyMode: isReadOnlyMode
541
560
  }
542
561
  };
543
562
  if (includeDebugInfo) {
@@ -650,15 +669,14 @@ async function handleCreateIndex(collection, args, objectIdMode = "auto", signal
650
669
  commitQuorum: typeof commitQuorum === "number" ? commitQuorum : void 0
651
670
  };
652
671
  signal?.throwIfAborted();
653
- const result = await collection.createIndexes(
672
+ const createdIndexes = await collection.createIndexes(
654
673
  processedIndexes,
655
674
  indexOptions
656
675
  );
657
676
  return formatResponse({
658
- acknowledged: result.acknowledged,
659
- createdIndexes: result.createdIndexes,
660
- numIndexesBefore: result.numIndexesBefore,
661
- numIndexesAfter: result.numIndexesAfter
677
+ acknowledged: true,
678
+ createdIndexes,
679
+ indexCount: createdIndexes.length
662
680
  });
663
681
  } catch (error) {
664
682
  return handleError(error, "create indexes", collection.collectionName);
@@ -694,6 +712,48 @@ async function handleCount(collection, args, objectIdMode = "auto", signal) {
694
712
  return handleError(error, "count documents", collection.collectionName);
695
713
  }
696
714
  }
715
+ function handleConvertTime(args) {
716
+ const { input } = args;
717
+ let date;
718
+ if (input === void 0 || input === null || input === "") {
719
+ date = /* @__PURE__ */ new Date();
720
+ } else if (typeof input === "number") {
721
+ date = new Date(Math.abs(input) < 1e12 ? input * 1e3 : input);
722
+ } else if (typeof input === "string") {
723
+ const trimmed = input.trim();
724
+ if (/^-?\d+$/.test(trimmed)) {
725
+ const n = Number(trimmed);
726
+ date = new Date(Math.abs(n) < 1e12 ? n * 1e3 : n);
727
+ } else {
728
+ date = new Date(trimmed);
729
+ }
730
+ } else {
731
+ throw new Error(
732
+ "convertTime 'input' must be a Unix timestamp (number) or a date string"
733
+ );
734
+ }
735
+ if (Number.isNaN(date.getTime())) {
736
+ throw new Error(
737
+ `Could not interpret '${String(input)}' as a date or Unix timestamp`
738
+ );
739
+ }
740
+ const offsetMinutes = -date.getTimezoneOffset();
741
+ const sign = offsetMinutes >= 0 ? "+" : "-";
742
+ const absMinutes = Math.abs(offsetMinutes);
743
+ const offsetLabel = `${sign}${String(Math.floor(absMinutes / 60)).padStart(
744
+ 2,
745
+ "0"
746
+ )}:${String(absMinutes % 60).padStart(2, "0")}`;
747
+ return formatResponse({
748
+ iso: date.toISOString(),
749
+ utc: date.toUTCString(),
750
+ unixSeconds: Math.floor(date.getTime() / 1e3),
751
+ unixMillis: date.getTime(),
752
+ serverLocal: date.toString(),
753
+ serverTimeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
754
+ serverUtcOffset: offsetLabel
755
+ });
756
+ }
697
757
  async function handleListCollections(db, args, objectIdMode = "auto", signal) {
698
758
  const { nameOnly, filter } = args;
699
759
  const skip = args.skip || 0;
@@ -972,6 +1032,10 @@ ${JSON.stringify(sampleDocs, null, 2)}`
972
1032
  }
973
1033
 
974
1034
  // src/schemas/resource.ts
1035
+ import {
1036
+ ProtocolError as ProtocolError2,
1037
+ ProtocolErrorCode as ProtocolErrorCode2
1038
+ } from "@modelcontextprotocol/server";
975
1039
  import { ObjectId as ObjectId2 } from "mongodb";
976
1040
  function detectMongoType(value) {
977
1041
  if (value === null) return "null";
@@ -1069,8 +1133,35 @@ async function handleReadResourceRequest({
1069
1133
  signal,
1070
1134
  sendProgress
1071
1135
  }) {
1072
- const url = new URL(request.params.uri);
1073
- const collectionName = url.pathname.replace(/^\//, "");
1136
+ let collectionName;
1137
+ try {
1138
+ const url = new URL(request.params.uri);
1139
+ if (url.protocol !== "mongodb:") {
1140
+ throw new ProtocolError2(
1141
+ ProtocolErrorCode2.InvalidParams,
1142
+ `Unsupported resource URI scheme '${url.protocol}': expected 'mongodb:'`
1143
+ );
1144
+ }
1145
+ collectionName = url.pathname.replace(/^\//, "");
1146
+ } catch (error) {
1147
+ if (error instanceof ProtocolError2) throw error;
1148
+ throw new ProtocolError2(
1149
+ ProtocolErrorCode2.InvalidParams,
1150
+ `Invalid resource URI: ${request.params.uri}`
1151
+ );
1152
+ }
1153
+ if (!collectionName) {
1154
+ throw new ProtocolError2(
1155
+ ProtocolErrorCode2.InvalidParams,
1156
+ "Resource URI is missing a collection name"
1157
+ );
1158
+ }
1159
+ if (collectionName.startsWith("system.")) {
1160
+ throw new ProtocolError2(
1161
+ ProtocolErrorCode2.InvalidParams,
1162
+ "Access to system collections is not allowed"
1163
+ );
1164
+ }
1074
1165
  try {
1075
1166
  const collection = db.collection(collectionName);
1076
1167
  const sampleSize = 100;
@@ -1238,7 +1329,6 @@ async function handleListToolsRequest({
1238
1329
  {
1239
1330
  name: "query",
1240
1331
  description: "Execute a MongoDB query with optional execution plan analysis",
1241
- execution: { taskSupport: "optional" },
1242
1332
  inputSchema: {
1243
1333
  type: "object",
1244
1334
  properties: {
@@ -1264,6 +1354,10 @@ async function handleListToolsRequest({
1264
1354
  description: "Number of documents to skip before returning results",
1265
1355
  default: 0
1266
1356
  },
1357
+ sort: {
1358
+ type: "object",
1359
+ description: "Sort order as a field-to-direction map, e.g. { orderDate: -1 } for descending or { name: 1 } for ascending"
1360
+ },
1267
1361
  explain: {
1268
1362
  type: "string",
1269
1363
  description: "Optional: Get query execution information",
@@ -1282,7 +1376,6 @@ async function handleListToolsRequest({
1282
1376
  {
1283
1377
  name: "aggregate",
1284
1378
  description: "Execute a MongoDB aggregation pipeline with optional execution plan analysis",
1285
- execution: { taskSupport: "optional" },
1286
1379
  inputSchema: {
1287
1380
  type: "object",
1288
1381
  properties: {
@@ -1315,7 +1408,6 @@ async function handleListToolsRequest({
1315
1408
  {
1316
1409
  name: "update",
1317
1410
  description: "Update documents in a MongoDB collection",
1318
- execution: { taskSupport: "optional" },
1319
1411
  inputSchema: {
1320
1412
  type: "object",
1321
1413
  properties: {
@@ -1352,7 +1444,6 @@ async function handleListToolsRequest({
1352
1444
  {
1353
1445
  name: "serverInfo",
1354
1446
  description: "Get MongoDB server information including version, storage engine, and other details",
1355
- execution: { taskSupport: "optional" },
1356
1447
  inputSchema: {
1357
1448
  type: "object",
1358
1449
  properties: {
@@ -1366,7 +1457,6 @@ async function handleListToolsRequest({
1366
1457
  {
1367
1458
  name: "insert",
1368
1459
  description: "Insert one or more documents into a MongoDB collection",
1369
- execution: { taskSupport: "optional" },
1370
1460
  inputSchema: {
1371
1461
  type: "object",
1372
1462
  properties: {
@@ -1404,7 +1494,6 @@ async function handleListToolsRequest({
1404
1494
  {
1405
1495
  name: "createIndex",
1406
1496
  description: "Create one or more indexes on a MongoDB collection",
1407
- execution: { taskSupport: "optional" },
1408
1497
  inputSchema: {
1409
1498
  type: "object",
1410
1499
  properties: {
@@ -1471,7 +1560,6 @@ async function handleListToolsRequest({
1471
1560
  {
1472
1561
  name: "count",
1473
1562
  description: "Count documents in a collection matching a query",
1474
- execution: { taskSupport: "optional" },
1475
1563
  inputSchema: {
1476
1564
  type: "object",
1477
1565
  properties: {
@@ -1502,7 +1590,6 @@ async function handleListToolsRequest({
1502
1590
  {
1503
1591
  name: "listCollections",
1504
1592
  description: "List all collections in the MongoDB database",
1505
- execution: { taskSupport: "optional" },
1506
1593
  inputSchema: {
1507
1594
  type: "object",
1508
1595
  properties: {
@@ -1532,29 +1619,39 @@ async function handleListToolsRequest({
1532
1619
  }
1533
1620
  }
1534
1621
  }
1622
+ },
1623
+ {
1624
+ name: "convertTime",
1625
+ description: "Convert a Unix timestamp or date string to multiple formats (UTC ISO 8601, GMT, Unix seconds/milliseconds) and report the server's current time and timezone. Useful for building unambiguous date queries when the server and user are in different timezones. Omit 'input' to get the current time.",
1626
+ inputSchema: {
1627
+ type: "object",
1628
+ properties: {
1629
+ input: {
1630
+ type: "string",
1631
+ description: "A Unix timestamp (seconds or milliseconds, as a number or numeric string) or a date string (ISO 8601, with or without a timezone offset). Omit to use the current server time."
1632
+ }
1633
+ }
1634
+ }
1535
1635
  }
1536
1636
  ]
1537
1637
  };
1538
1638
  }
1539
1639
 
1540
1640
  // src/server.ts
1541
- function createSendProgress(extra) {
1542
- const progressToken = extra._meta?.progressToken;
1641
+ function createSendProgress(progressToken, notify) {
1543
1642
  if (progressToken === void 0) return void 0;
1544
- return (progress, total, message) => extra.sendNotification({
1643
+ return (progress, total, message) => notify({
1545
1644
  method: "notifications/progress",
1546
1645
  params: { progressToken, progress, total, message }
1547
1646
  });
1548
1647
  }
1549
- function createServer(client, db, isReadOnlyMode = false, options = {}) {
1550
- const taskStore = new InMemoryTaskStore();
1551
- const taskMessageQueue = new InMemoryTaskMessageQueue();
1648
+ function createServer(client, db, isReadOnlyMode = false, allowCrossDb = false, allowServerJs = false, options = {}) {
1552
1649
  const server = new Server(
1553
1650
  {
1554
1651
  name: "mongodb",
1555
1652
  title: "MongoDB MCP Server",
1556
- version: "2.1.1",
1557
- description: "MCP server for MongoDB: query, aggregate, and manage collections with read-only mode, progress notifications, cancellation, and task support",
1653
+ version: "3.0.1",
1654
+ description: "MCP server for MongoDB: query, aggregate, and manage collections with read-only mode, progress notifications, and cancellation",
1558
1655
  websiteUrl: "https://github.com/kiliczsh/mcp-mongo-server",
1559
1656
  ...options
1560
1657
  },
@@ -1563,110 +1660,109 @@ function createServer(client, db, isReadOnlyMode = false, options = {}) {
1563
1660
  completions: {},
1564
1661
  resources: {},
1565
1662
  tools: {},
1566
- prompts: {},
1567
- tasks: {
1568
- list: {},
1569
- cancel: {},
1570
- requests: { tools: { call: {} } }
1571
- }
1663
+ prompts: {}
1572
1664
  },
1573
- taskStore,
1574
- taskMessageQueue,
1575
1665
  ...options
1576
1666
  }
1577
1667
  );
1578
1668
  server.setRequestHandler(
1579
- PingRequestSchema,
1580
- (request, extra) => handlePingRequest({
1669
+ "ping",
1670
+ (request, ctx) => handlePingRequest({
1581
1671
  request,
1582
1672
  client,
1583
1673
  db,
1584
1674
  isReadOnlyMode,
1585
- signal: extra.signal
1675
+ signal: ctx.mcpReq.signal
1586
1676
  })
1587
1677
  );
1588
1678
  server.setRequestHandler(
1589
- ListResourcesRequestSchema,
1590
- (request, extra) => handleListResourcesRequest({
1679
+ "resources/list",
1680
+ (request, ctx) => handleListResourcesRequest({
1591
1681
  request,
1592
1682
  client,
1593
1683
  db,
1594
1684
  isReadOnlyMode,
1595
- signal: extra.signal
1685
+ signal: ctx.mcpReq.signal
1596
1686
  })
1597
1687
  );
1598
1688
  server.setRequestHandler(
1599
- ReadResourceRequestSchema,
1600
- (request, extra) => handleReadResourceRequest({
1689
+ "resources/read",
1690
+ (request, ctx) => handleReadResourceRequest({
1601
1691
  request,
1602
1692
  client,
1603
1693
  db,
1604
1694
  isReadOnlyMode,
1605
- signal: extra.signal,
1606
- sendProgress: createSendProgress(extra)
1695
+ signal: ctx.mcpReq.signal,
1696
+ sendProgress: createSendProgress(
1697
+ request.params._meta?.progressToken,
1698
+ ctx.mcpReq.notify
1699
+ )
1607
1700
  })
1608
1701
  );
1609
1702
  server.setRequestHandler(
1610
- ListToolsRequestSchema,
1611
- (request, extra) => handleListToolsRequest({
1703
+ "tools/list",
1704
+ (request, ctx) => handleListToolsRequest({
1612
1705
  request,
1613
1706
  client,
1614
1707
  db,
1615
1708
  isReadOnlyMode,
1616
- signal: extra.signal
1709
+ signal: ctx.mcpReq.signal
1617
1710
  })
1618
1711
  );
1619
- server.setRequestHandler(CallToolRequestSchema, (request, extra) => {
1620
- const hasTaskParams = !!request.params.task;
1621
- return handleCallToolRequest({
1712
+ server.setRequestHandler(
1713
+ "tools/call",
1714
+ (request, ctx) => handleCallToolRequest({
1622
1715
  request,
1623
1716
  client,
1624
1717
  db,
1625
1718
  isReadOnlyMode,
1626
- signal: extra.signal,
1627
- taskStore: hasTaskParams ? extra.taskStore : void 0,
1628
- taskTtl: hasTaskParams ? extra.taskRequestedTtl : void 0
1629
- });
1630
- });
1719
+ allowCrossDb,
1720
+ allowServerJs,
1721
+ signal: ctx.mcpReq.signal
1722
+ })
1723
+ );
1631
1724
  server.setRequestHandler(
1632
- ListPromptsRequestSchema,
1633
- (request, extra) => handleListPromptsRequest({
1725
+ "prompts/list",
1726
+ (request, ctx) => handleListPromptsRequest({
1634
1727
  request,
1635
1728
  client,
1636
1729
  db,
1637
1730
  isReadOnlyMode,
1638
- signal: extra.signal
1731
+ signal: ctx.mcpReq.signal
1639
1732
  })
1640
1733
  );
1641
1734
  server.setRequestHandler(
1642
- GetPromptRequestSchema,
1643
- (request, extra) => handleGetPromptRequest({
1735
+ "prompts/get",
1736
+ (request, ctx) => handleGetPromptRequest({
1644
1737
  request,
1645
1738
  client,
1646
1739
  db,
1647
1740
  isReadOnlyMode,
1648
- signal: extra.signal,
1649
- sendProgress: createSendProgress(extra)
1741
+ signal: ctx.mcpReq.signal,
1742
+ sendProgress: createSendProgress(
1743
+ request.params._meta?.progressToken,
1744
+ ctx.mcpReq.notify
1745
+ )
1650
1746
  })
1651
1747
  );
1652
1748
  server.setRequestHandler(
1653
- ListResourceTemplatesRequestSchema,
1654
- (request, extra) => handleListResourceTemplatesRequest({
1749
+ "resources/templates/list",
1750
+ (request, ctx) => handleListResourceTemplatesRequest({
1655
1751
  request,
1656
1752
  client,
1657
1753
  db,
1658
1754
  isReadOnlyMode,
1659
- signal: extra.signal
1755
+ signal: ctx.mcpReq.signal
1660
1756
  })
1661
1757
  );
1662
1758
  server.setRequestHandler(
1663
- CompleteRequestSchema,
1664
- (request, extra) => handleCompletionRequest({
1759
+ "completion/complete",
1760
+ (request, ctx) => handleCompletionRequest({
1665
1761
  request,
1666
1762
  client,
1667
1763
  db,
1668
1764
  isReadOnlyMode,
1669
- signal: extra.signal
1765
+ signal: ctx.mcpReq.signal
1670
1766
  })
1671
1767
  );
1672
1768
  return server;
@@ -1678,12 +1774,20 @@ async function main() {
1678
1774
  const args = process.argv.slice(2);
1679
1775
  let connectionUrl = "";
1680
1776
  let readOnlyMode = process.env.MCP_MONGODB_READONLY === "true" || false;
1777
+ let allowCrossDb = process.env.MCP_MONGODB_ALLOW_CROSS_DB === "true" || false;
1778
+ let allowServerJs = process.env.MCP_MONGODB_ALLOW_SERVER_JS === "true" || false;
1681
1779
  let transportMode = "stdio";
1682
1780
  let port = Number(process.env.MCP_PORT) || 3001;
1683
1781
  const allowedOrigins = (process.env.MCP_HTTP_ALLOWED_ORIGINS || "").split(",").map((origin) => origin.trim()).filter(Boolean);
1782
+ let jsonLimit = process.env.MCP_HTTP_JSON_LIMIT || "10mb";
1783
+ let authToken = process.env.MCP_HTTP_AUTH_TOKEN || "";
1684
1784
  for (let i = 0; i < args.length; i++) {
1685
1785
  if (args[i] === "--read-only" || args[i] === "-r") {
1686
1786
  readOnlyMode = true;
1787
+ } else if (args[i] === "--allow-cross-db") {
1788
+ allowCrossDb = true;
1789
+ } else if (args[i] === "--allow-server-js") {
1790
+ allowServerJs = true;
1687
1791
  } else if (args[i] === "--transport" || args[i] === "-t") {
1688
1792
  const value = args[++i];
1689
1793
  if (value !== "stdio" && value !== "http") {
@@ -1701,6 +1805,12 @@ async function main() {
1701
1805
  allowedOrigins.push(
1702
1806
  ...(args[++i] || "").split(",").map((origin) => origin.trim()).filter(Boolean)
1703
1807
  );
1808
+ } else if (args[i] === "--json-limit") {
1809
+ const value = args[++i];
1810
+ if (value) jsonLimit = value;
1811
+ } else if (args[i] === "--auth-token") {
1812
+ const value = args[++i];
1813
+ if (value) authToken = value;
1704
1814
  } else if (!connectionUrl) {
1705
1815
  connectionUrl = args[i];
1706
1816
  }
@@ -1737,9 +1847,25 @@ async function main() {
1737
1847
  process.exit(1);
1738
1848
  }
1739
1849
  if (transportMode === "http") {
1740
- await startHttpServer(client, db, isReadOnlyMode, port, allowedOrigins);
1850
+ await startHttpServer(
1851
+ client,
1852
+ db,
1853
+ isReadOnlyMode,
1854
+ allowCrossDb,
1855
+ allowServerJs,
1856
+ port,
1857
+ allowedOrigins,
1858
+ jsonLimit,
1859
+ authToken
1860
+ );
1741
1861
  } else {
1742
- await startStdioServer(client, db, isReadOnlyMode);
1862
+ await startStdioServer(
1863
+ client,
1864
+ db,
1865
+ isReadOnlyMode,
1866
+ allowCrossDb,
1867
+ allowServerJs
1868
+ );
1743
1869
  }
1744
1870
  } catch (error) {
1745
1871
  console.error("Failed to connect to MongoDB:", error);
@@ -1749,10 +1875,10 @@ async function main() {
1749
1875
  process.exit(1);
1750
1876
  }
1751
1877
  }
1752
- async function startStdioServer(client, db, isReadOnlyMode) {
1753
- const server = createServer(client, db, isReadOnlyMode);
1754
- const transport = new StdioServerTransport();
1755
- await server.connect(transport);
1878
+ async function startStdioServer(client, db, isReadOnlyMode, allowCrossDb, allowServerJs) {
1879
+ serveStdio(
1880
+ () => createServer(client, db, isReadOnlyMode, allowCrossDb, allowServerJs)
1881
+ );
1756
1882
  console.warn("Server connected successfully via stdio");
1757
1883
  }
1758
1884
  function isOriginAllowed(origin, allowedOrigins) {
@@ -1769,8 +1895,17 @@ function isOriginAllowed(origin, allowedOrigins) {
1769
1895
  return false;
1770
1896
  }
1771
1897
  }
1772
- async function startHttpServer(client, db, isReadOnlyMode, port, allowedOrigins) {
1773
- const app = createMcpExpressApp({ host: "0.0.0.0" });
1898
+ function bearerTokenValid(authorization, expected) {
1899
+ const prefix = "Bearer ";
1900
+ if (!authorization?.startsWith(prefix)) {
1901
+ return false;
1902
+ }
1903
+ const provided = Buffer.from(authorization.slice(prefix.length));
1904
+ const wanted = Buffer.from(expected);
1905
+ return provided.length === wanted.length && timingSafeEqual(provided, wanted);
1906
+ }
1907
+ async function startHttpServer(client, db, isReadOnlyMode, allowCrossDb, allowServerJs, port, allowedOrigins, jsonLimit, authToken) {
1908
+ const app = createMcpExpressApp({ host: "0.0.0.0", jsonLimit });
1774
1909
  app.use((req, res, next) => {
1775
1910
  const start = Date.now();
1776
1911
  const ip = req.headers["x-forwarded-for"] || req.socket.remoteAddress;
@@ -1803,56 +1938,44 @@ async function startHttpServer(client, db, isReadOnlyMode, port, allowedOrigins)
1803
1938
  }
1804
1939
  next();
1805
1940
  });
1806
- app.post("/mcp", async (req, res) => {
1807
- const server = createServer(client, db, isReadOnlyMode);
1808
- try {
1809
- const transport = new StreamableHTTPServerTransport({
1810
- sessionIdGenerator: void 0
1811
- });
1812
- await server.connect(transport);
1813
- await transport.handleRequest(req, res, req.body);
1814
- res.on("close", () => {
1815
- transport.close();
1816
- server.close();
1817
- });
1818
- } catch (error) {
1819
- console.error("Error handling MCP request:", error);
1820
- if (!res.headersSent) {
1821
- res.status(500).json({
1941
+ if (authToken) {
1942
+ app.use((req, res, next) => {
1943
+ if (!bearerTokenValid(req.headers.authorization, authToken)) {
1944
+ res.status(401).set("WWW-Authenticate", 'Bearer realm="mcp", error="invalid_token"').json({
1822
1945
  jsonrpc: "2.0",
1823
1946
  error: {
1824
- code: -32603,
1825
- message: "Internal server error"
1947
+ code: -32001,
1948
+ message: "Unauthorized: missing or invalid bearer token"
1826
1949
  },
1827
1950
  id: null
1828
1951
  });
1952
+ return;
1829
1953
  }
1830
- }
1831
- });
1832
- app.get("/mcp", async (_req, res) => {
1833
- res.writeHead(405).end(
1834
- JSON.stringify({
1835
- jsonrpc: "2.0",
1836
- error: {
1837
- code: -32e3,
1838
- message: "Method not allowed."
1839
- },
1840
- id: null
1841
- })
1842
- );
1954
+ next();
1955
+ });
1956
+ }
1957
+ const mcpHandler = createMcpHandler(
1958
+ (_ctx) => createServer(client, db, isReadOnlyMode, allowCrossDb, allowServerJs)
1959
+ );
1960
+ const nodeHandler = toNodeHandler(mcpHandler, {
1961
+ onerror: (error) => console.error("Error handling MCP request:", error)
1843
1962
  });
1844
- app.delete("/mcp", async (_req, res) => {
1845
- res.writeHead(405).end(
1846
- JSON.stringify({
1963
+ app.all("/mcp", (req, res) => nodeHandler(req, res, req.body));
1964
+ app.use(
1965
+ (err, _req, res, next) => {
1966
+ if (res.headersSent) return next(err);
1967
+ const status = err.status ?? err.statusCode ?? 400;
1968
+ const tooLarge = err.type === "entity.too.large" || status === 413;
1969
+ res.status(tooLarge ? 413 : 400).json({
1847
1970
  jsonrpc: "2.0",
1848
1971
  error: {
1849
- code: -32e3,
1850
- message: "Method not allowed."
1972
+ code: tooLarge ? -32600 : -32700,
1973
+ message: tooLarge ? "Request body exceeds the configured size limit" : `Invalid request body: ${err.message ?? "parse error"}`
1851
1974
  },
1852
1975
  id: null
1853
- })
1854
- );
1855
- });
1976
+ });
1977
+ }
1978
+ );
1856
1979
  app.listen(port, () => {
1857
1980
  console.log(`MCP MongoDB Streamable HTTP Server listening on port ${port}`);
1858
1981
  console.log(`Endpoint: http://localhost:${port}/mcp`);
package/package.json CHANGED
@@ -1,11 +1,14 @@
1
1
  {
2
2
  "name": "mcp-mongo-server",
3
- "version": "2.1.1",
3
+ "version": "3.0.1",
4
4
  "description": "A Model Context Protocol server for MongoDB connections",
5
5
  "private": false,
6
6
  "type": "module",
7
+ "engines": {
8
+ "node": ">=20"
9
+ },
7
10
  "bin": {
8
- "mongodb": "./build/index.js"
11
+ "mcp-mongo-server": "./build/index.js"
9
12
  },
10
13
  "files": [
11
14
  "build"
@@ -23,12 +26,14 @@
23
26
  "inspector": "mcp-inspector --config inspector-config.json --server mongodb"
24
27
  },
25
28
  "dependencies": {
26
- "@modelcontextprotocol/sdk": "^1.29.0",
27
- "mongodb": "^7.5.0",
28
- "zod": "^4.4.3"
29
+ "@modelcontextprotocol/express": "^2.0.0",
30
+ "@modelcontextprotocol/node": "^2.0.0",
31
+ "@modelcontextprotocol/server": "^2.0.0",
32
+ "mongodb": "^7.5.0"
29
33
  },
30
34
  "devDependencies": {
31
35
  "@biomejs/biome": "^2.5.5",
36
+ "@modelcontextprotocol/client": "^2.0.0",
32
37
  "@modelcontextprotocol/inspector": "^1.0.0",
33
38
  "@types/node": "^26.1.1",
34
39
  "tsup": "^8.5.1",