mcp-mongo-server 0.1.3 → 1.0.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 (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +38 -17
  3. package/build/index.js +662 -99
  4. package/package.json +51 -51
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 kiliczsh
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -3,9 +3,9 @@
3
3
  ![NPM Version](https://img.shields.io/npm/v/mcp-mongo-server)
4
4
  ![NPM Downloads](https://img.shields.io/npm/dm/mcp-mongo-server)
5
5
  ![NPM License](https://img.shields.io/npm/l/mcp-mongo-server)
6
- [![smithery badge](https://smithery.ai/badge/mcp-mongo-server)](https://smithery.ai/protocol/mcp-mongo-server)
6
+ [![smithery badge](https://smithery.ai/badge/mcp-mongo-server)](https://smithery.ai/server/mcp-mongo-server)
7
7
 
8
- A Model Context Protocol server that provides access to MongoDB databases. This server enables LLMs to inspect collection schemas and execute read-only queries.
8
+ A Model Context Protocol server that provides access to MongoDB databases. This server enables LLMs to inspect collection schemas and execute MongoDB operations.
9
9
 
10
10
  ## Demo
11
11
 
@@ -20,23 +20,44 @@ A Model Context Protocol server that provides access to MongoDB databases. This
20
20
 
21
21
  ### Tools
22
22
  - **query**
23
- - Execute read-only MongoDB queries against the connected database
24
- - Input: `query` (object): The MongoDB query to execute
25
- - All queries are executed with read-only permissions
23
+ - Execute MongoDB queries with optional execution plan analysis
24
+ - Input: Collection name, filter, projection, limit, explain options
25
+ - Returns query results or execution plan
26
26
 
27
27
  - **aggregate**
28
- - Execute read-only MongoDB queries against the connected database
29
- - Input: `aggregate` (object): The MongoDB query to execute
30
- - All queries are executed with read-only permissions
28
+ - Execute MongoDB aggregation pipelines with optional execution plan analysis
29
+ - Input: Collection name, pipeline stages, explain options
30
+ - Returns aggregation results or execution plan
31
+
32
+ - **update**
33
+ - Update documents in a collection
34
+ - Input: Collection name, filter, update operations, upsert/multi options
35
+ - Returns update operation results
36
+
37
+ - **serverInfo**
38
+ - Get MongoDB server information and status
39
+ - Input: Optional debug info flag
40
+ - Returns version, storage engine, and server details
41
+
42
+ - **insert**
43
+ - Insert documents into a collection
44
+ - Input: Collection name, documents array, write options
45
+ - Returns insert operation results
46
+
47
+ - **createIndex**
48
+ - Create indexes on a collection
49
+ - Input: Collection name, index specifications, write options
50
+ - Returns index creation results
51
+
52
+ - **count**
53
+ - Count documents matching a query
54
+ - Input: Collection name, query filter, count options
55
+ - Returns document count
31
56
 
32
57
  ### Prompts
33
- - `query` - Provide insights about the collection's structure, data types, and basic statistics
34
- - Input: `collection` (string): The name of the collection to analyze
35
- - Output: `text` (string): A summary of the collection's structure, data types, and basic statistics
36
- - `aggregate` - Provide insights about the collection's structure, data types, and basic statistics
37
- - Input: `collection` (string): The name of the collection to analyze
38
- - Output: `text` (string): A summary of the collection's structure, data types, and basic statistics
39
-
58
+ - `analyze_collection` - Analyze collection structure and contents
59
+ - Input: Collection name
60
+ - Output: Insights about schema, data types, and statistics
40
61
 
41
62
 
42
63
  ## Development
@@ -125,10 +146,10 @@ To use this server with the Claude Desktop app, add the following configuration
125
146
 
126
147
  ### Installing via Smithery
127
148
 
128
- To install MCP MongoDB Server for Claude Desktop automatically via [Smithery](https://smithery.ai/protocol/mcp-mongo-server):
149
+ To install MCP MongoDB Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/mcp-mongo-server):
129
150
 
130
151
  ```bash
131
- npx @smithery/cli install mcp-mongo-server --client claude
152
+ npx -y @smithery/cli install mcp-mongo-server --client claude
132
153
  ```
133
154
 
134
155
  ### Installing via mcp-get
package/build/index.js CHANGED
@@ -9,8 +9,8 @@
9
9
  */
10
10
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
11
11
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
12
- import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, ListResourceTemplatesRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
13
- import { MongoClient, ServerApiVersion } from "mongodb";
12
+ import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, ListResourceTemplatesRequestSchema, PingRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
13
+ import { MongoClient } from "mongodb";
14
14
  /**
15
15
  * MongoDB connection client and database reference
16
16
  */
@@ -22,7 +22,7 @@ let db = null;
22
22
  */
23
23
  const server = new Server({
24
24
  name: "mongodb",
25
- version: "0.1.3",
25
+ version: "1.0.0",
26
26
  }, {
27
27
  capabilities: {
28
28
  resources: {},
@@ -35,13 +35,7 @@ const server = new Server({
35
35
  */
36
36
  async function connectToMongoDB(url) {
37
37
  try {
38
- client = new MongoClient(url, {
39
- serverApi: {
40
- version: ServerApiVersion.v1,
41
- strict: true,
42
- deprecationErrors: true,
43
- },
44
- });
38
+ client = new MongoClient(url);
45
39
  await client.connect();
46
40
  db = client.db();
47
41
  return true;
@@ -51,6 +45,26 @@ async function connectToMongoDB(url) {
51
45
  return false;
52
46
  }
53
47
  }
48
+ /**
49
+ * Handler for ping requests to check server health
50
+ */
51
+ server.setRequestHandler(PingRequestSchema, async () => {
52
+ try {
53
+ // Check MongoDB connection
54
+ if (!client) {
55
+ throw new Error("MongoDB connection is not available");
56
+ }
57
+ // Ping MongoDB to verify connection
58
+ await db.command({ ping: 1 });
59
+ return {};
60
+ }
61
+ catch (error) {
62
+ if (error instanceof Error) {
63
+ throw new Error(`MongoDB ping failed: ${error.message}`);
64
+ }
65
+ throw new Error("MongoDB ping failed: Unknown error");
66
+ }
67
+ });
54
68
  /**
55
69
  * Handler for listing available collections as resources.
56
70
  * Each collection is exposed as a resource with:
@@ -74,7 +88,7 @@ server.setRequestHandler(ListResourcesRequestSchema, async () => {
74
88
  if (error instanceof Error) {
75
89
  throw new Error(`Failed to list collections: ${error.message}`);
76
90
  }
77
- throw new Error('Failed to list collections: Unknown error');
91
+ throw new Error("Failed to list collections: Unknown error");
78
92
  }
79
93
  });
80
94
  /**
@@ -89,29 +103,33 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
89
103
  const sample = await collection.findOne({});
90
104
  const indexes = await collection.indexes();
91
105
  // Infer schema from sample document
92
- const schema = sample ? {
93
- type: "collection",
94
- name: collectionName,
95
- fields: Object.entries(sample).map(([key, value]) => ({
96
- name: key,
97
- type: typeof value,
98
- })),
99
- indexes: indexes.map((idx) => ({
100
- name: idx.name,
101
- keys: idx.key,
102
- })),
103
- } : {
104
- type: "collection",
105
- name: collectionName,
106
- fields: [],
107
- indexes: [],
108
- };
106
+ const schema = sample
107
+ ? {
108
+ type: "collection",
109
+ name: collectionName,
110
+ fields: Object.entries(sample).map(([key, value]) => ({
111
+ name: key,
112
+ type: typeof value,
113
+ })),
114
+ indexes: indexes.map((idx) => ({
115
+ name: idx.name,
116
+ keys: idx.key,
117
+ })),
118
+ }
119
+ : {
120
+ type: "collection",
121
+ name: collectionName,
122
+ fields: [],
123
+ indexes: [],
124
+ };
109
125
  return {
110
- contents: [{
126
+ contents: [
127
+ {
111
128
  uri: request.params.uri,
112
129
  mimeType: "application/json",
113
- text: JSON.stringify(schema, null, 2)
114
- }]
130
+ text: JSON.stringify(schema, null, 2),
131
+ },
132
+ ],
115
133
  };
116
134
  }
117
135
  catch (error) {
@@ -130,49 +148,237 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
130
148
  tools: [
131
149
  {
132
150
  name: "query",
133
- description: "Execute a MongoDB query",
151
+ description: "Execute a MongoDB query with optional execution plan analysis",
134
152
  inputSchema: {
135
153
  type: "object",
136
154
  properties: {
137
155
  collection: {
138
156
  type: "string",
139
- description: "Name of the collection to query"
157
+ description: "Name of the collection to query",
140
158
  },
141
159
  filter: {
142
160
  type: "object",
143
- description: "MongoDB query filter"
161
+ description: "MongoDB query filter",
144
162
  },
145
163
  projection: {
146
164
  type: "object",
147
- description: "Fields to include/exclude"
165
+ description: "Fields to include/exclude",
148
166
  },
149
167
  limit: {
150
168
  type: "number",
151
- description: "Maximum number of documents to return"
152
- }
169
+ description: "Maximum number of documents to return",
170
+ },
171
+ explain: {
172
+ type: "string",
173
+ description: "Optional: Get query execution information (queryPlanner, executionStats, or allPlansExecution)",
174
+ enum: ["queryPlanner", "executionStats", "allPlansExecution"],
175
+ },
153
176
  },
154
- required: ["collection"]
155
- }
177
+ required: ["collection"],
178
+ },
156
179
  },
157
180
  {
158
181
  name: "aggregate",
159
- description: "Execute a MongoDB aggregation pipeline",
182
+ description: "Execute a MongoDB aggregation pipeline with optional execution plan analysis",
160
183
  inputSchema: {
161
184
  type: "object",
162
185
  properties: {
163
186
  collection: {
164
187
  type: "string",
165
- description: "Name of the collection to aggregate"
188
+ description: "Name of the collection to aggregate",
166
189
  },
167
190
  pipeline: {
168
191
  type: "array",
169
- description: "Aggregation pipeline stages"
170
- }
192
+ description: "Aggregation pipeline stages",
193
+ },
194
+ explain: {
195
+ type: "string",
196
+ description: "Optional: Get aggregation execution information (queryPlanner, executionStats, or allPlansExecution)",
197
+ enum: ["queryPlanner", "executionStats", "allPlansExecution"],
198
+ },
171
199
  },
172
- required: ["collection", "pipeline"]
173
- }
174
- }
175
- ]
200
+ required: ["collection", "pipeline"],
201
+ },
202
+ },
203
+ {
204
+ name: "update",
205
+ description: "Update documents in a MongoDB collection",
206
+ inputSchema: {
207
+ type: "object",
208
+ properties: {
209
+ collection: {
210
+ type: "string",
211
+ description: "Name of the collection to update",
212
+ },
213
+ filter: {
214
+ type: "object",
215
+ description: "Filter to select documents to update",
216
+ },
217
+ update: {
218
+ type: "object",
219
+ description: "Update operations to apply ($set, $unset, $inc, etc.)",
220
+ },
221
+ upsert: {
222
+ type: "boolean",
223
+ description: "Create a new document if no documents match the filter",
224
+ },
225
+ multi: {
226
+ type: "boolean",
227
+ description: "Update multiple documents that match the filter",
228
+ },
229
+ },
230
+ required: ["collection", "filter", "update"],
231
+ },
232
+ },
233
+ {
234
+ name: "serverInfo",
235
+ description: "Get MongoDB server information including version, storage engine, and other details",
236
+ inputSchema: {
237
+ type: "object",
238
+ properties: {
239
+ includeDebugInfo: {
240
+ type: "boolean",
241
+ description: "Include additional debug information about the server",
242
+ },
243
+ },
244
+ },
245
+ },
246
+ {
247
+ name: "insert",
248
+ description: "Insert one or more documents into a MongoDB collection",
249
+ inputSchema: {
250
+ type: "object",
251
+ properties: {
252
+ collection: {
253
+ type: "string",
254
+ description: "Name of the collection to insert into",
255
+ },
256
+ documents: {
257
+ type: "array",
258
+ description: "Array of documents to insert",
259
+ items: {
260
+ type: "object",
261
+ },
262
+ },
263
+ ordered: {
264
+ type: "boolean",
265
+ description: "Optional: If true, perform an ordered insert of the documents. If false, perform an unordered insert",
266
+ },
267
+ writeConcern: {
268
+ type: "object",
269
+ description: "Optional: Write concern for the insert operation",
270
+ },
271
+ bypassDocumentValidation: {
272
+ type: "boolean",
273
+ description: "Optional: Allow insert to bypass schema validation",
274
+ },
275
+ },
276
+ required: ["collection", "documents"],
277
+ },
278
+ },
279
+ {
280
+ name: "createIndex",
281
+ description: "Create one or more indexes on a MongoDB collection",
282
+ inputSchema: {
283
+ type: "object",
284
+ properties: {
285
+ collection: {
286
+ type: "string",
287
+ description: "Name of the collection to create indexes on",
288
+ },
289
+ indexes: {
290
+ type: "array",
291
+ description: "Array of index specifications",
292
+ items: {
293
+ type: "object",
294
+ properties: {
295
+ key: {
296
+ type: "object",
297
+ description: "Index key pattern, e.g. { field: 1 } for ascending, { field: -1 } for descending",
298
+ },
299
+ name: {
300
+ type: "string",
301
+ description: "Optional: Name of the index",
302
+ },
303
+ unique: {
304
+ type: "boolean",
305
+ description: "Optional: If true, creates a unique index",
306
+ },
307
+ sparse: {
308
+ type: "boolean",
309
+ description: "Optional: If true, creates a sparse index",
310
+ },
311
+ background: {
312
+ type: "boolean",
313
+ description: "Optional: If true, creates the index in the background",
314
+ },
315
+ expireAfterSeconds: {
316
+ type: "number",
317
+ description: "Optional: Specifies the TTL for documents (time to live)",
318
+ },
319
+ partialFilterExpression: {
320
+ type: "object",
321
+ description: "Optional: Filter expression for partial indexes",
322
+ },
323
+ },
324
+ required: ["key"],
325
+ },
326
+ },
327
+ writeConcern: {
328
+ type: "object",
329
+ description: "Optional: Write concern for the index creation",
330
+ },
331
+ commitQuorum: {
332
+ type: ["string", "number"],
333
+ description: "Optional: Number of voting members required to create index",
334
+ },
335
+ },
336
+ required: ["collection", "indexes"],
337
+ },
338
+ },
339
+ {
340
+ name: "count",
341
+ description: "Count the number of documents in a collection that match a query",
342
+ inputSchema: {
343
+ type: "object",
344
+ properties: {
345
+ collection: {
346
+ type: "string",
347
+ description: "Name of the collection to count documents in",
348
+ },
349
+ query: {
350
+ type: "object",
351
+ description: "Optional: Query filter to select documents to count",
352
+ },
353
+ limit: {
354
+ type: "integer",
355
+ description: "Optional: Maximum number of documents to count",
356
+ },
357
+ skip: {
358
+ type: "integer",
359
+ description: "Optional: Number of documents to skip before counting",
360
+ },
361
+ hint: {
362
+ type: "object",
363
+ description: "Optional: Index hint to force query plan",
364
+ },
365
+ readConcern: {
366
+ type: "object",
367
+ description: "Optional: Read concern for the count operation",
368
+ },
369
+ maxTimeMS: {
370
+ type: "integer",
371
+ description: "Optional: Maximum time to allow the count to run",
372
+ },
373
+ collation: {
374
+ type: "object",
375
+ description: "Optional: Collation rules for string comparison",
376
+ },
377
+ },
378
+ required: ["collection"],
379
+ },
380
+ },
381
+ ],
176
382
  };
177
383
  });
178
384
  /**
@@ -183,42 +389,66 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
183
389
  const collection = db.collection(request.params.arguments?.collection);
184
390
  switch (request.params.name) {
185
391
  case "query": {
186
- const { filter, projection, limit } = request.params.arguments || {};
392
+ const { filter, projection, limit, explain } = request.params.arguments || {};
187
393
  // Validate collection name to prevent access to system collections
188
- if (collection.collectionName.startsWith('system.')) {
189
- throw new Error('Access to system collections is not allowed');
394
+ if (collection.collectionName.startsWith("system.")) {
395
+ throw new Error("Access to system collections is not allowed");
190
396
  }
191
397
  // Validate and parse filter
192
398
  let queryFilter = {};
193
399
  if (filter) {
194
- if (typeof filter === 'string') {
400
+ if (typeof filter === "string") {
195
401
  try {
196
402
  queryFilter = JSON.parse(filter);
197
403
  }
198
404
  catch (e) {
199
- throw new Error('Invalid filter format: must be a valid JSON object');
405
+ throw new Error("Invalid filter format: must be a valid JSON object");
200
406
  }
201
407
  }
202
- else if (typeof filter === 'object' && filter !== null && !Array.isArray(filter)) {
408
+ else if (typeof filter === "object" &&
409
+ filter !== null &&
410
+ !Array.isArray(filter)) {
203
411
  queryFilter = filter;
204
412
  }
205
413
  else {
206
- throw new Error('Query filter must be a plain object or ObjectId');
414
+ throw new Error("Query filter must be a plain object or ObjectId");
207
415
  }
208
416
  }
209
417
  // Execute the find operation with error handling
210
418
  try {
211
- const cursor = collection.find(queryFilter, {
212
- projection,
213
- limit: limit || 100
214
- });
215
- const results = await cursor.toArray();
216
- return {
217
- content: [{
218
- type: "text",
219
- text: JSON.stringify(results, null, 2)
220
- }]
221
- };
419
+ if (explain) {
420
+ // Use explain for query analysis
421
+ const explainResult = await collection
422
+ .find(queryFilter, {
423
+ projection,
424
+ limit: limit || 100,
425
+ })
426
+ .explain(explain);
427
+ return {
428
+ content: [
429
+ {
430
+ type: "text",
431
+ text: JSON.stringify(explainResult, null, 2),
432
+ },
433
+ ],
434
+ };
435
+ }
436
+ else {
437
+ // Regular query execution
438
+ const cursor = collection.find(queryFilter, {
439
+ projection,
440
+ limit: limit || 100,
441
+ });
442
+ const results = await cursor.toArray();
443
+ return {
444
+ content: [
445
+ {
446
+ type: "text",
447
+ text: JSON.stringify(results, null, 2),
448
+ },
449
+ ],
450
+ };
451
+ }
222
452
  }
223
453
  catch (error) {
224
454
  if (error instanceof Error) {
@@ -228,29 +458,362 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
228
458
  }
229
459
  }
230
460
  case "aggregate": {
231
- const { pipeline } = request.params.arguments || {};
461
+ const { pipeline, explain } = request.params.arguments || {};
232
462
  if (!Array.isArray(pipeline)) {
233
463
  throw new Error("Pipeline must be an array");
234
464
  }
235
465
  // Validate collection name to prevent access to system collections
236
- if (collection.collectionName.startsWith('system.')) {
237
- throw new Error('Access to system collections is not allowed');
466
+ if (collection.collectionName.startsWith("system.")) {
467
+ throw new Error("Access to system collections is not allowed");
238
468
  }
239
469
  // Execute the aggregation operation with error handling
240
470
  try {
241
- const results = await collection.aggregate(pipeline).toArray();
471
+ if (explain) {
472
+ // Use explain for aggregation analysis
473
+ const explainResult = await collection
474
+ .aggregate(pipeline, {
475
+ explain: {
476
+ verbosity: explain,
477
+ },
478
+ })
479
+ .toArray();
480
+ return {
481
+ content: [
482
+ {
483
+ type: "text",
484
+ text: JSON.stringify(explainResult, null, 2),
485
+ },
486
+ ],
487
+ };
488
+ }
489
+ else {
490
+ // Regular aggregation execution
491
+ const results = await collection.aggregate(pipeline).toArray();
492
+ return {
493
+ content: [
494
+ {
495
+ type: "text",
496
+ text: JSON.stringify(results, null, 2),
497
+ },
498
+ ],
499
+ };
500
+ }
501
+ }
502
+ catch (error) {
503
+ if (error instanceof Error) {
504
+ throw new Error(`Failed to aggregate collection ${collection.collectionName}: ${error.message}`);
505
+ }
506
+ throw new Error(`Failed to aggregate collection ${collection.collectionName}: Unknown error`);
507
+ }
508
+ }
509
+ case "update": {
510
+ const { filter, update, upsert, multi } = request.params.arguments || {};
511
+ // Validate collection name to prevent access to system collections
512
+ if (collection.collectionName.startsWith("system.")) {
513
+ throw new Error("Access to system collections is not allowed");
514
+ }
515
+ // Validate and parse filter
516
+ let queryFilter = {};
517
+ if (filter) {
518
+ if (typeof filter === "string") {
519
+ try {
520
+ queryFilter = JSON.parse(filter);
521
+ }
522
+ catch (e) {
523
+ throw new Error("Invalid filter format: must be a valid JSON object");
524
+ }
525
+ }
526
+ else if (typeof filter === "object" &&
527
+ filter !== null &&
528
+ !Array.isArray(filter)) {
529
+ queryFilter = filter;
530
+ }
531
+ else {
532
+ throw new Error("Query filter must be a plain object or ObjectId");
533
+ }
534
+ }
535
+ // Validate update operations
536
+ if (!update || typeof update !== "object" || Array.isArray(update)) {
537
+ throw new Error("Update must be a valid MongoDB update document");
538
+ }
539
+ // Check if update operations use valid operators
540
+ const validUpdateOperators = [
541
+ "$set",
542
+ "$unset",
543
+ "$inc",
544
+ "$push",
545
+ "$pull",
546
+ "$addToSet",
547
+ "$pop",
548
+ "$rename",
549
+ "$mul",
550
+ ];
551
+ const hasValidOperator = Object.keys(update).some((key) => validUpdateOperators.includes(key));
552
+ if (!hasValidOperator) {
553
+ throw new Error("Update must include at least one valid update operator ($set, $unset, etc.)");
554
+ }
555
+ try {
556
+ const options = {
557
+ upsert: !!upsert,
558
+ multi: !!multi,
559
+ };
560
+ // Use updateOne or updateMany based on multi option
561
+ const updateMethod = options.multi ? "updateMany" : "updateOne";
562
+ const result = await collection[updateMethod](queryFilter, update, options);
242
563
  return {
243
- content: [{
564
+ content: [
565
+ {
244
566
  type: "text",
245
- text: JSON.stringify(results, null, 2)
246
- }]
567
+ text: JSON.stringify({
568
+ matchedCount: result.matchedCount,
569
+ modifiedCount: result.modifiedCount,
570
+ upsertedCount: result.upsertedCount,
571
+ upsertedId: result.upsertedId,
572
+ }, null, 2),
573
+ },
574
+ ],
247
575
  };
248
576
  }
249
577
  catch (error) {
250
578
  if (error instanceof Error) {
251
- throw new Error(`Failed to aggregate collection ${collection.collectionName}: ${error.message}`);
579
+ throw new Error(`Failed to update collection ${collection.collectionName}: ${error.message}`);
252
580
  }
253
- throw new Error(`Failed to aggregate collection ${collection.collectionName}: Unknown error`);
581
+ throw new Error(`Failed to update collection ${collection.collectionName}: Unknown error`);
582
+ }
583
+ }
584
+ case "serverInfo": {
585
+ const { includeDebugInfo } = request.params.arguments || {};
586
+ try {
587
+ // Get basic server information using buildInfo command
588
+ const buildInfo = await db.command({ buildInfo: 1 });
589
+ // Get additional server status if debug info is requested
590
+ let serverStatus = null;
591
+ if (includeDebugInfo) {
592
+ serverStatus = await db.command({ serverStatus: 1 });
593
+ }
594
+ // Construct the response
595
+ const serverInfo = {
596
+ version: buildInfo.version,
597
+ gitVersion: buildInfo.gitVersion,
598
+ modules: buildInfo.modules,
599
+ allocator: buildInfo.allocator,
600
+ javascriptEngine: buildInfo.javascriptEngine,
601
+ sysInfo: buildInfo.sysInfo,
602
+ storageEngines: buildInfo.storageEngines,
603
+ debug: buildInfo.debug,
604
+ maxBsonObjectSize: buildInfo.maxBsonObjectSize,
605
+ openssl: buildInfo.openssl,
606
+ buildEnvironment: buildInfo.buildEnvironment,
607
+ bits: buildInfo.bits,
608
+ ok: buildInfo.ok,
609
+ status: {},
610
+ };
611
+ // Add server status information if requested
612
+ if (serverStatus) {
613
+ serverInfo.status = {
614
+ host: serverStatus.host,
615
+ version: serverStatus.version,
616
+ process: serverStatus.process,
617
+ pid: serverStatus.pid,
618
+ uptime: serverStatus.uptime,
619
+ uptimeMillis: serverStatus.uptimeMillis,
620
+ uptimeEstimate: serverStatus.uptimeEstimate,
621
+ localTime: serverStatus.localTime,
622
+ connections: serverStatus.connections,
623
+ network: serverStatus.network,
624
+ memory: serverStatus.mem,
625
+ storageEngine: serverStatus.storageEngine,
626
+ security: serverStatus.security,
627
+ };
628
+ }
629
+ return {
630
+ content: [
631
+ {
632
+ type: "text",
633
+ text: JSON.stringify(serverInfo, null, 2),
634
+ },
635
+ ],
636
+ };
637
+ }
638
+ catch (error) {
639
+ if (error instanceof Error) {
640
+ throw new Error(`Failed to get server information: ${error.message}`);
641
+ }
642
+ throw new Error("Failed to get server information: Unknown error");
643
+ }
644
+ }
645
+ case "insert": {
646
+ const { documents, ordered, writeConcern, bypassDocumentValidation } = request.params.arguments || {};
647
+ // Validate collection name to prevent access to system collections
648
+ if (collection.collectionName.startsWith("system.")) {
649
+ throw new Error("Access to system collections is not allowed");
650
+ }
651
+ // Validate documents array
652
+ if (!Array.isArray(documents)) {
653
+ throw new Error("Documents must be an array");
654
+ }
655
+ if (documents.length === 0) {
656
+ throw new Error("Documents array cannot be empty");
657
+ }
658
+ if (!documents.every((doc) => doc && typeof doc === "object" && !Array.isArray(doc))) {
659
+ throw new Error("Each document must be a valid MongoDB document object");
660
+ }
661
+ try {
662
+ // Prepare insert options
663
+ const options = {
664
+ ordered: ordered !== false, // default to true if not specified
665
+ writeConcern,
666
+ bypassDocumentValidation,
667
+ };
668
+ // Use insertMany for consistency, it works for single documents too
669
+ const result = await collection.insertMany(documents, options);
670
+ return {
671
+ content: [
672
+ {
673
+ type: "text",
674
+ text: JSON.stringify({
675
+ acknowledged: result.acknowledged,
676
+ insertedCount: result.insertedCount,
677
+ insertedIds: result.insertedIds,
678
+ }, null, 2),
679
+ },
680
+ ],
681
+ };
682
+ }
683
+ catch (error) {
684
+ if (error instanceof Error) {
685
+ // Handle bulk write errors specially to provide more detail
686
+ if (error.name === "BulkWriteError") {
687
+ const bulkError = error;
688
+ return {
689
+ content: [
690
+ {
691
+ type: "text",
692
+ text: JSON.stringify({
693
+ error: "Bulk write error occurred",
694
+ writeErrors: bulkError.writeErrors,
695
+ insertedCount: bulkError.result?.nInserted || 0,
696
+ failedCount: bulkError.result?.nFailedInserts || 0,
697
+ }, null, 2),
698
+ },
699
+ ],
700
+ };
701
+ }
702
+ throw new Error(`Failed to insert documents into collection ${collection.collectionName}: ${error.message}`);
703
+ }
704
+ throw new Error(`Failed to insert documents into collection ${collection.collectionName}: Unknown error`);
705
+ }
706
+ }
707
+ case "createIndex": {
708
+ const { indexes, writeConcern, commitQuorum } = request.params.arguments || {};
709
+ // Validate collection name to prevent access to system collections
710
+ if (collection.collectionName.startsWith("system.")) {
711
+ throw new Error("Access to system collections is not allowed");
712
+ }
713
+ // Validate indexes array
714
+ if (!Array.isArray(indexes) || indexes.length === 0) {
715
+ throw new Error("Indexes must be a non-empty array");
716
+ }
717
+ // Validate writeConcern
718
+ if (writeConcern &&
719
+ (typeof writeConcern !== "object" || Array.isArray(writeConcern))) {
720
+ throw new Error("Write concern must be a valid MongoDB write concern object");
721
+ }
722
+ // Validate commitQuorum
723
+ if (commitQuorum &&
724
+ typeof commitQuorum !== "string" &&
725
+ typeof commitQuorum !== "number") {
726
+ throw new Error("Commit quorum must be a string or number");
727
+ }
728
+ try {
729
+ const result = await collection.createIndexes(indexes, {
730
+ writeConcern,
731
+ commitQuorum: typeof commitQuorum === "number" ? commitQuorum : undefined,
732
+ });
733
+ return {
734
+ content: [
735
+ {
736
+ type: "text",
737
+ text: JSON.stringify({
738
+ acknowledged: result.acknowledged,
739
+ createdIndexes: result.createdIndexes,
740
+ numIndexesBefore: result.numIndexesBefore,
741
+ numIndexesAfter: result.numIndexesAfter,
742
+ }, null, 2),
743
+ },
744
+ ],
745
+ };
746
+ }
747
+ catch (error) {
748
+ if (error instanceof Error) {
749
+ throw new Error(`Failed to create indexes on collection ${collection.collectionName}: ${error.message}`);
750
+ }
751
+ throw new Error(`Failed to create indexes on collection ${collection.collectionName}: Unknown error`);
752
+ }
753
+ }
754
+ case "count": {
755
+ const args = request.params.arguments || {};
756
+ const { query } = args;
757
+ // Validate collection name to prevent access to system collections
758
+ if (collection.collectionName.startsWith("system.")) {
759
+ throw new Error("Access to system collections is not allowed");
760
+ }
761
+ // Validate and parse query
762
+ let countQuery = {};
763
+ if (query) {
764
+ if (typeof query === "string") {
765
+ try {
766
+ countQuery = JSON.parse(query);
767
+ }
768
+ catch (e) {
769
+ throw new Error("Invalid query format: must be a valid JSON object");
770
+ }
771
+ }
772
+ else if (typeof query === "object" &&
773
+ query !== null &&
774
+ !Array.isArray(query)) {
775
+ countQuery = query;
776
+ }
777
+ else {
778
+ throw new Error("Query must be a plain object");
779
+ }
780
+ }
781
+ try {
782
+ const options = {
783
+ limit: typeof args.limit === "number" ? args.limit : undefined,
784
+ skip: typeof args.skip === "number" ? args.skip : undefined,
785
+ hint: typeof args.hint === "object" && args.hint !== null
786
+ ? args.hint
787
+ : undefined,
788
+ readConcern: typeof args.readConcern === "object" && args.readConcern !== null
789
+ ? args.readConcern
790
+ : undefined,
791
+ maxTimeMS: typeof args.maxTimeMS === "number" ? args.maxTimeMS : undefined,
792
+ collation: typeof args.collation === "object" && args.collation !== null
793
+ ? args.collation
794
+ : undefined,
795
+ };
796
+ // Remove undefined options
797
+ Object.keys(options).forEach((key) => options[key] === undefined && delete options[key]);
798
+ // Execute count operation
799
+ const count = await collection.countDocuments(countQuery, options);
800
+ return {
801
+ content: [
802
+ {
803
+ type: "text",
804
+ text: JSON.stringify({
805
+ count: count,
806
+ ok: 1,
807
+ }, null, 2),
808
+ },
809
+ ],
810
+ };
811
+ }
812
+ catch (error) {
813
+ if (error instanceof Error) {
814
+ throw new Error(`Failed to count documents in collection ${collection.collectionName}: ${error.message}`);
815
+ }
816
+ throw new Error(`Failed to count documents in collection ${collection.collectionName}: Unknown error`);
254
817
  }
255
818
  }
256
819
  default:
@@ -271,11 +834,11 @@ server.setRequestHandler(ListPromptsRequestSchema, async () => {
271
834
  {
272
835
  name: "collection",
273
836
  description: "Name of the collection to analyze",
274
- required: true
275
- }
276
- ]
277
- }
278
- ]
837
+ required: true,
838
+ },
839
+ ],
840
+ },
841
+ ],
279
842
  };
280
843
  });
281
844
  /**
@@ -293,22 +856,22 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
293
856
  try {
294
857
  const collection = db.collection(collectionName);
295
858
  // Validate collection name to prevent access to system collections
296
- if (collection.collectionName.startsWith('system.')) {
297
- throw new Error('Access to system collections is not allowed');
859
+ if (collection.collectionName.startsWith("system.")) {
860
+ throw new Error("Access to system collections is not allowed");
298
861
  }
299
862
  const schema = await collection.findOne({});
300
863
  // Get basic collection stats - just count in API v1
301
- const stats = await collection.aggregate([
864
+ const stats = await collection
865
+ .aggregate([
302
866
  {
303
867
  $collStats: {
304
- count: {}
305
- }
306
- }
307
- ]).toArray();
308
- // Also get a sample of documents to show data distribution
309
- const sampleDocs = await collection.find({})
310
- .limit(5)
868
+ count: {},
869
+ },
870
+ },
871
+ ])
311
872
  .toArray();
873
+ // Also get a sample of documents to show data distribution
874
+ const sampleDocs = await collection.find({}).limit(5).toArray();
312
875
  return {
313
876
  messages: [
314
877
  {
@@ -322,20 +885,20 @@ Schema:
322
885
  ${JSON.stringify(schema, null, 2)}
323
886
 
324
887
  Stats:
325
- Document count: ${stats[0]?.count || 'unknown'}
888
+ Document count: ${stats[0]?.count || "unknown"}
326
889
 
327
890
  Sample documents:
328
- ${JSON.stringify(sampleDocs, null, 2)}`
329
- }
891
+ ${JSON.stringify(sampleDocs, null, 2)}`,
892
+ },
330
893
  },
331
894
  {
332
895
  role: "user",
333
896
  content: {
334
897
  type: "text",
335
- text: "Provide insights about the collection's structure, data types, and basic statistics."
336
- }
337
- }
338
- ]
898
+ text: "Provide insights about the collection's structure, data types, and basic statistics.",
899
+ },
900
+ },
901
+ ],
339
902
  };
340
903
  }
341
904
  catch (error) {
@@ -379,9 +942,9 @@ Example queries:
379
942
  3. Find documents with existing email:
380
943
  { "email": { "$exists": true } }
381
944
 
382
- Use these patterns to construct MongoDB queries.`
383
- }
384
- ]
945
+ Use these patterns to construct MongoDB queries.`,
946
+ },
947
+ ],
385
948
  };
386
949
  });
387
950
  /**
package/package.json CHANGED
@@ -1,52 +1,52 @@
1
1
  {
2
- "name": "mcp-mongo-server",
3
- "version": "0.1.3",
4
- "description": "A Model Context Protocol server for MongoDB connections",
5
- "private": false,
6
- "type": "module",
7
- "bin": {
8
- "mongodb": "./build/index.js"
9
- },
10
- "files": [
11
- "build"
12
- ],
13
- "scripts": {
14
- "build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
15
- "prepare": "npm run build",
16
- "watch": "tsc --watch",
17
- "inspector": "npx @modelcontextprotocol/inspector build/index.js"
18
- },
19
- "dependencies": {
20
- "@modelcontextprotocol/sdk": "0.6.0",
21
- "@types/mongodb": "^4.0.6",
22
- "mongodb": "^6.11.0",
23
- "zod": "^3.23.8"
24
- },
25
- "devDependencies": {
26
- "@types/node": "^20.11.24",
27
- "typescript": "^5.3.3"
28
- },
29
- "publishConfig": {
30
- "access": "public"
31
- },
32
- "keywords": [
33
- "claude",
34
- "claude-desktop",
35
- "mcp",
36
- "mongo",
37
- "mongodb",
38
- "model-context-protocol",
39
- "llm",
40
- "mcp-server",
41
- "mcp-protocol",
42
- "mcp-connector",
43
- "mcp-connector-mongodb"
44
- ],
45
- "author": "Muhammed Kilic <kiliczsh>",
46
- "license": "MIT",
47
- "repository": {
48
- "type": "git",
49
- "url": "https://github.com/kiliczsh/mcp-mongo-server"
50
- },
51
- "homepage": "https://github.com/kiliczsh/mcp-mongo-server"
52
- }
2
+ "name": "mcp-mongo-server",
3
+ "version": "1.0.0",
4
+ "description": "A Model Context Protocol server for MongoDB connections",
5
+ "private": false,
6
+ "type": "module",
7
+ "bin": {
8
+ "mongodb": "./build/index.js"
9
+ },
10
+ "files": [
11
+ "build"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
15
+ "prepare": "npm run build",
16
+ "watch": "tsc --watch",
17
+ "inspector": "npx @modelcontextprotocol/inspector build/index.js"
18
+ },
19
+ "dependencies": {
20
+ "@modelcontextprotocol/sdk": "0.6.0",
21
+ "@types/mongodb": "^4.0.6",
22
+ "mongodb": "^6.11.0",
23
+ "zod": "^3.23.8"
24
+ },
25
+ "devDependencies": {
26
+ "@types/node": "^20.11.24",
27
+ "typescript": "^5.3.3"
28
+ },
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
32
+ "keywords": [
33
+ "claude",
34
+ "claude-desktop",
35
+ "mcp",
36
+ "mongo",
37
+ "mongodb",
38
+ "model-context-protocol",
39
+ "llm",
40
+ "mcp-server",
41
+ "mcp-protocol",
42
+ "mcp-connector",
43
+ "mcp-connector-mongodb"
44
+ ],
45
+ "author": "Muhammed Kilic <kiliczsh>",
46
+ "license": "MIT",
47
+ "repository": {
48
+ "type": "git",
49
+ "url": "https://github.com/kiliczsh/mcp-mongo-server"
50
+ },
51
+ "homepage": "https://github.com/kiliczsh/mcp-mongo-server"
52
+ }