mcp-mongo-server 1.3.0 → 2.0.2

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,78 +1,1521 @@
1
1
  #!/usr/bin/env node
2
+
3
+ // src/index.ts
2
4
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
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
7
- /**
8
- * Start the server using stdio transport and initialize MongoDB connection.
9
- */
10
- async function main() {
11
- const args = process.argv.slice(2);
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)
16
- for (let i = 0; i < args.length; i++) {
17
- if (args[i] === "--read-only" || args[i] === "-r") {
18
- readOnlyMode = true;
5
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
6
+ import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
7
+
8
+ // src/mongo.ts
9
+ import { MongoClient, ReadPreference } from "mongodb";
10
+ async function connectToMongoDB(url, readOnly) {
11
+ try {
12
+ const options = readOnly ? { readPreference: ReadPreference.SECONDARY } : {};
13
+ const client = new MongoClient(url, options);
14
+ await client.connect();
15
+ const db = client.db();
16
+ console.warn(`Connected to MongoDB database: ${db.databaseName}`);
17
+ return {
18
+ client,
19
+ db,
20
+ isConnected: true,
21
+ isReadOnlyMode: readOnly
22
+ };
23
+ } catch (error) {
24
+ console.error("Failed to connect to MongoDB:", error);
25
+ return {
26
+ client: null,
27
+ db: null,
28
+ isConnected: false,
29
+ isReadOnlyMode: readOnly
30
+ };
31
+ }
32
+ }
33
+
34
+ // src/server.ts
35
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
36
+ import {
37
+ CallToolRequestSchema,
38
+ CompleteRequestSchema,
39
+ GetPromptRequestSchema,
40
+ ListPromptsRequestSchema,
41
+ ListResourcesRequestSchema,
42
+ ListResourceTemplatesRequestSchema,
43
+ ListToolsRequestSchema,
44
+ PingRequestSchema,
45
+ ReadResourceRequestSchema
46
+ } from "@modelcontextprotocol/sdk/types.js";
47
+
48
+ // src/schemas/call.ts
49
+ import { ObjectId } from "mongodb";
50
+ var COLLECTION_OPERATIONS = [
51
+ "query",
52
+ "aggregate",
53
+ "update",
54
+ "insert",
55
+ "createIndex",
56
+ "count"
57
+ ];
58
+ var WRITE_OPERATIONS = ["update", "insert", "createIndex"];
59
+ async function handleCallToolRequest({
60
+ request,
61
+ client,
62
+ db,
63
+ isReadOnlyMode
64
+ }) {
65
+ const { name, arguments: args = {} } = request.params;
66
+ const operation = name;
67
+ const objectIdMode = args.objectIdMode || "auto";
68
+ const filteredArgs = {};
69
+ for (const [key, value] of Object.entries(args)) {
70
+ if (key !== "objectIdMode") {
71
+ filteredArgs[key] = value;
72
+ }
73
+ }
74
+ if (args.sort) {
75
+ args.sort = parseSort(args.sort);
76
+ }
77
+ Object.assign(args, filteredArgs);
78
+ validateOperation(operation);
79
+ checkReadOnlyMode(operation, isReadOnlyMode);
80
+ let collection = null;
81
+ if (COLLECTION_OPERATIONS.includes(operation)) {
82
+ const collectionName = args.collection;
83
+ if (!collectionName) {
84
+ throw new Error(
85
+ `Collection name is required for '${operation}' operation`
86
+ );
87
+ }
88
+ collection = db.collection(collectionName);
89
+ validateCollection(collection);
90
+ }
91
+ switch (operation) {
92
+ case "query":
93
+ return handleQuery(collection, args, objectIdMode);
94
+ case "aggregate":
95
+ return handleAggregate(collection, args, objectIdMode);
96
+ case "update":
97
+ return handleUpdate(collection, args, objectIdMode);
98
+ case "serverInfo":
99
+ return handleServerInfo(db, isReadOnlyMode, args);
100
+ case "insert":
101
+ return handleInsert(collection, args, objectIdMode);
102
+ case "createIndex":
103
+ return handleCreateIndex(collection, args, objectIdMode);
104
+ case "count":
105
+ return handleCount(collection, args, objectIdMode);
106
+ case "listCollections":
107
+ return handleListCollections(db, args, objectIdMode);
108
+ default:
109
+ throw new Error(`Unknown operation: ${operation}`);
110
+ }
111
+ }
112
+ function validateOperation(operation) {
113
+ const validOperations = [
114
+ "query",
115
+ "aggregate",
116
+ "update",
117
+ "serverInfo",
118
+ "insert",
119
+ "createIndex",
120
+ "count",
121
+ "listCollections"
122
+ ];
123
+ if (!validOperations.includes(operation)) {
124
+ throw new Error(`Unknown operation: ${operation}`);
125
+ }
126
+ }
127
+ function validateCollection(collection) {
128
+ if (!collection.collectionName) {
129
+ throw new Error("Collection name cannot be empty");
130
+ }
131
+ if (collection.collectionName.startsWith("system.")) {
132
+ throw new Error("Access to system collections is not allowed");
133
+ }
134
+ }
135
+ function checkReadOnlyMode(operation, isReadOnlyMode) {
136
+ if (isReadOnlyMode && WRITE_OPERATIONS.includes(operation)) {
137
+ throw new Error(
138
+ `ReadonlyError: Operation '${operation}' is not allowed in read-only mode`
139
+ );
140
+ }
141
+ }
142
+ function parseSort(sort) {
143
+ if (!sort) return null;
144
+ if (typeof sort !== "object" || sort === null || Array.isArray(sort)) {
145
+ return null;
146
+ }
147
+ const validSort = {};
148
+ for (const [key, value] of Object.entries(sort)) {
149
+ if (typeof value === "number" && (value === 1 || value === -1)) {
150
+ validSort[key] = value;
151
+ }
152
+ }
153
+ return Object.keys(validSort).length > 0 ? validSort : null;
154
+ }
155
+ function parseFilter(filter, objectIdMode = "auto") {
156
+ if (!filter) {
157
+ return {};
158
+ }
159
+ if (typeof filter === "string") {
160
+ try {
161
+ return processObjectIdInFilter(JSON.parse(filter), objectIdMode);
162
+ } catch {
163
+ throw new Error("Invalid filter format: must be a valid JSON object");
164
+ }
165
+ }
166
+ if (typeof filter === "object" && filter !== null && !Array.isArray(filter)) {
167
+ return processObjectIdInFilter(
168
+ filter,
169
+ objectIdMode
170
+ );
171
+ }
172
+ throw new Error("Query filter must be a plain object or ObjectId");
173
+ }
174
+ function isObjectIdField(fieldName) {
175
+ const lowerFieldName = fieldName.toLowerCase();
176
+ return lowerFieldName === "_id" || lowerFieldName === "id" || lowerFieldName.endsWith("id") || lowerFieldName.endsWith("_id");
177
+ }
178
+ function processObjectIdInFilter(filter, objectIdMode = "auto") {
179
+ if (objectIdMode === "none") {
180
+ const result2 = {};
181
+ for (const [key, value] of Object.entries(filter)) {
182
+ if (typeof value === "string" && isISODateString(value)) {
183
+ result2[key] = new Date(value);
184
+ } else if (typeof value === "string" && value.startsWith("ISODate(") && value.endsWith(")")) {
185
+ const dateString = value.substring(8, value.length - 2);
186
+ if (isISODateString(dateString)) {
187
+ result2[key] = new Date(dateString);
188
+ } else {
189
+ result2[key] = value;
19
190
  }
20
- else if (!connectionUrl) {
21
- connectionUrl = args[i];
191
+ } else if (typeof value === "object" && value !== null) {
192
+ if (Array.isArray(value)) {
193
+ result2[key] = value.map((item) => {
194
+ if (typeof item === "string" && isISODateString(item)) {
195
+ return new Date(item);
196
+ } else if (typeof item === "string" && item.startsWith("ISODate(") && item.endsWith(")")) {
197
+ const dateString = item.substring(8, item.length - 2);
198
+ return isISODateString(dateString) ? new Date(dateString) : item;
199
+ }
200
+ return item;
201
+ });
202
+ } else {
203
+ result2[key] = processObjectIdInFilter(
204
+ value,
205
+ "none"
206
+ );
22
207
  }
208
+ } else {
209
+ result2[key] = value;
210
+ }
23
211
  }
24
- // If no connection URL from command line, use environment variable
25
- if (!connectionUrl) {
26
- connectionUrl = process.env.MCP_MONGODB_URI || "";
212
+ return result2;
213
+ }
214
+ const result = {};
215
+ for (const [key, value] of Object.entries(filter)) {
216
+ if (typeof value === "string" && isObjectIdString(value)) {
217
+ if (objectIdMode === "force" || objectIdMode === "auto" && isObjectIdField(key)) {
218
+ result[key] = new ObjectId(value);
219
+ } else {
220
+ result[key] = value;
221
+ }
222
+ } else if (typeof value === "string" && isISODateString(value)) {
223
+ result[key] = new Date(value);
224
+ } else if (typeof value === "string" && value.startsWith("ISODate(") && value.endsWith(")")) {
225
+ const dateString = value.substring(8, value.length - 2);
226
+ if (isISODateString(dateString)) {
227
+ result[key] = new Date(dateString);
228
+ } else {
229
+ result[key] = value;
230
+ }
231
+ } else if (typeof value === "object" && value !== null) {
232
+ if (Array.isArray(value)) {
233
+ result[key] = value.map((item) => {
234
+ if (typeof item === "string" && isObjectIdString(item) && (objectIdMode === "force" || objectIdMode === "auto" && isObjectIdField(key))) {
235
+ return new ObjectId(item);
236
+ } else if (typeof item === "string" && isISODateString(item)) {
237
+ return new Date(item);
238
+ } else if (typeof item === "string" && item.startsWith("ISODate(") && item.endsWith(")")) {
239
+ const dateString = item.substring(8, item.length - 2);
240
+ return isISODateString(dateString) ? new Date(dateString) : item;
241
+ }
242
+ return item;
243
+ });
244
+ } else {
245
+ result[key] = processObjectIdInFilter(
246
+ value,
247
+ objectIdMode
248
+ );
249
+ }
250
+ } else {
251
+ result[key] = value;
27
252
  }
28
- if (!connectionUrl) {
29
- console.error("Please provide a MongoDB connection URL via command-line argument or MCP_MONGODB_URI environment variable");
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);
253
+ }
254
+ return result;
255
+ }
256
+ function isObjectIdString(str) {
257
+ return /^[0-9a-fA-F]{24}$/.test(str);
258
+ }
259
+ function isISODateString(str) {
260
+ return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,3})?Z$/.test(str);
261
+ }
262
+ function formatResponse(data) {
263
+ return {
264
+ content: [
265
+ {
266
+ type: "text",
267
+ text: JSON.stringify(data, null, 2)
268
+ }
269
+ ]
270
+ };
271
+ }
272
+ function handleError(error, operation, collectionName) {
273
+ const context = collectionName ? `collection ${collectionName}` : "operation";
274
+ if (error instanceof Error) {
275
+ throw new Error(`Failed to ${operation} ${context}: ${error.message}`);
276
+ }
277
+ throw new Error(`Failed to ${operation} ${context}: Unknown error`);
278
+ }
279
+ async function handleQuery(collection, args, objectIdMode = "auto") {
280
+ if (!collection) {
281
+ throw new Error("Collection is required for query operation");
282
+ }
283
+ const { filter, projection, limit, explain, sort } = args;
284
+ const queryFilter = parseFilter(filter, objectIdMode);
285
+ try {
286
+ if (explain) {
287
+ const explainResult = await collection.find(queryFilter, {
288
+ projection,
289
+ limit: limit || 100,
290
+ sort
291
+ }).explain(explain);
292
+ return formatResponse(explainResult);
33
293
  }
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://'");
38
- process.exit(1);
294
+ const cursor = collection.find(queryFilter, {
295
+ projection,
296
+ limit: limit || 100,
297
+ sort
298
+ });
299
+ const results = await cursor.toArray();
300
+ return formatResponse(results);
301
+ } catch (error) {
302
+ return handleError(error, "query", collection.collectionName);
303
+ }
304
+ }
305
+ async function handleAggregate(collection, args, objectIdMode = "auto") {
306
+ if (!collection) {
307
+ throw new Error("Collection is required for aggregate operation");
308
+ }
309
+ const { pipeline, explain } = args;
310
+ if (!Array.isArray(pipeline)) {
311
+ throw new Error("Pipeline must be an array");
312
+ }
313
+ const processedPipeline = pipeline.map((stage) => {
314
+ if (typeof stage === "object" && stage !== null) {
315
+ return processObjectIdInFilter(
316
+ stage,
317
+ objectIdMode
318
+ );
319
+ }
320
+ return stage;
321
+ });
322
+ try {
323
+ if (explain) {
324
+ const explainResult = await collection.aggregate(processedPipeline, {
325
+ explain: {
326
+ verbosity: explain
327
+ }
328
+ }).toArray();
329
+ return formatResponse(explainResult);
330
+ }
331
+ const results = await collection.aggregate(processedPipeline).toArray();
332
+ return formatResponse(results);
333
+ } catch (error) {
334
+ return handleError(error, "aggregate", collection.collectionName);
335
+ }
336
+ }
337
+ async function handleUpdate(collection, args, objectIdMode = "auto") {
338
+ if (!collection) {
339
+ throw new Error("Collection is required for update operation");
340
+ }
341
+ const { filter, update, upsert, multi } = args;
342
+ const queryFilter = parseFilter(filter, objectIdMode);
343
+ let processedUpdate = update;
344
+ if (update && typeof update === "object" && !Array.isArray(update)) {
345
+ processedUpdate = processObjectIdInFilter(
346
+ update,
347
+ objectIdMode
348
+ );
349
+ }
350
+ if (!processedUpdate || typeof processedUpdate !== "object" || Array.isArray(processedUpdate)) {
351
+ throw new Error("Update must be a valid MongoDB update document");
352
+ }
353
+ const validUpdateOperators = [
354
+ "$set",
355
+ "$unset",
356
+ "$inc",
357
+ "$push",
358
+ "$pull",
359
+ "$addToSet",
360
+ "$pop",
361
+ "$rename",
362
+ "$mul"
363
+ ];
364
+ const hasValidOperator = Object.keys(processedUpdate).some(
365
+ (key) => validUpdateOperators.includes(key)
366
+ );
367
+ if (!hasValidOperator) {
368
+ throw new Error(
369
+ "Update must include at least one valid update operator ($set, $unset, etc.)"
370
+ );
371
+ }
372
+ try {
373
+ const options = {
374
+ upsert: !!upsert,
375
+ multi: !!multi
376
+ };
377
+ const updateMethod = options.multi ? "updateMany" : "updateOne";
378
+ const result = await collection[updateMethod](
379
+ queryFilter,
380
+ processedUpdate,
381
+ options
382
+ );
383
+ return formatResponse({
384
+ matchedCount: result.matchedCount,
385
+ modifiedCount: result.modifiedCount,
386
+ upsertedCount: result.upsertedCount,
387
+ upsertedId: result.upsertedId
388
+ });
389
+ } catch (error) {
390
+ return handleError(error, "update", collection.collectionName);
391
+ }
392
+ }
393
+ async function handleServerInfo(db, isReadOnlyMode, args) {
394
+ const { includeDebugInfo } = args;
395
+ try {
396
+ const buildInfo = await db.command({ buildInfo: 1 });
397
+ let serverStatus = null;
398
+ if (includeDebugInfo) {
399
+ serverStatus = await db.command({ serverStatus: 1 });
400
+ }
401
+ const serverInfo = {
402
+ version: buildInfo.version,
403
+ gitVersion: buildInfo.gitVersion,
404
+ modules: buildInfo.modules,
405
+ allocator: buildInfo.allocator,
406
+ javascriptEngine: buildInfo.javascriptEngine,
407
+ sysInfo: buildInfo.sysInfo,
408
+ storageEngines: buildInfo.storageEngines,
409
+ debug: buildInfo.debug,
410
+ maxBsonObjectSize: buildInfo.maxBsonObjectSize,
411
+ openssl: buildInfo.openssl,
412
+ buildEnvironment: buildInfo.buildEnvironment,
413
+ bits: buildInfo.bits,
414
+ ok: buildInfo.ok,
415
+ status: {},
416
+ connectionInfo: {
417
+ readOnlyMode: isReadOnlyMode,
418
+ readPreference: isReadOnlyMode ? "secondary" : "primary"
419
+ }
420
+ };
421
+ if (serverStatus) {
422
+ serverInfo.status = {
423
+ host: serverStatus.host,
424
+ version: serverStatus.version,
425
+ process: serverStatus.process,
426
+ pid: serverStatus.pid,
427
+ uptime: serverStatus.uptime,
428
+ uptimeMillis: serverStatus.uptimeMillis,
429
+ uptimeEstimate: serverStatus.uptimeEstimate,
430
+ localTime: serverStatus.localTime,
431
+ connections: serverStatus.connections,
432
+ network: serverStatus.network,
433
+ memory: serverStatus.mem,
434
+ storageEngine: serverStatus.storageEngine,
435
+ security: serverStatus.security
436
+ };
437
+ }
438
+ return formatResponse(serverInfo);
439
+ } catch (error) {
440
+ return handleError(error, "get server information");
441
+ }
442
+ }
443
+ async function handleInsert(collection, args, objectIdMode = "auto") {
444
+ if (!collection) {
445
+ throw new Error("Collection is required for insert operation");
446
+ }
447
+ const { documents, ordered, writeConcern, bypassDocumentValidation } = args;
448
+ if (!Array.isArray(documents)) {
449
+ throw new Error("Documents must be an array");
450
+ }
451
+ if (documents.length === 0) {
452
+ throw new Error("Documents array cannot be empty");
453
+ }
454
+ if (!documents.every(
455
+ (doc) => doc && typeof doc === "object" && !Array.isArray(doc)
456
+ )) {
457
+ throw new Error("Each document must be a valid MongoDB document object");
458
+ }
459
+ const processedDocuments = documents.map(
460
+ (doc) => processObjectIdInFilter(doc, objectIdMode)
461
+ );
462
+ try {
463
+ const options = {
464
+ ordered: ordered !== false,
465
+ // default to true if not specified
466
+ writeConcern,
467
+ bypassDocumentValidation
468
+ };
469
+ const result = await collection.insertMany(
470
+ processedDocuments,
471
+ options
472
+ );
473
+ return formatResponse({
474
+ acknowledged: result.acknowledged,
475
+ insertedCount: result.insertedCount,
476
+ insertedIds: result.insertedIds
477
+ });
478
+ } catch (error) {
479
+ if (error instanceof Error && error.name === "BulkWriteError") {
480
+ const bulkError = error;
481
+ return formatResponse({
482
+ error: "Bulk write error occurred",
483
+ writeErrors: bulkError.writeErrors,
484
+ insertedCount: bulkError.result?.nInserted || 0,
485
+ failedCount: bulkError.result?.nFailedInserts || 0
486
+ });
487
+ }
488
+ return handleError(error, "insert", collection.collectionName);
489
+ }
490
+ }
491
+ async function handleCreateIndex(collection, args, objectIdMode = "auto") {
492
+ if (!collection) {
493
+ throw new Error("Collection is required for createIndex operation");
494
+ }
495
+ const { indexes, commitQuorum, writeConcern } = args;
496
+ if (!Array.isArray(indexes) || indexes.length === 0) {
497
+ throw new Error("Indexes must be a non-empty array");
498
+ }
499
+ if (writeConcern && (typeof writeConcern !== "object" || Array.isArray(writeConcern))) {
500
+ throw new Error(
501
+ "Write concern must be a valid MongoDB write concern object"
502
+ );
503
+ }
504
+ if (commitQuorum && typeof commitQuorum !== "string" && typeof commitQuorum !== "number") {
505
+ throw new Error("Commit quorum must be a string or number");
506
+ }
507
+ const processedIndexes = indexes.map((index) => {
508
+ if (index && typeof index === "object") {
509
+ return processObjectIdInFilter(
510
+ index,
511
+ objectIdMode
512
+ );
513
+ }
514
+ return index;
515
+ });
516
+ try {
517
+ const indexOptions = {
518
+ commitQuorum: typeof commitQuorum === "number" ? commitQuorum : void 0
519
+ };
520
+ const result = await collection.createIndexes(
521
+ processedIndexes,
522
+ indexOptions
523
+ );
524
+ return formatResponse({
525
+ acknowledged: result.acknowledged,
526
+ createdIndexes: result.createdIndexes,
527
+ numIndexesBefore: result.numIndexesBefore,
528
+ numIndexesAfter: result.numIndexesAfter
529
+ });
530
+ } catch (error) {
531
+ return handleError(error, "create indexes", collection.collectionName);
532
+ }
533
+ }
534
+ async function handleCount(collection, args, objectIdMode = "auto") {
535
+ if (!collection) {
536
+ throw new Error("Collection is required for count operation");
537
+ }
538
+ const { query, limit, skip, hint, readConcern, maxTimeMS, collation } = args;
539
+ const countQuery = parseFilter(query, objectIdMode);
540
+ try {
541
+ const options = {
542
+ limit: typeof limit === "number" ? limit : void 0,
543
+ skip: typeof skip === "number" ? skip : void 0,
544
+ hint: typeof hint === "object" && hint !== null ? hint : void 0,
545
+ readConcern: typeof readConcern === "object" && readConcern !== null ? readConcern : void 0,
546
+ maxTimeMS: typeof maxTimeMS === "number" ? maxTimeMS : void 0,
547
+ collation: typeof collation === "object" && collation !== null ? collation : void 0
548
+ };
549
+ for (const key of Object.keys(options)) {
550
+ if (options[key] === void 0) {
551
+ delete options[key];
552
+ }
553
+ }
554
+ const count = await collection.countDocuments(countQuery, options);
555
+ return formatResponse({
556
+ count,
557
+ ok: 1
558
+ });
559
+ } catch (error) {
560
+ return handleError(error, "count documents", collection.collectionName);
561
+ }
562
+ }
563
+ async function handleListCollections(db, args, objectIdMode = "auto") {
564
+ const { nameOnly, filter } = args;
565
+ let processedFilter = filter;
566
+ if (filter && typeof filter === "object") {
567
+ processedFilter = processObjectIdInFilter(
568
+ filter,
569
+ objectIdMode
570
+ );
571
+ }
572
+ try {
573
+ const options = processedFilter ? { filter: processedFilter } : {};
574
+ const collections = await db.listCollections(options).toArray();
575
+ const result = nameOnly ? collections.map((collection) => collection.name) : collections;
576
+ return formatResponse(result);
577
+ } catch (error) {
578
+ return handleError(error, "list collections");
579
+ }
580
+ }
581
+
582
+ // src/schemas/completion.ts
583
+ async function handleCompletionRequest({
584
+ request,
585
+ client,
586
+ db,
587
+ isReadOnlyMode
588
+ }) {
589
+ const { ref, argument } = request.params;
590
+ if (ref.type === "ref/prompt") {
591
+ return handlePromptCompletion(
592
+ client,
593
+ db,
594
+ isReadOnlyMode,
595
+ ref.name,
596
+ argument
597
+ );
598
+ }
599
+ if (ref.type === "ref/resource") {
600
+ return handleResourceCompletion(
601
+ client,
602
+ db,
603
+ isReadOnlyMode,
604
+ ref.uri,
605
+ argument
606
+ );
607
+ }
608
+ return emptyCompletionResult();
609
+ }
610
+ async function handlePromptCompletion(client, db, isReadOnlyMode, promptName, argument) {
611
+ if (!promptName) {
612
+ return emptyCompletionResult();
613
+ }
614
+ if (argument.name === "collection") {
615
+ return await completeCollectionNames(argument.value, db, isReadOnlyMode);
616
+ }
617
+ return emptyCompletionResult();
618
+ }
619
+ async function handleResourceCompletion(client, db, isReadOnlyMode, promptName, argument) {
620
+ if (!promptName) {
621
+ return emptyCompletionResult();
622
+ }
623
+ if (argument.name === "collection") {
624
+ return await completeCollectionNames(argument.value, db, isReadOnlyMode);
625
+ }
626
+ return emptyCompletionResult();
627
+ }
628
+ async function completeCollectionNames(partialValue, db, isReadOnlyMode) {
629
+ try {
630
+ console.warn(
631
+ `Completing collection names with partial value: ${partialValue}`
632
+ );
633
+ const collections = await db.listCollections().toArray();
634
+ const matchingCollections = collections.map(
635
+ (c) => c.name
636
+ ).filter(
637
+ (name) => (
638
+ // Filter out system collections
639
+ !name.startsWith("system.") && // Match partial value
640
+ name.toLowerCase().includes(partialValue.toLowerCase())
641
+ )
642
+ ).sort();
643
+ console.warn(`Found ${matchingCollections.length} matching collections`);
644
+ const MAX_ITEMS = 100;
645
+ const limitedResults = matchingCollections.slice(0, MAX_ITEMS);
646
+ const hasMore = matchingCollections.length > MAX_ITEMS;
647
+ return {
648
+ completion: {
649
+ values: limitedResults,
650
+ total: matchingCollections.length,
651
+ hasMore
652
+ }
653
+ };
654
+ } catch (error) {
655
+ console.error("Error completing collection names:", error);
656
+ return emptyCompletionResult();
657
+ }
658
+ }
659
+ function emptyCompletionResult() {
660
+ return {
661
+ completion: {
662
+ values: [],
663
+ total: 0,
664
+ hasMore: false
665
+ }
666
+ };
667
+ }
668
+
669
+ // src/schemas/ping.ts
670
+ async function handlePingRequest({
671
+ request,
672
+ client,
673
+ db,
674
+ isReadOnlyMode
675
+ }) {
676
+ try {
677
+ if (!client) {
678
+ throw new Error("MongoDB connection is not available");
679
+ }
680
+ const pong = await db.command({ ping: 1 });
681
+ if (pong.ok !== 1) {
682
+ throw new Error(`MongoDB ping failed: ${pong.errmsg}`);
683
+ }
684
+ return {};
685
+ } catch (error) {
686
+ if (error instanceof Error) {
687
+ throw new Error(`MongoDB ping failed: ${error.message}`);
688
+ }
689
+ throw new Error("MongoDB ping failed: Unknown error");
690
+ }
691
+ }
692
+
693
+ // src/schemas/prompts.ts
694
+ async function handleListPromptsRequest({
695
+ request,
696
+ client,
697
+ db,
698
+ isReadOnlyMode
699
+ }) {
700
+ return {
701
+ prompts: [
702
+ {
703
+ name: "analyze_collection",
704
+ description: "Analyze a MongoDB collection structure and contents",
705
+ arguments: [
706
+ {
707
+ name: "collection",
708
+ description: "Name of the collection to analyze",
709
+ required: true
710
+ }
711
+ ]
712
+ }
713
+ ]
714
+ };
715
+ }
716
+ async function handleGetPromptRequest({
717
+ request,
718
+ client,
719
+ db,
720
+ isReadOnlyMode
721
+ }) {
722
+ const { name, arguments: args = {} } = request.params;
723
+ if (name !== "analyze_collection") {
724
+ throw new Error("Unknown prompt");
725
+ }
726
+ const collectionName = args.collection;
727
+ if (!collectionName) {
728
+ throw new Error("Collection name is required");
729
+ }
730
+ try {
731
+ const collection = db.collection(collectionName);
732
+ if (collection.collectionName.startsWith("system.")) {
733
+ throw new Error("Access to system collections is not allowed");
39
734
  }
735
+ const schemaSample = await collection.findOne({});
736
+ const stats = await collection.aggregate([{ $collStats: { count: {} } }]).toArray();
737
+ const sampleDocs = await collection.find({}).limit(5).toArray();
738
+ const documentCount = stats[0]?.count ?? "unknown";
739
+ return {
740
+ messages: [
741
+ {
742
+ role: "user",
743
+ content: {
744
+ type: "text",
745
+ text: `Please analyze the following MongoDB collection:
746
+ Collection: ${collectionName}
747
+
748
+ Schema:
749
+ ${JSON.stringify(schemaSample, null, 2)}
750
+
751
+ Stats:
752
+ Document count: ${documentCount}
753
+
754
+ Sample documents:
755
+ ${JSON.stringify(sampleDocs, null, 2)}`
756
+ }
757
+ },
758
+ {
759
+ role: "user",
760
+ content: {
761
+ type: "text",
762
+ text: "Provide insights about the collection's structure, data types, and basic statistics."
763
+ }
764
+ }
765
+ ]
766
+ };
767
+ } catch (error) {
768
+ const msg = error instanceof Error ? error.message : "Unknown error";
769
+ throw new Error(`Failed to analyze collection ${collectionName}: ${msg}`);
770
+ }
771
+ }
772
+
773
+ // src/schemas/resource.ts
774
+ import { ObjectId as ObjectId2 } from "mongodb";
775
+ function detectMongoType(value) {
776
+ if (value === null) return "null";
777
+ if (value === void 0) return "undefined";
778
+ if (value instanceof ObjectId2) return "ObjectId";
779
+ if (value instanceof Date) return "Date";
780
+ if (Array.isArray(value)) {
781
+ if (value.length === 0) return "Array";
782
+ const elementTypes = new Set(value.map((item) => detectMongoType(item)));
783
+ if (elementTypes.size === 1) {
784
+ return `Array<${Array.from(elementTypes)[0]}>`;
785
+ }
786
+ return "Array<mixed>";
787
+ }
788
+ if (typeof value === "object") {
789
+ return "Document";
790
+ }
791
+ return typeof value;
792
+ }
793
+ function inferSchemaFromSamples(documents) {
794
+ if (!documents || documents.length === 0) {
795
+ return { fields: [] };
796
+ }
797
+ const fieldMap = /* @__PURE__ */ new Map();
798
+ for (const doc of documents) {
799
+ for (const [key, value] of Object.entries(doc)) {
800
+ if (!fieldMap.has(key)) {
801
+ fieldMap.set(key, {
802
+ name: key,
803
+ types: /* @__PURE__ */ new Set([detectMongoType(value)]),
804
+ nullable: false,
805
+ // Store sample values for complex types
806
+ samples: [value]
807
+ });
808
+ } else {
809
+ const fieldInfo = fieldMap.get(key);
810
+ fieldInfo.types.add(detectMongoType(value));
811
+ if (fieldInfo.samples.length < 3 && !fieldInfo.samples.some(
812
+ (sample) => JSON.stringify(sample) === JSON.stringify(value)
813
+ )) {
814
+ fieldInfo.samples.push(value);
815
+ }
816
+ }
817
+ }
818
+ }
819
+ for (const doc of documents) {
820
+ for (const [key] of fieldMap.entries()) {
821
+ if (!(key in doc)) {
822
+ const fieldInfo = fieldMap.get(key);
823
+ fieldInfo.nullable = true;
824
+ }
825
+ }
826
+ }
827
+ for (const [key, fieldInfo] of fieldMap.entries()) {
828
+ if (fieldInfo.types.has("Document")) {
829
+ const nestedDocs = documents.filter(
830
+ (doc) => doc[key] && typeof doc[key] === "object" && !Array.isArray(doc[key])
831
+ ).map((doc) => doc[key]);
832
+ if (nestedDocs.length > 0) {
833
+ fieldInfo.nestedSchema = inferSchemaFromSamples(nestedDocs);
834
+ }
835
+ }
836
+ }
837
+ const fields = Array.from(fieldMap.values()).map((fieldInfo) => {
838
+ const result = {
839
+ name: fieldInfo.name,
840
+ types: Array.from(fieldInfo.types),
841
+ nullable: fieldInfo.nullable,
842
+ prevalence: `${Math.round(
843
+ documents.filter((doc) => fieldInfo.name in doc).length / documents.length * 100
844
+ )}%`,
845
+ examples: []
846
+ };
847
+ if (fieldInfo.nestedSchema) {
848
+ result.nestedSchema = fieldInfo.nestedSchema;
849
+ }
850
+ const sampleValues = fieldInfo.samples.map((sample) => {
851
+ if (sample instanceof ObjectId2) return sample.toString();
852
+ if (sample instanceof Date) return sample.toISOString();
853
+ if (typeof sample === "object") {
854
+ return Array.isArray(sample) ? "[...]" : "{...}";
855
+ }
856
+ return sample;
857
+ });
858
+ result.examples = sampleValues;
859
+ return result;
860
+ });
861
+ return { fields };
862
+ }
863
+ async function handleReadResourceRequest({
864
+ request,
865
+ client,
866
+ db,
867
+ isReadOnlyMode
868
+ }) {
869
+ const url = new URL(request.params.uri);
870
+ const collectionName = url.pathname.replace(/^\//, "");
871
+ try {
872
+ const collection = db.collection(collectionName);
873
+ const sampleSize = 100;
874
+ let sampleDocuments = [];
875
+ try {
876
+ sampleDocuments = await collection.aggregate([{ $sample: { size: sampleSize } }]).toArray();
877
+ } catch (sampleError) {
878
+ console.warn(
879
+ `$sample aggregation failed for ${collectionName}, falling back to sequential scan: ${sampleError}`
880
+ );
881
+ sampleDocuments = await collection.find({}).limit(sampleSize).toArray();
882
+ }
883
+ const indexes = await collection.indexes();
884
+ const inferredSchema = inferSchemaFromSamples(sampleDocuments);
885
+ let documentCount = null;
40
886
  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);
887
+ documentCount = await Promise.race([
888
+ collection.countDocuments(),
889
+ new Promise(
890
+ (_, reject) => setTimeout(
891
+ () => reject(new Error("Count operation timed out")),
892
+ 5e3
893
+ )
894
+ )
895
+ ]);
896
+ } catch (countError) {
897
+ console.warn(
898
+ `Count operation failed or timed out for ${collectionName}: ${countError}`
899
+ );
900
+ try {
901
+ const stats = await db.command({ collStats: collectionName });
902
+ documentCount = stats.count;
903
+ } catch {
904
+ documentCount = "unknown (count operation timed out)";
905
+ }
906
+ }
907
+ const schema = {
908
+ type: "collection",
909
+ name: collectionName,
910
+ fields: inferredSchema.fields,
911
+ indexes: indexes.map((idx) => ({
912
+ name: idx.name,
913
+ keys: idx.key
914
+ })),
915
+ documentCount,
916
+ sampleSize: sampleDocuments.length,
917
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
918
+ };
919
+ return {
920
+ contents: [
921
+ {
922
+ uri: request.params.uri,
923
+ mimeType: "application/json",
924
+ text: JSON.stringify(schema, null, 2)
925
+ }
926
+ ]
927
+ };
928
+ } catch (error) {
929
+ if (error instanceof Error) {
930
+ throw new Error(
931
+ `Failed to read collection ${collectionName}: ${error.message}`
932
+ );
933
+ }
934
+ throw new Error(
935
+ `Failed to read collection ${collectionName}: Unknown error`
936
+ );
937
+ }
938
+ }
939
+ async function handleListResourcesRequest({
940
+ request,
941
+ client,
942
+ db,
943
+ isReadOnlyMode
944
+ }) {
945
+ try {
946
+ const collections = await db.listCollections().toArray();
947
+ return {
948
+ resources: collections.map((collection) => ({
949
+ uri: `mongodb:///${collection.name}`,
950
+ mimeType: "application/json",
951
+ name: collection.name,
952
+ description: `MongoDB collection: ${collection.name}`
953
+ }))
954
+ };
955
+ } catch (error) {
956
+ if (error instanceof Error) {
957
+ throw new Error(`Failed to list collections: ${error.message}`);
958
+ }
959
+ throw new Error("Failed to list collections: Unknown error");
960
+ }
961
+ }
962
+
963
+ // src/schemas/templates.ts
964
+ async function handleListResourceTemplatesRequest({
965
+ request,
966
+ client,
967
+ db,
968
+ isReadOnlyMode
969
+ }) {
970
+ return {
971
+ resourceTemplates: [
972
+ {
973
+ name: "mongodb_query",
974
+ description: "Template for constructing MongoDB queries",
975
+ uriTemplate: "mongodb:///{collection}",
976
+ text: `To query MongoDB collections, you can use these operators:
977
+
978
+ Filter operators:
979
+ - $eq: Matches values equal to a specified value
980
+ - $gt/$gte: Matches values greater than (or equal to) a specified value
981
+ - $lt/$lte: Matches values less than (or equal to) a specified value
982
+ - $in: Matches any of the values in an array
983
+ - $nin: Matches none of the values in an array
984
+ - $ne: Matches values not equal to a specified value
985
+ - $exists: Matches documents that have the specified field
986
+
987
+ Example queries:
988
+ 1. Find documents where age > 21:
989
+ { "age": { "$gt": 21 } }
990
+
991
+ 2. Find documents with specific status:
992
+ { "status": { "$in": ["active", "pending"] } }
993
+
994
+ 3. Find documents with existing email:
995
+ { "email": { "$exists": true } }
996
+
997
+ 4. Find documents with dates:
998
+ // Using ISO date string format
999
+ { "createdAt": { "$gt": "2023-01-01T00:00:00Z" } }
1000
+ // Using ISODate syntax
1001
+ { "createdAt": { "$gt": ISODate("2023-01-01T00:00:00Z") } }
1002
+
1003
+ Use these patterns to construct MongoDB queries.`
1004
+ }
1005
+ ]
1006
+ };
1007
+ }
1008
+
1009
+ // src/schemas/tools.ts
1010
+ async function handleListToolsRequest({
1011
+ request,
1012
+ client,
1013
+ db,
1014
+ isReadOnlyMode
1015
+ }) {
1016
+ return {
1017
+ tools: [
1018
+ {
1019
+ name: "query",
1020
+ description: "Execute a MongoDB query with optional execution plan analysis",
1021
+ inputSchema: {
1022
+ type: "object",
1023
+ properties: {
1024
+ collection: {
1025
+ type: "string",
1026
+ description: "Name of the collection to query"
1027
+ },
1028
+ filter: {
1029
+ type: "object",
1030
+ description: "MongoDB query filter. Supports date strings in ISO format ('2025-01-01T00:00:00Z') and ISODate('2025-01-01T00:00:00Z') notation"
1031
+ },
1032
+ projection: {
1033
+ type: "object",
1034
+ description: "Fields to include/exclude"
1035
+ },
1036
+ limit: {
1037
+ type: "number",
1038
+ description: "Maximum number of documents to return",
1039
+ default: 10
1040
+ },
1041
+ explain: {
1042
+ type: "string",
1043
+ description: "Optional: Get query execution information",
1044
+ enum: ["queryPlanner", "executionStats", "allPlansExecution"]
1045
+ },
1046
+ objectIdMode: {
1047
+ type: "string",
1048
+ description: "Control how 24-character hex strings are handled",
1049
+ enum: ["auto", "none", "force"],
1050
+ default: "auto"
1051
+ }
1052
+ },
1053
+ required: ["collection"]
47
1054
  }
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();
1055
+ },
1056
+ {
1057
+ name: "aggregate",
1058
+ description: "Execute a MongoDB aggregation pipeline with optional execution plan analysis",
1059
+ inputSchema: {
1060
+ type: "object",
1061
+ properties: {
1062
+ collection: {
1063
+ type: "string",
1064
+ description: "Name of the collection to aggregate"
1065
+ },
1066
+ pipeline: {
1067
+ type: "array",
1068
+ description: "Aggregation pipeline stages. Supports date strings in ISO format ('2025-01-01T00:00:00Z') and ISODate('2025-01-01T00:00:00Z') notation",
1069
+ items: {
1070
+ type: "object"
1071
+ }
1072
+ },
1073
+ explain: {
1074
+ type: "string",
1075
+ description: "Optional: Get aggregation execution information (queryPlanner, executionStats, or allPlansExecution)",
1076
+ enum: ["queryPlanner", "executionStats", "allPlansExecution"]
1077
+ },
1078
+ objectIdMode: {
1079
+ type: "string",
1080
+ description: "Control how 24-character hex strings are handled",
1081
+ enum: ["auto", "none", "force"],
1082
+ default: "auto"
1083
+ }
1084
+ },
1085
+ required: ["collection", "pipeline"]
58
1086
  }
59
- process.exit(1);
1087
+ },
1088
+ {
1089
+ name: "update",
1090
+ description: "Update documents in a MongoDB collection",
1091
+ inputSchema: {
1092
+ type: "object",
1093
+ properties: {
1094
+ collection: {
1095
+ type: "string",
1096
+ description: "Name of the collection to update"
1097
+ },
1098
+ filter: {
1099
+ type: "object",
1100
+ description: "Filter to select documents to update. Supports date strings in ISO format ('2025-01-01T00:00:00Z') and ISODate('2025-01-01T00:00:00Z') notation"
1101
+ },
1102
+ update: {
1103
+ type: "object",
1104
+ description: "Update operations to apply ($set, $unset, $inc, etc.)"
1105
+ },
1106
+ upsert: {
1107
+ type: "boolean",
1108
+ description: "Create a new document if no documents match the filter"
1109
+ },
1110
+ multi: {
1111
+ type: "boolean",
1112
+ description: "Update multiple documents that match the filter"
1113
+ },
1114
+ objectIdMode: {
1115
+ type: "string",
1116
+ description: "Control how 24-character hex strings are handled",
1117
+ enum: ["auto", "none", "force"],
1118
+ default: "auto"
1119
+ }
1120
+ },
1121
+ required: ["collection", "filter", "update"]
1122
+ }
1123
+ },
1124
+ {
1125
+ name: "serverInfo",
1126
+ description: "Get MongoDB server information including version, storage engine, and other details",
1127
+ inputSchema: {
1128
+ type: "object",
1129
+ properties: {
1130
+ includeDebugInfo: {
1131
+ type: "boolean",
1132
+ description: "Include additional debug information about the server"
1133
+ }
1134
+ }
1135
+ }
1136
+ },
1137
+ {
1138
+ name: "insert",
1139
+ description: "Insert one or more documents into a MongoDB collection",
1140
+ inputSchema: {
1141
+ type: "object",
1142
+ properties: {
1143
+ collection: {
1144
+ type: "string",
1145
+ description: "Name of the collection to insert into"
1146
+ },
1147
+ documents: {
1148
+ type: "array",
1149
+ description: "Array of documents to insert",
1150
+ items: { type: "object" }
1151
+ },
1152
+ ordered: {
1153
+ type: "boolean",
1154
+ description: "If true, perform ordered insert. If false, insert unordered"
1155
+ },
1156
+ writeConcern: {
1157
+ type: "object",
1158
+ description: "Write concern for the insert operation"
1159
+ },
1160
+ bypassDocumentValidation: {
1161
+ type: "boolean",
1162
+ description: "Allow insert to bypass schema validation"
1163
+ },
1164
+ objectIdMode: {
1165
+ type: "string",
1166
+ description: "Control how 24-character hex strings are handled",
1167
+ enum: ["auto", "none", "force"],
1168
+ default: "auto"
1169
+ }
1170
+ },
1171
+ required: ["collection", "documents"]
1172
+ }
1173
+ },
1174
+ {
1175
+ name: "createIndex",
1176
+ description: "Create one or more indexes on a MongoDB collection",
1177
+ inputSchema: {
1178
+ type: "object",
1179
+ properties: {
1180
+ collection: {
1181
+ type: "string",
1182
+ description: "Name of the collection to create indexes on"
1183
+ },
1184
+ indexes: {
1185
+ type: "array",
1186
+ description: "Array of index specifications",
1187
+ items: {
1188
+ type: "object",
1189
+ properties: {
1190
+ key: {
1191
+ type: "object",
1192
+ description: "Index key pattern, e.g. { field: 1 }"
1193
+ },
1194
+ name: {
1195
+ type: "string",
1196
+ description: "Optional: Name of the index"
1197
+ },
1198
+ unique: {
1199
+ type: "boolean",
1200
+ description: "Optional: Creates a unique index"
1201
+ },
1202
+ sparse: {
1203
+ type: "boolean",
1204
+ description: "Optional: Creates a sparse index"
1205
+ },
1206
+ background: {
1207
+ type: "boolean",
1208
+ description: "Optional: Builds index in background"
1209
+ },
1210
+ expireAfterSeconds: {
1211
+ type: "number",
1212
+ description: "TTL in seconds for documents"
1213
+ },
1214
+ partialFilterExpression: {
1215
+ type: "object",
1216
+ description: "Filter expression for partial indexes"
1217
+ }
1218
+ },
1219
+ required: ["key"]
1220
+ }
1221
+ },
1222
+ writeConcern: {
1223
+ type: "object",
1224
+ description: "Write concern for index creation"
1225
+ },
1226
+ commitQuorum: {
1227
+ type: "string",
1228
+ description: "Number of members required to create the index"
1229
+ },
1230
+ objectIdMode: {
1231
+ type: "string",
1232
+ description: "Control how 24-character hex strings are handled",
1233
+ enum: ["auto", "none", "force"],
1234
+ default: "auto"
1235
+ }
1236
+ },
1237
+ required: ["collection", "indexes"]
1238
+ }
1239
+ },
1240
+ {
1241
+ name: "count",
1242
+ description: "Count documents in a collection matching a query",
1243
+ inputSchema: {
1244
+ type: "object",
1245
+ properties: {
1246
+ collection: { type: "string", description: "Collection name" },
1247
+ query: { type: "object", description: "Query filter to count" },
1248
+ limit: { type: "integer", description: "Max documents to count" },
1249
+ skip: {
1250
+ type: "integer",
1251
+ description: "Docs to skip before counting"
1252
+ },
1253
+ hint: { type: "object", description: "Index hint" },
1254
+ readConcern: { type: "object", description: "Read concern option" },
1255
+ maxTimeMS: { type: "integer", description: "Max execution time" },
1256
+ collation: {
1257
+ type: "object",
1258
+ description: "Collation rules for comparison"
1259
+ },
1260
+ objectIdMode: {
1261
+ type: "string",
1262
+ description: "Control how 24-character hex strings are handled",
1263
+ enum: ["auto", "none", "force"],
1264
+ default: "auto"
1265
+ }
1266
+ },
1267
+ required: ["collection"]
1268
+ }
1269
+ },
1270
+ {
1271
+ name: "listCollections",
1272
+ description: "List all collections in the MongoDB database",
1273
+ inputSchema: {
1274
+ type: "object",
1275
+ properties: {
1276
+ nameOnly: {
1277
+ type: "boolean",
1278
+ description: "If true, return only collection names"
1279
+ },
1280
+ filter: {
1281
+ type: "object",
1282
+ description: "Filter for collections"
1283
+ },
1284
+ objectIdMode: {
1285
+ type: "string",
1286
+ description: "Control how 24-character hex strings are handled",
1287
+ enum: ["auto", "none", "force"],
1288
+ default: "auto"
1289
+ }
1290
+ }
1291
+ }
1292
+ }
1293
+ ]
1294
+ };
1295
+ }
1296
+
1297
+ // src/server.ts
1298
+ function createServer(client, db, isReadOnlyMode = false, options = {}) {
1299
+ const server = new Server(
1300
+ {
1301
+ name: "mongodb",
1302
+ version: "2.0.2",
1303
+ ...options
1304
+ },
1305
+ {
1306
+ capabilities: {
1307
+ completions: {},
1308
+ resources: {},
1309
+ tools: {},
1310
+ prompts: {}
1311
+ },
1312
+ ...options
60
1313
  }
1314
+ );
1315
+ server.setRequestHandler(
1316
+ PingRequestSchema,
1317
+ (request) => handlePingRequest({ request, client, db, isReadOnlyMode })
1318
+ );
1319
+ server.setRequestHandler(
1320
+ ListResourcesRequestSchema,
1321
+ (request) => handleListResourcesRequest({ request, client, db, isReadOnlyMode })
1322
+ );
1323
+ server.setRequestHandler(
1324
+ ReadResourceRequestSchema,
1325
+ (request) => handleReadResourceRequest({ request, client, db, isReadOnlyMode })
1326
+ );
1327
+ server.setRequestHandler(
1328
+ ListToolsRequestSchema,
1329
+ (request) => handleListToolsRequest({ request, client, db, isReadOnlyMode })
1330
+ );
1331
+ server.setRequestHandler(
1332
+ CallToolRequestSchema,
1333
+ (request) => handleCallToolRequest({ request, client, db, isReadOnlyMode })
1334
+ );
1335
+ server.setRequestHandler(
1336
+ ListPromptsRequestSchema,
1337
+ (request) => handleListPromptsRequest({ request, client, db, isReadOnlyMode })
1338
+ );
1339
+ server.setRequestHandler(
1340
+ GetPromptRequestSchema,
1341
+ (request) => handleGetPromptRequest({ request, client, db, isReadOnlyMode })
1342
+ );
1343
+ server.setRequestHandler(
1344
+ ListResourceTemplatesRequestSchema,
1345
+ (request) => handleListResourceTemplatesRequest({ request, client, db, isReadOnlyMode })
1346
+ );
1347
+ server.setRequestHandler(
1348
+ CompleteRequestSchema,
1349
+ (request) => handleCompletionRequest({ request, client, db, isReadOnlyMode })
1350
+ );
1351
+ return server;
61
1352
  }
62
- // Handle cleanup
63
- process.on("SIGINT", async () => {
1353
+
1354
+ // src/index.ts
1355
+ var mongoClient = null;
1356
+ async function main() {
1357
+ const args = process.argv.slice(2);
1358
+ let connectionUrl = "";
1359
+ let readOnlyMode = process.env.MCP_MONGODB_READONLY === "true" || false;
1360
+ let transportMode = "stdio";
1361
+ let port = Number(process.env.MCP_PORT) || 3001;
1362
+ for (let i = 0; i < args.length; i++) {
1363
+ if (args[i] === "--read-only" || args[i] === "-r") {
1364
+ readOnlyMode = true;
1365
+ } else if (args[i] === "--transport" || args[i] === "-t") {
1366
+ const value = args[++i];
1367
+ if (value !== "stdio" && value !== "http") {
1368
+ console.error("Invalid transport mode. Use 'stdio' or 'http'.");
1369
+ process.exit(1);
1370
+ }
1371
+ transportMode = value;
1372
+ } else if (args[i] === "--port" || args[i] === "-p") {
1373
+ port = Number(args[++i]);
1374
+ if (Number.isNaN(port)) {
1375
+ console.error("Invalid port number.");
1376
+ process.exit(1);
1377
+ }
1378
+ } else if (!connectionUrl) {
1379
+ connectionUrl = args[i];
1380
+ }
1381
+ }
1382
+ if (!connectionUrl) {
1383
+ connectionUrl = process.env.MCP_MONGODB_URI || "";
1384
+ }
1385
+ if (!connectionUrl) {
1386
+ console.error(
1387
+ "Please provide a MongoDB connection URL via command-line argument or MCP_MONGODB_URI environment variable"
1388
+ );
1389
+ console.error(
1390
+ "Usage: command <mongodb-url> [--read-only|-r] [--transport stdio|http] [--port 3001]"
1391
+ );
1392
+ console.error(
1393
+ " or: MCP_MONGODB_URI=<mongodb-url> [MCP_MONGODB_READONLY=true] command"
1394
+ );
1395
+ process.exit(1);
1396
+ }
1397
+ if (!connectionUrl.startsWith("mongodb://") && !connectionUrl.startsWith("mongodb+srv://")) {
1398
+ console.error(
1399
+ "Invalid MongoDB connection URL. URL must start with 'mongodb://' or 'mongodb+srv://'"
1400
+ );
1401
+ process.exit(1);
1402
+ }
1403
+ try {
1404
+ const { client, db, isConnected, isReadOnlyMode } = await connectToMongoDB(
1405
+ connectionUrl,
1406
+ readOnlyMode
1407
+ );
1408
+ mongoClient = client;
1409
+ if (!isConnected || !client || !db) {
1410
+ console.error("Failed to connect to MongoDB");
1411
+ process.exit(1);
1412
+ }
1413
+ if (transportMode === "http") {
1414
+ await startHttpServer(client, db, isReadOnlyMode, port);
1415
+ } else {
1416
+ await startStdioServer(client, db, isReadOnlyMode);
1417
+ }
1418
+ } catch (error) {
1419
+ console.error("Failed to connect to MongoDB:", error);
64
1420
  if (mongoClient) {
65
- await mongoClient.close();
1421
+ await mongoClient.close();
66
1422
  }
67
- process.exit(0);
1423
+ process.exit(1);
1424
+ }
1425
+ }
1426
+ async function startStdioServer(client, db, isReadOnlyMode) {
1427
+ const server = createServer(client, db, isReadOnlyMode);
1428
+ const transport = new StdioServerTransport();
1429
+ await server.connect(transport);
1430
+ console.warn("Server connected successfully via stdio");
1431
+ }
1432
+ async function startHttpServer(client, db, isReadOnlyMode, port) {
1433
+ const app = createMcpExpressApp({ host: "0.0.0.0" });
1434
+ app.use((req, res, next) => {
1435
+ const start = Date.now();
1436
+ const ip = req.headers["x-forwarded-for"] || req.socket.remoteAddress;
1437
+ const method = req.method;
1438
+ const url = req.url;
1439
+ let mcpMethod = "-";
1440
+ if (req.body && typeof req.body === "object" && "method" in req.body) {
1441
+ mcpMethod = req.body.method;
1442
+ }
1443
+ res.on("finish", () => {
1444
+ const duration = Date.now() - start;
1445
+ console.log(
1446
+ `${(/* @__PURE__ */ new Date()).toISOString()} | ${ip} | ${method} ${url} | ${res.statusCode} | ${duration}ms | mcp:${mcpMethod}`
1447
+ );
1448
+ });
1449
+ next();
1450
+ });
1451
+ app.post("/mcp", async (req, res) => {
1452
+ const server = createServer(client, db, isReadOnlyMode);
1453
+ try {
1454
+ const transport = new StreamableHTTPServerTransport({
1455
+ sessionIdGenerator: void 0
1456
+ });
1457
+ await server.connect(transport);
1458
+ await transport.handleRequest(req, res, req.body);
1459
+ res.on("close", () => {
1460
+ transport.close();
1461
+ server.close();
1462
+ });
1463
+ } catch (error) {
1464
+ console.error("Error handling MCP request:", error);
1465
+ if (!res.headersSent) {
1466
+ res.status(500).json({
1467
+ jsonrpc: "2.0",
1468
+ error: {
1469
+ code: -32603,
1470
+ message: "Internal server error"
1471
+ },
1472
+ id: null
1473
+ });
1474
+ }
1475
+ }
1476
+ });
1477
+ app.get("/mcp", async (_req, res) => {
1478
+ res.writeHead(405).end(
1479
+ JSON.stringify({
1480
+ jsonrpc: "2.0",
1481
+ error: {
1482
+ code: -32e3,
1483
+ message: "Method not allowed."
1484
+ },
1485
+ id: null
1486
+ })
1487
+ );
1488
+ });
1489
+ app.delete("/mcp", async (_req, res) => {
1490
+ res.writeHead(405).end(
1491
+ JSON.stringify({
1492
+ jsonrpc: "2.0",
1493
+ error: {
1494
+ code: -32e3,
1495
+ message: "Method not allowed."
1496
+ },
1497
+ id: null
1498
+ })
1499
+ );
1500
+ });
1501
+ app.listen(port, () => {
1502
+ console.log(`MCP MongoDB Streamable HTTP Server listening on port ${port}`);
1503
+ console.log(`Endpoint: http://localhost:${port}/mcp`);
1504
+ });
1505
+ }
1506
+ process.on("SIGINT", async () => {
1507
+ if (mongoClient) {
1508
+ await mongoClient.close();
1509
+ }
1510
+ process.exit(0);
68
1511
  });
69
1512
  process.on("SIGTERM", async () => {
70
- if (mongoClient) {
71
- await mongoClient.close();
72
- }
73
- process.exit(0);
1513
+ if (mongoClient) {
1514
+ await mongoClient.close();
1515
+ }
1516
+ process.exit(0);
74
1517
  });
75
1518
  main().catch((error) => {
76
- console.error("Server error:", error);
77
- process.exit(1);
1519
+ console.error("Server error:", error);
1520
+ process.exit(1);
78
1521
  });