mcp-mongo-server 1.1.2 → 1.3.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/build/index.js CHANGED
@@ -1,1023 +1,18 @@
1
1
  #!/usr/bin/env node
2
- /**
3
- * This is an MCP server that implements a MongoDB interface.
4
- * It demonstrates core MCP concepts by allowing:
5
- * - Listing collections as resources
6
- * - Reading collection schemas and contents
7
- * - Executing MongoDB queries via tools
8
- * - Providing collection summaries via prompts
9
- */
10
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
11
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
12
- import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, ListResourceTemplatesRequestSchema, PingRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
13
- import { MongoClient, ReadPreference } from "mongodb";
14
- /**
15
- * MongoDB connection client and database reference
16
- */
17
- let client = null;
18
- let db = null;
19
- /**
20
- * Flag indicating whether the connection is in read-only mode
21
- */
22
- let isReadOnlyMode = false;
23
- /**
24
- * Create an MCP server with capabilities for resources (to list/read collections),
25
- * tools (to query data), and prompts (to analyze collections).
26
- */
27
- const server = new Server({
28
- name: "mongodb",
29
- version: "1.1.2",
30
- }, {
31
- capabilities: {
32
- resources: {},
33
- tools: {},
34
- prompts: {},
35
- },
36
- });
37
- /**
38
- * Initialize MongoDB connection
39
- */
40
- async function connectToMongoDB(url, readOnly = false) {
41
- try {
42
- const options = readOnly
43
- ? { readPreference: ReadPreference.SECONDARY }
44
- : {};
45
- client = new MongoClient(url, options);
46
- await client.connect();
47
- db = client.db();
48
- isReadOnlyMode = readOnly;
49
- return true;
50
- }
51
- catch (error) {
52
- console.error("Failed to connect to MongoDB:", error);
53
- return false;
54
- }
55
- }
56
- /**
57
- * Handler for ping requests to check server health
58
- */
59
- server.setRequestHandler(PingRequestSchema, async () => {
60
- try {
61
- // Check MongoDB connection
62
- if (!client) {
63
- throw new Error("MongoDB connection is not available");
64
- }
65
- // Ping MongoDB to verify connection
66
- await db.command({ ping: 1 });
67
- return {
68
- readOnlyMode: isReadOnlyMode,
69
- };
70
- }
71
- catch (error) {
72
- if (error instanceof Error) {
73
- throw new Error(`MongoDB ping failed: ${error.message}`);
74
- }
75
- throw new Error("MongoDB ping failed: Unknown error");
76
- }
77
- });
78
- /**
79
- * Handler for listing available collections as resources.
80
- * Each collection is exposed as a resource with:
81
- * - A mongodb:// URI scheme
82
- * - JSON MIME type
83
- * - Collection name and description
84
- */
85
- server.setRequestHandler(ListResourcesRequestSchema, async () => {
86
- try {
87
- const collections = await db.listCollections().toArray();
88
- return {
89
- resources: collections.map((collection) => ({
90
- uri: `mongodb:///${collection.name}`,
91
- mimeType: "application/json",
92
- name: collection.name,
93
- description: `MongoDB collection: ${collection.name}`,
94
- })),
95
- };
96
- }
97
- catch (error) {
98
- if (error instanceof Error) {
99
- throw new Error(`Failed to list collections: ${error.message}`);
100
- }
101
- throw new Error("Failed to list collections: Unknown error");
102
- }
103
- });
104
- /**
105
- * Handler for reading a collection's schema or contents.
106
- * Takes a mongodb:// URI and returns the collection info as JSON.
107
- */
108
- server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
109
- const url = new URL(request.params.uri);
110
- const collectionName = url.pathname.replace(/^\//, "");
111
- try {
112
- const collection = db.collection(collectionName);
113
- const sample = await collection.findOne({});
114
- const indexes = await collection.indexes();
115
- // Infer schema from sample document
116
- const schema = sample
117
- ? {
118
- type: "collection",
119
- name: collectionName,
120
- fields: Object.entries(sample).map(([key, value]) => ({
121
- name: key,
122
- type: typeof value,
123
- })),
124
- indexes: indexes.map((idx) => ({
125
- name: idx.name,
126
- keys: idx.key,
127
- })),
128
- }
129
- : {
130
- type: "collection",
131
- name: collectionName,
132
- fields: [],
133
- indexes: [],
134
- };
135
- return {
136
- contents: [
137
- {
138
- uri: request.params.uri,
139
- mimeType: "application/json",
140
- text: JSON.stringify(schema, null, 2),
141
- },
142
- ],
143
- };
144
- }
145
- catch (error) {
146
- if (error instanceof Error) {
147
- throw new Error(`Failed to read collection ${collectionName}: ${error.message}`);
148
- }
149
- throw new Error(`Failed to read collection ${collectionName}: Unknown error`);
150
- }
151
- });
152
- /**
153
- * Handler that lists available tools.
154
- * Exposes MongoDB query tools for interacting with collections.
155
- */
156
- server.setRequestHandler(ListToolsRequestSchema, async () => {
157
- return {
158
- tools: [
159
- {
160
- name: "query",
161
- description: "Execute a MongoDB query with optional execution plan analysis",
162
- inputSchema: {
163
- type: "object",
164
- properties: {
165
- collection: {
166
- type: "string",
167
- description: "Name of the collection to query",
168
- },
169
- filter: {
170
- type: "object",
171
- description: "MongoDB query filter",
172
- },
173
- projection: {
174
- type: "object",
175
- description: "Fields to include/exclude",
176
- },
177
- limit: {
178
- type: "number",
179
- description: "Maximum number of documents to return",
180
- },
181
- explain: {
182
- type: "string",
183
- description: "Optional: Get query execution information (queryPlanner, executionStats, or allPlansExecution)",
184
- enum: ["queryPlanner", "executionStats", "allPlansExecution"],
185
- },
186
- },
187
- required: ["collection"],
188
- },
189
- },
190
- {
191
- name: "aggregate",
192
- description: "Execute a MongoDB aggregation pipeline with optional execution plan analysis",
193
- inputSchema: {
194
- type: "object",
195
- properties: {
196
- collection: {
197
- type: "string",
198
- description: "Name of the collection to aggregate",
199
- },
200
- pipeline: {
201
- type: "array",
202
- description: "Aggregation pipeline stages",
203
- },
204
- explain: {
205
- type: "string",
206
- description: "Optional: Get aggregation execution information (queryPlanner, executionStats, or allPlansExecution)",
207
- enum: ["queryPlanner", "executionStats", "allPlansExecution"],
208
- },
209
- },
210
- required: ["collection", "pipeline"],
211
- },
212
- },
213
- {
214
- name: "update",
215
- description: "Update documents in a MongoDB collection",
216
- inputSchema: {
217
- type: "object",
218
- properties: {
219
- collection: {
220
- type: "string",
221
- description: "Name of the collection to update",
222
- },
223
- filter: {
224
- type: "object",
225
- description: "Filter to select documents to update",
226
- },
227
- update: {
228
- type: "object",
229
- description: "Update operations to apply ($set, $unset, $inc, etc.)",
230
- },
231
- upsert: {
232
- type: "boolean",
233
- description: "Create a new document if no documents match the filter",
234
- },
235
- multi: {
236
- type: "boolean",
237
- description: "Update multiple documents that match the filter",
238
- },
239
- },
240
- required: ["collection", "filter", "update"],
241
- },
242
- },
243
- {
244
- name: "serverInfo",
245
- description: "Get MongoDB server information including version, storage engine, and other details",
246
- inputSchema: {
247
- type: "object",
248
- properties: {
249
- includeDebugInfo: {
250
- type: "boolean",
251
- description: "Include additional debug information about the server",
252
- },
253
- },
254
- },
255
- },
256
- {
257
- name: "insert",
258
- description: "Insert one or more documents into a MongoDB collection",
259
- inputSchema: {
260
- type: "object",
261
- properties: {
262
- collection: {
263
- type: "string",
264
- description: "Name of the collection to insert into",
265
- },
266
- documents: {
267
- type: "array",
268
- description: "Array of documents to insert",
269
- items: {
270
- type: "object",
271
- },
272
- },
273
- ordered: {
274
- type: "boolean",
275
- description: "Optional: If true, perform an ordered insert of the documents. If false, perform an unordered insert",
276
- },
277
- writeConcern: {
278
- type: "object",
279
- description: "Optional: Write concern for the insert operation",
280
- },
281
- bypassDocumentValidation: {
282
- type: "boolean",
283
- description: "Optional: Allow insert to bypass schema validation",
284
- },
285
- },
286
- required: ["collection", "documents"],
287
- },
288
- },
289
- {
290
- name: "createIndex",
291
- description: "Create one or more indexes on a MongoDB collection",
292
- inputSchema: {
293
- type: "object",
294
- properties: {
295
- collection: {
296
- type: "string",
297
- description: "Name of the collection to create indexes on",
298
- },
299
- indexes: {
300
- type: "array",
301
- description: "Array of index specifications",
302
- items: {
303
- type: "object",
304
- properties: {
305
- key: {
306
- type: "object",
307
- description: "Index key pattern, e.g. { field: 1 } for ascending, { field: -1 } for descending",
308
- },
309
- name: {
310
- type: "string",
311
- description: "Optional: Name of the index",
312
- },
313
- unique: {
314
- type: "boolean",
315
- description: "Optional: If true, creates a unique index",
316
- },
317
- sparse: {
318
- type: "boolean",
319
- description: "Optional: If true, creates a sparse index",
320
- },
321
- background: {
322
- type: "boolean",
323
- description: "Optional: If true, creates the index in the background",
324
- },
325
- expireAfterSeconds: {
326
- type: "number",
327
- description: "Optional: Specifies the TTL for documents (time to live)",
328
- },
329
- partialFilterExpression: {
330
- type: "object",
331
- description: "Optional: Filter expression for partial indexes",
332
- },
333
- },
334
- required: ["key"],
335
- },
336
- },
337
- writeConcern: {
338
- type: "object",
339
- description: "Optional: Write concern for the index creation",
340
- },
341
- commitQuorum: {
342
- type: ["string", "number"],
343
- description: "Optional: Number of voting members required to create index",
344
- },
345
- },
346
- required: ["collection", "indexes"],
347
- },
348
- },
349
- {
350
- name: "count",
351
- description: "Count the number of documents in a collection that match a query",
352
- inputSchema: {
353
- type: "object",
354
- properties: {
355
- collection: {
356
- type: "string",
357
- description: "Name of the collection to count documents in",
358
- },
359
- query: {
360
- type: "object",
361
- description: "Optional: Query filter to select documents to count",
362
- },
363
- limit: {
364
- type: "integer",
365
- description: "Optional: Maximum number of documents to count",
366
- },
367
- skip: {
368
- type: "integer",
369
- description: "Optional: Number of documents to skip before counting",
370
- },
371
- hint: {
372
- type: "object",
373
- description: "Optional: Index hint to force query plan",
374
- },
375
- readConcern: {
376
- type: "object",
377
- description: "Optional: Read concern for the count operation",
378
- },
379
- maxTimeMS: {
380
- type: "integer",
381
- description: "Optional: Maximum time to allow the count to run",
382
- },
383
- collation: {
384
- type: "object",
385
- description: "Optional: Collation rules for string comparison",
386
- },
387
- },
388
- required: ["collection"],
389
- },
390
- },
391
- {
392
- name: "listCollections",
393
- description: "List all collections in the MongoDB database",
394
- inputSchema: {
395
- type: "object",
396
- properties: {
397
- nameOnly: {
398
- type: "boolean",
399
- description: "Optional: If true, returns only the collection names instead of full collection info",
400
- },
401
- filter: {
402
- type: "object",
403
- description: "Optional: Filter to apply to the collections",
404
- },
405
- },
406
- },
407
- },
408
- ],
409
- };
410
- });
411
- /**
412
- * Handler for MongoDB tools.
413
- * Executes queries and returns results.
414
- */
415
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
416
- const collection = db.collection(request.params.arguments?.collection);
417
- // Define write operations that should be blocked in read-only mode
418
- const writeOperations = ["update", "insert", "createIndex"];
419
- // Check if the operation is a write operation and we're in read-only mode
420
- if (isReadOnlyMode && writeOperations.includes(request.params.name)) {
421
- throw new Error(`ReadonlyError: Operation '${request.params.name}' is not allowed in read-only mode`);
422
- }
423
- switch (request.params.name) {
424
- case "query": {
425
- const { filter, projection, limit, explain } = request.params.arguments || {};
426
- // Validate collection name to prevent access to system collections
427
- if (collection.collectionName.startsWith("system.")) {
428
- throw new Error("Access to system collections is not allowed");
429
- }
430
- // Validate and parse filter
431
- let queryFilter = {};
432
- if (filter) {
433
- if (typeof filter === "string") {
434
- try {
435
- queryFilter = JSON.parse(filter);
436
- }
437
- catch (e) {
438
- throw new Error("Invalid filter format: must be a valid JSON object");
439
- }
440
- }
441
- else if (typeof filter === "object" &&
442
- filter !== null &&
443
- !Array.isArray(filter)) {
444
- queryFilter = filter;
445
- }
446
- else {
447
- throw new Error("Query filter must be a plain object or ObjectId");
448
- }
449
- }
450
- // Execute the find operation with error handling
451
- try {
452
- if (explain) {
453
- // Use explain for query analysis
454
- const explainResult = await collection
455
- .find(queryFilter, {
456
- projection,
457
- limit: limit || 100,
458
- })
459
- .explain(explain);
460
- return {
461
- content: [
462
- {
463
- type: "text",
464
- text: JSON.stringify(explainResult, null, 2),
465
- },
466
- ],
467
- };
468
- }
469
- else {
470
- // Regular query execution
471
- const cursor = collection.find(queryFilter, {
472
- projection,
473
- limit: limit || 100,
474
- });
475
- const results = await cursor.toArray();
476
- return {
477
- content: [
478
- {
479
- type: "text",
480
- text: JSON.stringify(results, null, 2),
481
- },
482
- ],
483
- };
484
- }
485
- }
486
- catch (error) {
487
- if (error instanceof Error) {
488
- throw new Error(`Failed to query collection ${collection.collectionName}: ${error.message}`);
489
- }
490
- throw new Error(`Failed to query collection ${collection.collectionName}: Unknown error`);
491
- }
492
- }
493
- case "aggregate": {
494
- const { pipeline, explain } = request.params.arguments || {};
495
- if (!Array.isArray(pipeline)) {
496
- throw new Error("Pipeline must be an array");
497
- }
498
- // Validate collection name to prevent access to system collections
499
- if (collection.collectionName.startsWith("system.")) {
500
- throw new Error("Access to system collections is not allowed");
501
- }
502
- // Execute the aggregation operation with error handling
503
- try {
504
- if (explain) {
505
- // Use explain for aggregation analysis
506
- const explainResult = await collection
507
- .aggregate(pipeline, {
508
- explain: {
509
- verbosity: explain,
510
- },
511
- })
512
- .toArray();
513
- return {
514
- content: [
515
- {
516
- type: "text",
517
- text: JSON.stringify(explainResult, null, 2),
518
- },
519
- ],
520
- };
521
- }
522
- else {
523
- // Regular aggregation execution
524
- const results = await collection.aggregate(pipeline).toArray();
525
- return {
526
- content: [
527
- {
528
- type: "text",
529
- text: JSON.stringify(results, null, 2),
530
- },
531
- ],
532
- };
533
- }
534
- }
535
- catch (error) {
536
- if (error instanceof Error) {
537
- throw new Error(`Failed to aggregate collection ${collection.collectionName}: ${error.message}`);
538
- }
539
- throw new Error(`Failed to aggregate collection ${collection.collectionName}: Unknown error`);
540
- }
541
- }
542
- case "update": {
543
- const { filter, update, upsert, multi } = request.params.arguments || {};
544
- // Validate collection name to prevent access to system collections
545
- if (collection.collectionName.startsWith("system.")) {
546
- throw new Error("Access to system collections is not allowed");
547
- }
548
- // Validate and parse filter
549
- let queryFilter = {};
550
- if (filter) {
551
- if (typeof filter === "string") {
552
- try {
553
- queryFilter = JSON.parse(filter);
554
- }
555
- catch (e) {
556
- throw new Error("Invalid filter format: must be a valid JSON object");
557
- }
558
- }
559
- else if (typeof filter === "object" &&
560
- filter !== null &&
561
- !Array.isArray(filter)) {
562
- queryFilter = filter;
563
- }
564
- else {
565
- throw new Error("Query filter must be a plain object or ObjectId");
566
- }
567
- }
568
- // Validate update operations
569
- if (!update || typeof update !== "object" || Array.isArray(update)) {
570
- throw new Error("Update must be a valid MongoDB update document");
571
- }
572
- // Check if update operations use valid operators
573
- const validUpdateOperators = [
574
- "$set",
575
- "$unset",
576
- "$inc",
577
- "$push",
578
- "$pull",
579
- "$addToSet",
580
- "$pop",
581
- "$rename",
582
- "$mul",
583
- ];
584
- const hasValidOperator = Object.keys(update).some((key) => validUpdateOperators.includes(key));
585
- if (!hasValidOperator) {
586
- throw new Error("Update must include at least one valid update operator ($set, $unset, etc.)");
587
- }
588
- try {
589
- const options = {
590
- upsert: !!upsert,
591
- multi: !!multi,
592
- };
593
- // Use updateOne or updateMany based on multi option
594
- const updateMethod = options.multi ? "updateMany" : "updateOne";
595
- const result = await collection[updateMethod](queryFilter, update, options);
596
- return {
597
- content: [
598
- {
599
- type: "text",
600
- text: JSON.stringify({
601
- matchedCount: result.matchedCount,
602
- modifiedCount: result.modifiedCount,
603
- upsertedCount: result.upsertedCount,
604
- upsertedId: result.upsertedId,
605
- }, null, 2),
606
- },
607
- ],
608
- };
609
- }
610
- catch (error) {
611
- if (error instanceof Error) {
612
- throw new Error(`Failed to update collection ${collection.collectionName}: ${error.message}`);
613
- }
614
- throw new Error(`Failed to update collection ${collection.collectionName}: Unknown error`);
615
- }
616
- }
617
- case "serverInfo": {
618
- const { includeDebugInfo } = request.params.arguments || {};
619
- try {
620
- // Get basic server information using buildInfo command
621
- const buildInfo = await db.command({ buildInfo: 1 });
622
- // Get additional server status if debug info is requested
623
- let serverStatus = null;
624
- if (includeDebugInfo) {
625
- serverStatus = await db.command({ serverStatus: 1 });
626
- }
627
- // Construct the response
628
- const serverInfo = {
629
- version: buildInfo.version,
630
- gitVersion: buildInfo.gitVersion,
631
- modules: buildInfo.modules,
632
- allocator: buildInfo.allocator,
633
- javascriptEngine: buildInfo.javascriptEngine,
634
- sysInfo: buildInfo.sysInfo,
635
- storageEngines: buildInfo.storageEngines,
636
- debug: buildInfo.debug,
637
- maxBsonObjectSize: buildInfo.maxBsonObjectSize,
638
- openssl: buildInfo.openssl,
639
- buildEnvironment: buildInfo.buildEnvironment,
640
- bits: buildInfo.bits,
641
- ok: buildInfo.ok,
642
- status: {},
643
- connectionInfo: {
644
- readOnlyMode: isReadOnlyMode,
645
- readPreference: isReadOnlyMode ? "secondary" : "primary",
646
- },
647
- };
648
- // Add server status information if requested
649
- if (serverStatus) {
650
- serverInfo.status = {
651
- host: serverStatus.host,
652
- version: serverStatus.version,
653
- process: serverStatus.process,
654
- pid: serverStatus.pid,
655
- uptime: serverStatus.uptime,
656
- uptimeMillis: serverStatus.uptimeMillis,
657
- uptimeEstimate: serverStatus.uptimeEstimate,
658
- localTime: serverStatus.localTime,
659
- connections: serverStatus.connections,
660
- network: serverStatus.network,
661
- memory: serverStatus.mem,
662
- storageEngine: serverStatus.storageEngine,
663
- security: serverStatus.security,
664
- };
665
- }
666
- return {
667
- content: [
668
- {
669
- type: "text",
670
- text: JSON.stringify(serverInfo, null, 2),
671
- },
672
- ],
673
- };
674
- }
675
- catch (error) {
676
- if (error instanceof Error) {
677
- throw new Error(`Failed to get server information: ${error.message}`);
678
- }
679
- throw new Error("Failed to get server information: Unknown error");
680
- }
681
- }
682
- case "insert": {
683
- const { documents, ordered, writeConcern, bypassDocumentValidation } = request.params.arguments || {};
684
- // Validate collection name to prevent access to system collections
685
- if (collection.collectionName.startsWith("system.")) {
686
- throw new Error("Access to system collections is not allowed");
687
- }
688
- // Validate documents array
689
- if (!Array.isArray(documents)) {
690
- throw new Error("Documents must be an array");
691
- }
692
- if (documents.length === 0) {
693
- throw new Error("Documents array cannot be empty");
694
- }
695
- if (!documents.every((doc) => doc && typeof doc === "object" && !Array.isArray(doc))) {
696
- throw new Error("Each document must be a valid MongoDB document object");
697
- }
698
- try {
699
- // Prepare insert options
700
- const options = {
701
- ordered: ordered !== false, // default to true if not specified
702
- writeConcern,
703
- bypassDocumentValidation,
704
- };
705
- // Use insertMany for consistency, it works for single documents too
706
- const result = await collection.insertMany(documents, options);
707
- return {
708
- content: [
709
- {
710
- type: "text",
711
- text: JSON.stringify({
712
- acknowledged: result.acknowledged,
713
- insertedCount: result.insertedCount,
714
- insertedIds: result.insertedIds,
715
- }, null, 2),
716
- },
717
- ],
718
- };
719
- }
720
- catch (error) {
721
- if (error instanceof Error) {
722
- // Handle bulk write errors specially to provide more detail
723
- if (error.name === "BulkWriteError") {
724
- const bulkError = error;
725
- return {
726
- content: [
727
- {
728
- type: "text",
729
- text: JSON.stringify({
730
- error: "Bulk write error occurred",
731
- writeErrors: bulkError.writeErrors,
732
- insertedCount: bulkError.result?.nInserted || 0,
733
- failedCount: bulkError.result?.nFailedInserts || 0,
734
- }, null, 2),
735
- },
736
- ],
737
- };
738
- }
739
- throw new Error(`Failed to insert documents into collection ${collection.collectionName}: ${error.message}`);
740
- }
741
- throw new Error(`Failed to insert documents into collection ${collection.collectionName}: Unknown error`);
742
- }
743
- }
744
- case "createIndex": {
745
- const { indexes, writeConcern, commitQuorum } = request.params.arguments || {};
746
- // Validate collection name to prevent access to system collections
747
- if (collection.collectionName.startsWith("system.")) {
748
- throw new Error("Access to system collections is not allowed");
749
- }
750
- // Validate indexes array
751
- if (!Array.isArray(indexes) || indexes.length === 0) {
752
- throw new Error("Indexes must be a non-empty array");
753
- }
754
- // Validate writeConcern
755
- if (writeConcern &&
756
- (typeof writeConcern !== "object" || Array.isArray(writeConcern))) {
757
- throw new Error("Write concern must be a valid MongoDB write concern object");
758
- }
759
- // Validate commitQuorum
760
- if (commitQuorum &&
761
- typeof commitQuorum !== "string" &&
762
- typeof commitQuorum !== "number") {
763
- throw new Error("Commit quorum must be a string or number");
764
- }
765
- try {
766
- const result = await collection.createIndexes(indexes, {
767
- writeConcern,
768
- commitQuorum: typeof commitQuorum === "number" ? commitQuorum : undefined,
769
- });
770
- return {
771
- content: [
772
- {
773
- type: "text",
774
- text: JSON.stringify({
775
- acknowledged: result.acknowledged,
776
- createdIndexes: result.createdIndexes,
777
- numIndexesBefore: result.numIndexesBefore,
778
- numIndexesAfter: result.numIndexesAfter,
779
- }, null, 2),
780
- },
781
- ],
782
- };
783
- }
784
- catch (error) {
785
- if (error instanceof Error) {
786
- throw new Error(`Failed to create indexes on collection ${collection.collectionName}: ${error.message}`);
787
- }
788
- throw new Error(`Failed to create indexes on collection ${collection.collectionName}: Unknown error`);
789
- }
790
- }
791
- case "count": {
792
- const args = request.params.arguments || {};
793
- const { query } = args;
794
- // Validate collection name to prevent access to system collections
795
- if (collection.collectionName.startsWith("system.")) {
796
- throw new Error("Access to system collections is not allowed");
797
- }
798
- // Validate and parse query
799
- let countQuery = {};
800
- if (query) {
801
- if (typeof query === "string") {
802
- try {
803
- countQuery = JSON.parse(query);
804
- }
805
- catch (e) {
806
- throw new Error("Invalid query format: must be a valid JSON object");
807
- }
808
- }
809
- else if (typeof query === "object" &&
810
- query !== null &&
811
- !Array.isArray(query)) {
812
- countQuery = query;
813
- }
814
- else {
815
- throw new Error("Query must be a plain object");
816
- }
817
- }
818
- try {
819
- const options = {
820
- limit: typeof args.limit === "number" ? args.limit : undefined,
821
- skip: typeof args.skip === "number" ? args.skip : undefined,
822
- hint: typeof args.hint === "object" && args.hint !== null
823
- ? args.hint
824
- : undefined,
825
- readConcern: typeof args.readConcern === "object" && args.readConcern !== null
826
- ? args.readConcern
827
- : undefined,
828
- maxTimeMS: typeof args.maxTimeMS === "number" ? args.maxTimeMS : undefined,
829
- collation: typeof args.collation === "object" && args.collation !== null
830
- ? args.collation
831
- : undefined,
832
- };
833
- // Remove undefined options
834
- Object.keys(options).forEach((key) => options[key] === undefined && delete options[key]);
835
- // Execute count operation
836
- const count = await collection.countDocuments(countQuery, options);
837
- return {
838
- content: [
839
- {
840
- type: "text",
841
- text: JSON.stringify({
842
- count: count,
843
- ok: 1,
844
- }, null, 2),
845
- },
846
- ],
847
- };
848
- }
849
- catch (error) {
850
- if (error instanceof Error) {
851
- throw new Error(`Failed to count documents in collection ${collection.collectionName}: ${error.message}`);
852
- }
853
- throw new Error(`Failed to count documents in collection ${collection.collectionName}: Unknown error`);
854
- }
855
- }
856
- case "listCollections": {
857
- const { nameOnly, filter } = request.params.arguments || {};
858
- try {
859
- // Get the list of collections
860
- const options = filter ? { filter } : {};
861
- const collections = await db.listCollections(options).toArray();
862
- // If nameOnly is true, return only the collection names
863
- const result = nameOnly
864
- ? collections.map((collection) => collection.name)
865
- : collections;
866
- return {
867
- content: [
868
- {
869
- type: "text",
870
- text: JSON.stringify(result, null, 2),
871
- },
872
- ],
873
- };
874
- }
875
- catch (error) {
876
- if (error instanceof Error) {
877
- throw new Error(`Failed to list collections: ${error.message}`);
878
- }
879
- throw new Error("Failed to list collections: Unknown error");
880
- }
881
- }
882
- default:
883
- throw new Error(`Unknown tool: ${request.params.name}`);
884
- }
885
- });
886
- /**
887
- * Handler that lists available prompts.
888
- * Exposes prompts for analyzing collections.
889
- */
890
- server.setRequestHandler(ListPromptsRequestSchema, async () => {
891
- return {
892
- prompts: [
893
- {
894
- name: "analyze_collection",
895
- description: "Analyze a MongoDB collection structure and contents",
896
- arguments: [
897
- {
898
- name: "collection",
899
- description: "Name of the collection to analyze",
900
- required: true,
901
- },
902
- ],
903
- },
904
- ],
905
- };
906
- });
907
- /**
908
- * Handler for collection analysis prompt.
909
- * Returns a prompt that requests analysis of a collection's structure and data.
910
- */
911
- server.setRequestHandler(GetPromptRequestSchema, async (request) => {
912
- if (request.params.name !== "analyze_collection") {
913
- throw new Error("Unknown prompt");
914
- }
915
- const collectionName = request.params.arguments?.collection;
916
- if (!collectionName) {
917
- throw new Error("Collection name is required");
918
- }
919
- try {
920
- const collection = db.collection(collectionName);
921
- // Validate collection name to prevent access to system collections
922
- if (collection.collectionName.startsWith("system.")) {
923
- throw new Error("Access to system collections is not allowed");
924
- }
925
- const schema = await collection.findOne({});
926
- // Get basic collection stats - just count in API v1
927
- const stats = await collection
928
- .aggregate([
929
- {
930
- $collStats: {
931
- count: {},
932
- },
933
- },
934
- ])
935
- .toArray();
936
- // Also get a sample of documents to show data distribution
937
- const sampleDocs = await collection.find({}).limit(5).toArray();
938
- return {
939
- messages: [
940
- {
941
- role: "user",
942
- content: {
943
- type: "text",
944
- text: `Please analyze the following MongoDB collection:
945
- Collection: ${collectionName}
946
-
947
- Schema:
948
- ${JSON.stringify(schema, null, 2)}
949
-
950
- Stats:
951
- Document count: ${stats[0]?.count || "unknown"}
952
-
953
- Sample documents:
954
- ${JSON.stringify(sampleDocs, null, 2)}`,
955
- },
956
- },
957
- {
958
- role: "user",
959
- content: {
960
- type: "text",
961
- text: "Provide insights about the collection's structure, data types, and basic statistics.",
962
- },
963
- },
964
- ],
965
- };
966
- }
967
- catch (error) {
968
- if (error instanceof Error) {
969
- throw new Error(`Failed to analyze collection ${collectionName}: ${error.message}`);
970
- }
971
- else {
972
- throw new Error(`Failed to analyze collection ${collectionName}: Unknown error`);
973
- }
974
- }
975
- });
976
- /**
977
- * Handler for listing templates.
978
- * Exposes templates for constructing MongoDB queries.
979
- */
980
- server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
981
- return {
982
- resourceTemplates: [
983
- {
984
- name: "mongodb_query",
985
- description: "Template for constructing MongoDB queries",
986
- uriTemplate: "mongodb:///{collection}",
987
- text: `To query MongoDB collections, you can use these operators:
988
-
989
- Filter operators:
990
- - $eq: Matches values equal to a specified value
991
- - $gt/$gte: Matches values greater than (or equal to) a specified value
992
- - $lt/$lte: Matches values less than (or equal to) a specified value
993
- - $in: Matches any of the values in an array
994
- - $nin: Matches none of the values in an array
995
- - $ne: Matches values not equal to a specified value
996
- - $exists: Matches documents that have the specified field
997
-
998
- Example queries:
999
- 1. Find documents where age > 21:
1000
- { "age": { "$gt": 21 } }
1001
-
1002
- 2. Find documents with specific status:
1003
- { "status": { "$in": ["active", "pending"] } }
1004
-
1005
- 3. Find documents with existing email:
1006
- { "email": { "$exists": true } }
1007
-
1008
- Use these patterns to construct MongoDB queries.`,
1009
- },
1010
- ],
1011
- };
1012
- });
3
+ import { connectToMongoDB } from "./mongo.js";
4
+ import { createServer } from "./server.js";
5
+ // Declare a client variable in the global scope for cleanup handlers
6
+ let mongoClient = null; // TODO: Fix Typescript error
1013
7
  /**
1014
8
  * Start the server using stdio transport and initialize MongoDB connection.
1015
9
  */
1016
10
  async function main() {
1017
11
  const args = process.argv.slice(2);
12
+ // Default to environment variables
1018
13
  let connectionUrl = "";
1019
- let readOnlyMode = false;
1020
- // Parse command line arguments
14
+ let readOnlyMode = process.env.MCP_MONGODB_READONLY === "true" || false;
15
+ // Parse command line arguments (these take precedence)
1021
16
  for (let i = 0; i < args.length; i++) {
1022
17
  if (args[i] === "--read-only" || args[i] === "-r") {
1023
18
  readOnlyMode = true;
@@ -1026,29 +21,54 @@ async function main() {
1026
21
  connectionUrl = args[i];
1027
22
  }
1028
23
  }
24
+ // If no connection URL from command line, use environment variable
25
+ if (!connectionUrl) {
26
+ connectionUrl = process.env.MCP_MONGODB_URI || "";
27
+ }
1029
28
  if (!connectionUrl) {
1030
- console.error("Please provide a MongoDB connection URL as a command-line argument");
29
+ console.error("Please provide a MongoDB connection URL via command-line argument or MCP_MONGODB_URI environment variable");
1031
30
  console.error("Usage: command <mongodb-url> [--read-only|-r]");
31
+ console.error(" or: MCP_MONGODB_URI=<mongodb-url> [MCP_MONGODB_READONLY=true] command");
32
+ process.exit(1);
33
+ }
34
+ // Ensure connection URL has the correct prefix
35
+ if (!connectionUrl.startsWith("mongodb://") &&
36
+ !connectionUrl.startsWith("mongodb+srv://")) {
37
+ console.error("Invalid MongoDB connection URL. URL must start with 'mongodb://' or 'mongodb+srv://'");
1032
38
  process.exit(1);
1033
39
  }
1034
- const connected = await connectToMongoDB(connectionUrl, readOnlyMode);
1035
- if (!connected) {
1036
- console.error("Failed to connect to MongoDB");
40
+ try {
41
+ const { client, db, isConnected, isReadOnlyMode } = await connectToMongoDB(connectionUrl, readOnlyMode);
42
+ // Store client in global variable for cleanup
43
+ mongoClient = client;
44
+ if (!isConnected || !client || !db) {
45
+ console.error("Failed to connect to MongoDB");
46
+ process.exit(1);
47
+ }
48
+ // Pass db instead of client to createServer
49
+ const server = createServer(client, db, isReadOnlyMode);
50
+ const transport = new StdioServerTransport();
51
+ await server.connect(transport);
52
+ console.warn("Server connected successfully");
53
+ }
54
+ catch (error) {
55
+ console.error("Failed to connect to MongoDB:", error);
56
+ if (mongoClient) {
57
+ await mongoClient.close();
58
+ }
1037
59
  process.exit(1);
1038
60
  }
1039
- const transport = new StdioServerTransport();
1040
- await server.connect(transport);
1041
61
  }
1042
62
  // Handle cleanup
1043
63
  process.on("SIGINT", async () => {
1044
- if (client) {
1045
- await client.close();
64
+ if (mongoClient) {
65
+ await mongoClient.close();
1046
66
  }
1047
67
  process.exit(0);
1048
68
  });
1049
69
  process.on("SIGTERM", async () => {
1050
- if (client) {
1051
- await client.close();
70
+ if (mongoClient) {
71
+ await mongoClient.close();
1052
72
  }
1053
73
  process.exit(0);
1054
74
  });