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