mcp-mongo-server 0.2.0 → 1.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.
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  ![NPM License](https://img.shields.io/npm/l/mcp-mongo-server)
6
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
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.2.0",
25
+ version: "1.1.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,254 @@ 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
+ },
199
+ },
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
+ },
171
229
  },
172
- required: ["collection", "pipeline"]
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
+ {
382
+ name: "listCollections",
383
+ description: "List all collections in the MongoDB database",
384
+ inputSchema: {
385
+ type: "object",
386
+ properties: {
387
+ nameOnly: {
388
+ type: "boolean",
389
+ description: "Optional: If true, returns only the collection names instead of full collection info"
390
+ },
391
+ filter: {
392
+ type: "object",
393
+ description: "Optional: Filter to apply to the collections"
394
+ }
395
+ }
173
396
  }
174
- }
175
- ]
397
+ },
398
+ ],
176
399
  };
177
400
  });
178
401
  /**
@@ -183,42 +406,66 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
183
406
  const collection = db.collection(request.params.arguments?.collection);
184
407
  switch (request.params.name) {
185
408
  case "query": {
186
- const { filter, projection, limit } = request.params.arguments || {};
409
+ const { filter, projection, limit, explain } = request.params.arguments || {};
187
410
  // 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');
411
+ if (collection.collectionName.startsWith("system.")) {
412
+ throw new Error("Access to system collections is not allowed");
190
413
  }
191
414
  // Validate and parse filter
192
415
  let queryFilter = {};
193
416
  if (filter) {
194
- if (typeof filter === 'string') {
417
+ if (typeof filter === "string") {
195
418
  try {
196
419
  queryFilter = JSON.parse(filter);
197
420
  }
198
421
  catch (e) {
199
- throw new Error('Invalid filter format: must be a valid JSON object');
422
+ throw new Error("Invalid filter format: must be a valid JSON object");
200
423
  }
201
424
  }
202
- else if (typeof filter === 'object' && filter !== null && !Array.isArray(filter)) {
425
+ else if (typeof filter === "object" &&
426
+ filter !== null &&
427
+ !Array.isArray(filter)) {
203
428
  queryFilter = filter;
204
429
  }
205
430
  else {
206
- throw new Error('Query filter must be a plain object or ObjectId');
431
+ throw new Error("Query filter must be a plain object or ObjectId");
207
432
  }
208
433
  }
209
434
  // Execute the find operation with error handling
210
435
  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
- };
436
+ if (explain) {
437
+ // Use explain for query analysis
438
+ const explainResult = await collection
439
+ .find(queryFilter, {
440
+ projection,
441
+ limit: limit || 100,
442
+ })
443
+ .explain(explain);
444
+ return {
445
+ content: [
446
+ {
447
+ type: "text",
448
+ text: JSON.stringify(explainResult, null, 2),
449
+ },
450
+ ],
451
+ };
452
+ }
453
+ else {
454
+ // Regular query execution
455
+ const cursor = collection.find(queryFilter, {
456
+ projection,
457
+ limit: limit || 100,
458
+ });
459
+ const results = await cursor.toArray();
460
+ return {
461
+ content: [
462
+ {
463
+ type: "text",
464
+ text: JSON.stringify(results, null, 2),
465
+ },
466
+ ],
467
+ };
468
+ }
222
469
  }
223
470
  catch (error) {
224
471
  if (error instanceof Error) {
@@ -228,33 +475,392 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
228
475
  }
229
476
  }
230
477
  case "aggregate": {
231
- const { pipeline } = request.params.arguments || {};
478
+ const { pipeline, explain } = request.params.arguments || {};
232
479
  if (!Array.isArray(pipeline)) {
233
480
  throw new Error("Pipeline must be an array");
234
481
  }
235
482
  // 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');
483
+ if (collection.collectionName.startsWith("system.")) {
484
+ throw new Error("Access to system collections is not allowed");
238
485
  }
239
486
  // Execute the aggregation operation with error handling
240
487
  try {
241
- const results = await collection.aggregate(pipeline).toArray();
488
+ if (explain) {
489
+ // Use explain for aggregation analysis
490
+ const explainResult = await collection
491
+ .aggregate(pipeline, {
492
+ explain: {
493
+ verbosity: explain,
494
+ },
495
+ })
496
+ .toArray();
497
+ return {
498
+ content: [
499
+ {
500
+ type: "text",
501
+ text: JSON.stringify(explainResult, null, 2),
502
+ },
503
+ ],
504
+ };
505
+ }
506
+ else {
507
+ // Regular aggregation execution
508
+ const results = await collection.aggregate(pipeline).toArray();
509
+ return {
510
+ content: [
511
+ {
512
+ type: "text",
513
+ text: JSON.stringify(results, null, 2),
514
+ },
515
+ ],
516
+ };
517
+ }
518
+ }
519
+ catch (error) {
520
+ if (error instanceof Error) {
521
+ throw new Error(`Failed to aggregate collection ${collection.collectionName}: ${error.message}`);
522
+ }
523
+ throw new Error(`Failed to aggregate collection ${collection.collectionName}: Unknown error`);
524
+ }
525
+ }
526
+ case "update": {
527
+ const { filter, update, upsert, multi } = request.params.arguments || {};
528
+ // Validate collection name to prevent access to system collections
529
+ if (collection.collectionName.startsWith("system.")) {
530
+ throw new Error("Access to system collections is not allowed");
531
+ }
532
+ // Validate and parse filter
533
+ let queryFilter = {};
534
+ if (filter) {
535
+ if (typeof filter === "string") {
536
+ try {
537
+ queryFilter = JSON.parse(filter);
538
+ }
539
+ catch (e) {
540
+ throw new Error("Invalid filter format: must be a valid JSON object");
541
+ }
542
+ }
543
+ else if (typeof filter === "object" &&
544
+ filter !== null &&
545
+ !Array.isArray(filter)) {
546
+ queryFilter = filter;
547
+ }
548
+ else {
549
+ throw new Error("Query filter must be a plain object or ObjectId");
550
+ }
551
+ }
552
+ // Validate update operations
553
+ if (!update || typeof update !== "object" || Array.isArray(update)) {
554
+ throw new Error("Update must be a valid MongoDB update document");
555
+ }
556
+ // Check if update operations use valid operators
557
+ const validUpdateOperators = [
558
+ "$set",
559
+ "$unset",
560
+ "$inc",
561
+ "$push",
562
+ "$pull",
563
+ "$addToSet",
564
+ "$pop",
565
+ "$rename",
566
+ "$mul",
567
+ ];
568
+ const hasValidOperator = Object.keys(update).some((key) => validUpdateOperators.includes(key));
569
+ if (!hasValidOperator) {
570
+ throw new Error("Update must include at least one valid update operator ($set, $unset, etc.)");
571
+ }
572
+ try {
573
+ const options = {
574
+ upsert: !!upsert,
575
+ multi: !!multi,
576
+ };
577
+ // Use updateOne or updateMany based on multi option
578
+ const updateMethod = options.multi ? "updateMany" : "updateOne";
579
+ const result = await collection[updateMethod](queryFilter, update, options);
242
580
  return {
243
- content: [{
581
+ content: [
582
+ {
244
583
  type: "text",
245
- text: JSON.stringify(results, null, 2)
246
- }]
584
+ text: JSON.stringify({
585
+ matchedCount: result.matchedCount,
586
+ modifiedCount: result.modifiedCount,
587
+ upsertedCount: result.upsertedCount,
588
+ upsertedId: result.upsertedId,
589
+ }, null, 2),
590
+ },
591
+ ],
247
592
  };
248
593
  }
249
594
  catch (error) {
250
595
  if (error instanceof Error) {
251
- throw new Error(`Failed to aggregate collection ${collection.collectionName}: ${error.message}`);
596
+ throw new Error(`Failed to update collection ${collection.collectionName}: ${error.message}`);
252
597
  }
253
- throw new Error(`Failed to aggregate collection ${collection.collectionName}: Unknown error`);
598
+ throw new Error(`Failed to update collection ${collection.collectionName}: Unknown error`);
599
+ }
600
+ }
601
+ case "serverInfo": {
602
+ const { includeDebugInfo } = request.params.arguments || {};
603
+ try {
604
+ // Get basic server information using buildInfo command
605
+ const buildInfo = await db.command({ buildInfo: 1 });
606
+ // Get additional server status if debug info is requested
607
+ let serverStatus = null;
608
+ if (includeDebugInfo) {
609
+ serverStatus = await db.command({ serverStatus: 1 });
610
+ }
611
+ // Construct the response
612
+ const serverInfo = {
613
+ version: buildInfo.version,
614
+ gitVersion: buildInfo.gitVersion,
615
+ modules: buildInfo.modules,
616
+ allocator: buildInfo.allocator,
617
+ javascriptEngine: buildInfo.javascriptEngine,
618
+ sysInfo: buildInfo.sysInfo,
619
+ storageEngines: buildInfo.storageEngines,
620
+ debug: buildInfo.debug,
621
+ maxBsonObjectSize: buildInfo.maxBsonObjectSize,
622
+ openssl: buildInfo.openssl,
623
+ buildEnvironment: buildInfo.buildEnvironment,
624
+ bits: buildInfo.bits,
625
+ ok: buildInfo.ok,
626
+ status: {},
627
+ };
628
+ // Add server status information if requested
629
+ if (serverStatus) {
630
+ serverInfo.status = {
631
+ host: serverStatus.host,
632
+ version: serverStatus.version,
633
+ process: serverStatus.process,
634
+ pid: serverStatus.pid,
635
+ uptime: serverStatus.uptime,
636
+ uptimeMillis: serverStatus.uptimeMillis,
637
+ uptimeEstimate: serverStatus.uptimeEstimate,
638
+ localTime: serverStatus.localTime,
639
+ connections: serverStatus.connections,
640
+ network: serverStatus.network,
641
+ memory: serverStatus.mem,
642
+ storageEngine: serverStatus.storageEngine,
643
+ security: serverStatus.security,
644
+ };
645
+ }
646
+ return {
647
+ content: [
648
+ {
649
+ type: "text",
650
+ text: JSON.stringify(serverInfo, null, 2),
651
+ },
652
+ ],
653
+ };
654
+ }
655
+ catch (error) {
656
+ if (error instanceof Error) {
657
+ throw new Error(`Failed to get server information: ${error.message}`);
658
+ }
659
+ throw new Error("Failed to get server information: Unknown error");
660
+ }
661
+ }
662
+ case "insert": {
663
+ const { documents, ordered, writeConcern, bypassDocumentValidation } = request.params.arguments || {};
664
+ // Validate collection name to prevent access to system collections
665
+ if (collection.collectionName.startsWith("system.")) {
666
+ throw new Error("Access to system collections is not allowed");
667
+ }
668
+ // Validate documents array
669
+ if (!Array.isArray(documents)) {
670
+ throw new Error("Documents must be an array");
671
+ }
672
+ if (documents.length === 0) {
673
+ throw new Error("Documents array cannot be empty");
674
+ }
675
+ if (!documents.every((doc) => doc && typeof doc === "object" && !Array.isArray(doc))) {
676
+ throw new Error("Each document must be a valid MongoDB document object");
677
+ }
678
+ try {
679
+ // Prepare insert options
680
+ const options = {
681
+ ordered: ordered !== false, // default to true if not specified
682
+ writeConcern,
683
+ bypassDocumentValidation,
684
+ };
685
+ // Use insertMany for consistency, it works for single documents too
686
+ const result = await collection.insertMany(documents, options);
687
+ return {
688
+ content: [
689
+ {
690
+ type: "text",
691
+ text: JSON.stringify({
692
+ acknowledged: result.acknowledged,
693
+ insertedCount: result.insertedCount,
694
+ insertedIds: result.insertedIds,
695
+ }, null, 2),
696
+ },
697
+ ],
698
+ };
699
+ }
700
+ catch (error) {
701
+ if (error instanceof Error) {
702
+ // Handle bulk write errors specially to provide more detail
703
+ if (error.name === "BulkWriteError") {
704
+ const bulkError = error;
705
+ return {
706
+ content: [
707
+ {
708
+ type: "text",
709
+ text: JSON.stringify({
710
+ error: "Bulk write error occurred",
711
+ writeErrors: bulkError.writeErrors,
712
+ insertedCount: bulkError.result?.nInserted || 0,
713
+ failedCount: bulkError.result?.nFailedInserts || 0,
714
+ }, null, 2),
715
+ },
716
+ ],
717
+ };
718
+ }
719
+ throw new Error(`Failed to insert documents into collection ${collection.collectionName}: ${error.message}`);
720
+ }
721
+ throw new Error(`Failed to insert documents into collection ${collection.collectionName}: Unknown error`);
722
+ }
723
+ }
724
+ case "createIndex": {
725
+ const { indexes, writeConcern, commitQuorum } = request.params.arguments || {};
726
+ // Validate collection name to prevent access to system collections
727
+ if (collection.collectionName.startsWith("system.")) {
728
+ throw new Error("Access to system collections is not allowed");
729
+ }
730
+ // Validate indexes array
731
+ if (!Array.isArray(indexes) || indexes.length === 0) {
732
+ throw new Error("Indexes must be a non-empty array");
733
+ }
734
+ // Validate writeConcern
735
+ if (writeConcern &&
736
+ (typeof writeConcern !== "object" || Array.isArray(writeConcern))) {
737
+ throw new Error("Write concern must be a valid MongoDB write concern object");
738
+ }
739
+ // Validate commitQuorum
740
+ if (commitQuorum &&
741
+ typeof commitQuorum !== "string" &&
742
+ typeof commitQuorum !== "number") {
743
+ throw new Error("Commit quorum must be a string or number");
744
+ }
745
+ try {
746
+ const result = await collection.createIndexes(indexes, {
747
+ writeConcern,
748
+ commitQuorum: typeof commitQuorum === "number" ? commitQuorum : undefined,
749
+ });
750
+ return {
751
+ content: [
752
+ {
753
+ type: "text",
754
+ text: JSON.stringify({
755
+ acknowledged: result.acknowledged,
756
+ createdIndexes: result.createdIndexes,
757
+ numIndexesBefore: result.numIndexesBefore,
758
+ numIndexesAfter: result.numIndexesAfter,
759
+ }, null, 2),
760
+ },
761
+ ],
762
+ };
763
+ }
764
+ catch (error) {
765
+ if (error instanceof Error) {
766
+ throw new Error(`Failed to create indexes on collection ${collection.collectionName}: ${error.message}`);
767
+ }
768
+ throw new Error(`Failed to create indexes on collection ${collection.collectionName}: Unknown error`);
769
+ }
770
+ }
771
+ case "count": {
772
+ const args = request.params.arguments || {};
773
+ const { query } = args;
774
+ // Validate collection name to prevent access to system collections
775
+ if (collection.collectionName.startsWith("system.")) {
776
+ throw new Error("Access to system collections is not allowed");
777
+ }
778
+ // Validate and parse query
779
+ let countQuery = {};
780
+ if (query) {
781
+ if (typeof query === "string") {
782
+ try {
783
+ countQuery = JSON.parse(query);
784
+ }
785
+ catch (e) {
786
+ throw new Error("Invalid query format: must be a valid JSON object");
787
+ }
788
+ }
789
+ else if (typeof query === "object" &&
790
+ query !== null &&
791
+ !Array.isArray(query)) {
792
+ countQuery = query;
793
+ }
794
+ else {
795
+ throw new Error("Query must be a plain object");
796
+ }
797
+ }
798
+ try {
799
+ const options = {
800
+ limit: typeof args.limit === "number" ? args.limit : undefined,
801
+ skip: typeof args.skip === "number" ? args.skip : undefined,
802
+ hint: typeof args.hint === "object" && args.hint !== null
803
+ ? args.hint
804
+ : undefined,
805
+ readConcern: typeof args.readConcern === "object" && args.readConcern !== null
806
+ ? args.readConcern
807
+ : undefined,
808
+ maxTimeMS: typeof args.maxTimeMS === "number" ? args.maxTimeMS : undefined,
809
+ collation: typeof args.collation === "object" && args.collation !== null
810
+ ? args.collation
811
+ : undefined,
812
+ };
813
+ // Remove undefined options
814
+ Object.keys(options).forEach((key) => options[key] === undefined && delete options[key]);
815
+ // Execute count operation
816
+ const count = await collection.countDocuments(countQuery, options);
817
+ return {
818
+ content: [
819
+ {
820
+ type: "text",
821
+ text: JSON.stringify({
822
+ count: count,
823
+ ok: 1,
824
+ }, null, 2),
825
+ },
826
+ ],
827
+ };
828
+ }
829
+ catch (error) {
830
+ if (error instanceof Error) {
831
+ throw new Error(`Failed to count documents in collection ${collection.collectionName}: ${error.message}`);
832
+ }
833
+ throw new Error(`Failed to count documents in collection ${collection.collectionName}: Unknown error`);
834
+ }
835
+ }
836
+ case "listCollections": {
837
+ const { nameOnly, filter } = request.params.arguments || {};
838
+ try {
839
+ // Get the list of collections
840
+ const options = filter ? { filter } : {};
841
+ const collections = await db.listCollections(options).toArray();
842
+ // If nameOnly is true, return only the collection names
843
+ const result = nameOnly
844
+ ? collections.map((collection) => collection.name)
845
+ : collections;
846
+ return {
847
+ content: [
848
+ {
849
+ type: "text",
850
+ text: JSON.stringify(result, null, 2),
851
+ },
852
+ ],
853
+ };
854
+ }
855
+ catch (error) {
856
+ if (error instanceof Error) {
857
+ throw new Error(`Failed to list collections: ${error.message}`);
858
+ }
859
+ throw new Error("Failed to list collections: Unknown error");
254
860
  }
255
861
  }
256
862
  default:
257
- throw new Error("Unknown tool");
863
+ throw new Error(`Unknown tool: ${request.params.name}`);
258
864
  }
259
865
  });
260
866
  /**
@@ -271,11 +877,11 @@ server.setRequestHandler(ListPromptsRequestSchema, async () => {
271
877
  {
272
878
  name: "collection",
273
879
  description: "Name of the collection to analyze",
274
- required: true
275
- }
276
- ]
277
- }
278
- ]
880
+ required: true,
881
+ },
882
+ ],
883
+ },
884
+ ],
279
885
  };
280
886
  });
281
887
  /**
@@ -293,22 +899,22 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
293
899
  try {
294
900
  const collection = db.collection(collectionName);
295
901
  // 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');
902
+ if (collection.collectionName.startsWith("system.")) {
903
+ throw new Error("Access to system collections is not allowed");
298
904
  }
299
905
  const schema = await collection.findOne({});
300
906
  // Get basic collection stats - just count in API v1
301
- const stats = await collection.aggregate([
907
+ const stats = await collection
908
+ .aggregate([
302
909
  {
303
910
  $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)
911
+ count: {},
912
+ },
913
+ },
914
+ ])
311
915
  .toArray();
916
+ // Also get a sample of documents to show data distribution
917
+ const sampleDocs = await collection.find({}).limit(5).toArray();
312
918
  return {
313
919
  messages: [
314
920
  {
@@ -322,20 +928,20 @@ Schema:
322
928
  ${JSON.stringify(schema, null, 2)}
323
929
 
324
930
  Stats:
325
- Document count: ${stats[0]?.count || 'unknown'}
931
+ Document count: ${stats[0]?.count || "unknown"}
326
932
 
327
933
  Sample documents:
328
- ${JSON.stringify(sampleDocs, null, 2)}`
329
- }
934
+ ${JSON.stringify(sampleDocs, null, 2)}`,
935
+ },
330
936
  },
331
937
  {
332
938
  role: "user",
333
939
  content: {
334
940
  type: "text",
335
- text: "Provide insights about the collection's structure, data types, and basic statistics."
336
- }
337
- }
338
- ]
941
+ text: "Provide insights about the collection's structure, data types, and basic statistics.",
942
+ },
943
+ },
944
+ ],
339
945
  };
340
946
  }
341
947
  catch (error) {
@@ -379,9 +985,9 @@ Example queries:
379
985
  3. Find documents with existing email:
380
986
  { "email": { "$exists": true } }
381
987
 
382
- Use these patterns to construct MongoDB queries.`
383
- }
384
- ]
988
+ Use these patterns to construct MongoDB queries.`,
989
+ },
990
+ ],
385
991
  };
386
992
  });
387
993
  /**
package/build/types.js CHANGED
@@ -4,11 +4,13 @@ import { CallToolRequestSchema, ToolSchema, ResourceSchema, TextResourceContents
4
4
  export const MongoCollectionSchema = ResourceSchema.extend({
5
5
  collectionName: z.string(),
6
6
  databaseName: z.string(),
7
- indexes: z.array(z.object({
7
+ indexes: z
8
+ .array(z.object({
8
9
  name: z.string(),
9
10
  keys: z.record(z.union([z.number(), z.string()])),
10
11
  unique: z.optional(z.boolean()),
11
- })).optional(),
12
+ }))
13
+ .optional(),
12
14
  });
13
15
  // MongoDB Document Schema
14
16
  export const MongoDocumentSchema = TextResourceContentsSchema.extend({
@@ -40,14 +42,16 @@ export const MongoSortSchema = z.record(z.union([z.literal(1), z.literal(-1)]));
40
42
  export const MongoQueryToolSchema = ToolSchema.extend({
41
43
  inputSchema: z.object({
42
44
  type: z.literal("object"),
43
- properties: z.object({
45
+ properties: z
46
+ .object({
44
47
  collection: z.string(),
45
48
  filter: z.record(z.union([z.unknown(), MongoQueryOperatorSchema])),
46
49
  projection: z.record(z.union([z.literal(0), z.literal(1)])).optional(),
47
50
  sort: MongoSortSchema.optional(),
48
51
  limit: z.number().int().positive().optional(),
49
52
  skip: z.number().int().nonnegative().optional(),
50
- }).passthrough(),
53
+ })
54
+ .passthrough(),
51
55
  required: z.array(z.literal("collection")),
52
56
  }),
53
57
  });
@@ -55,10 +59,12 @@ export const MongoQueryToolSchema = ToolSchema.extend({
55
59
  export const MongoAggregateToolSchema = ToolSchema.extend({
56
60
  inputSchema: z.object({
57
61
  type: z.literal("object"),
58
- properties: z.object({
62
+ properties: z
63
+ .object({
59
64
  collection: z.string(),
60
65
  pipeline: z.array(z.record(z.unknown())),
61
- }).passthrough(),
66
+ })
67
+ .passthrough(),
62
68
  required: z.array(z.literal("collection")),
63
69
  }),
64
70
  });
@@ -66,10 +72,14 @@ export const MongoAggregateToolSchema = ToolSchema.extend({
66
72
  export const MongoCountToolSchema = ToolSchema.extend({
67
73
  inputSchema: z.object({
68
74
  type: z.literal("object"),
69
- properties: z.object({
75
+ properties: z
76
+ .object({
70
77
  collection: z.string(),
71
- filter: z.record(z.union([z.unknown(), MongoQueryOperatorSchema])).optional(),
72
- }).passthrough(),
78
+ filter: z
79
+ .record(z.union([z.unknown(), MongoQueryOperatorSchema]))
80
+ .optional(),
81
+ })
82
+ .passthrough(),
73
83
  required: z.array(z.literal("collection")),
74
84
  }),
75
85
  });
@@ -77,18 +87,32 @@ export const MongoCountToolSchema = ToolSchema.extend({
77
87
  export const MongoDistinctToolSchema = ToolSchema.extend({
78
88
  inputSchema: z.object({
79
89
  type: z.literal("object"),
80
- properties: z.object({
90
+ properties: z
91
+ .object({
81
92
  collection: z.string(),
82
93
  field: z.string(),
83
- filter: z.record(z.union([z.unknown(), MongoQueryOperatorSchema])).optional(),
84
- }).passthrough(),
94
+ filter: z
95
+ .record(z.union([z.unknown(), MongoQueryOperatorSchema]))
96
+ .optional(),
97
+ })
98
+ .passthrough(),
85
99
  required: z.array(z.literal("collection")),
86
100
  }),
87
101
  });
88
102
  // MongoDB Call Tool Request Schema
89
103
  export const MongoCallToolRequestSchema = CallToolRequestSchema.extend({
90
104
  params: z.object({
91
- name: z.enum(["query", "aggregate", "count", "distinct"]),
105
+ name: z.enum([
106
+ "query",
107
+ "aggregate",
108
+ "update",
109
+ "serverInfo",
110
+ "insert",
111
+ "createIndex",
112
+ "count",
113
+ "distinct",
114
+ "listCollections",
115
+ ]),
92
116
  arguments: z.union([
93
117
  MongoQueryToolSchema.shape.inputSchema,
94
118
  MongoAggregateToolSchema.shape.inputSchema,
@@ -136,8 +160,10 @@ export const MongoFieldSchemaSchema = z.object({
136
160
  export const MongoCollectionSchemaSchema = z.object({
137
161
  name: z.string(),
138
162
  fields: z.record(MongoFieldSchemaSchema),
139
- options: z.object({
163
+ options: z
164
+ .object({
140
165
  timestamps: z.boolean().optional(),
141
166
  strict: z.boolean().optional(),
142
- }).optional(),
167
+ })
168
+ .optional(),
143
169
  });
package/package.json CHANGED
@@ -1,52 +1,52 @@
1
1
  {
2
- "name": "mcp-mongo-server",
3
- "version": "0.2.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
- }
2
+ "name": "mcp-mongo-server",
3
+ "version": "1.1.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
+ }