mcp-mongo-server 2.0.2 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -2
- package/build/index.js +454 -99
- package/package.json +24 -8
package/README.md
CHANGED
|
@@ -4,8 +4,6 @@
|
|
|
4
4
|

|
|
5
5
|

|
|
6
6
|

|
|
7
|
-
[](https://smithery.ai/server/mcp-mongo-server)
|
|
8
|
-
[](https://mseep.ai/app/e274a3dd-7fe6-4440-8c43-043bae668251)
|
|
9
7
|
|
|
10
8
|
A Model Context Protocol server that enables LLMs to interact with MongoDB databases. This server provides capabilities for inspecting collection schemas and executing MongoDB operations through a standardized interface.
|
|
11
9
|
|
package/build/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
+
import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
|
|
4
5
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
6
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
6
|
-
import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
|
|
7
7
|
|
|
8
8
|
// src/mongo.ts
|
|
9
9
|
import { MongoClient, ReadPreference } from "mongodb";
|
|
@@ -32,6 +32,10 @@ async function connectToMongoDB(url, readOnly) {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
// src/server.ts
|
|
35
|
+
import {
|
|
36
|
+
InMemoryTaskMessageQueue,
|
|
37
|
+
InMemoryTaskStore
|
|
38
|
+
} from "@modelcontextprotocol/sdk/experimental/tasks";
|
|
35
39
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
36
40
|
import {
|
|
37
41
|
CallToolRequestSchema,
|
|
@@ -46,6 +50,10 @@ import {
|
|
|
46
50
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
47
51
|
|
|
48
52
|
// src/schemas/call.ts
|
|
53
|
+
import {
|
|
54
|
+
ErrorCode,
|
|
55
|
+
McpError
|
|
56
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
49
57
|
import { ObjectId } from "mongodb";
|
|
50
58
|
var COLLECTION_OPERATIONS = [
|
|
51
59
|
"query",
|
|
@@ -56,11 +64,40 @@ var COLLECTION_OPERATIONS = [
|
|
|
56
64
|
"count"
|
|
57
65
|
];
|
|
58
66
|
var WRITE_OPERATIONS = ["update", "insert", "createIndex"];
|
|
67
|
+
var READONLY_FORBIDDEN_AGG_OPERATORS = /* @__PURE__ */ new Set([
|
|
68
|
+
"$out",
|
|
69
|
+
"$merge",
|
|
70
|
+
"$function",
|
|
71
|
+
"$accumulator",
|
|
72
|
+
"$where"
|
|
73
|
+
]);
|
|
74
|
+
function findForbiddenAggOperator(value) {
|
|
75
|
+
if (Array.isArray(value)) {
|
|
76
|
+
for (const item of value) {
|
|
77
|
+
const found = findForbiddenAggOperator(item);
|
|
78
|
+
if (found) return found;
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
if (value && typeof value === "object") {
|
|
83
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
84
|
+
if (READONLY_FORBIDDEN_AGG_OPERATORS.has(key)) {
|
|
85
|
+
return key;
|
|
86
|
+
}
|
|
87
|
+
const found = findForbiddenAggOperator(nested);
|
|
88
|
+
if (found) return found;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
59
93
|
async function handleCallToolRequest({
|
|
60
94
|
request,
|
|
61
95
|
client,
|
|
62
96
|
db,
|
|
63
|
-
isReadOnlyMode
|
|
97
|
+
isReadOnlyMode,
|
|
98
|
+
signal,
|
|
99
|
+
taskStore,
|
|
100
|
+
taskTtl
|
|
64
101
|
}) {
|
|
65
102
|
const { name, arguments: args = {} } = request.params;
|
|
66
103
|
const operation = name;
|
|
@@ -76,37 +113,93 @@ async function handleCallToolRequest({
|
|
|
76
113
|
}
|
|
77
114
|
Object.assign(args, filteredArgs);
|
|
78
115
|
validateOperation(operation);
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
116
|
+
try {
|
|
117
|
+
checkReadOnlyMode(operation, isReadOnlyMode);
|
|
118
|
+
let collection = null;
|
|
119
|
+
if (COLLECTION_OPERATIONS.includes(operation)) {
|
|
120
|
+
const collectionName = args.collection;
|
|
121
|
+
if (!collectionName) {
|
|
122
|
+
throw new Error(
|
|
123
|
+
`Collection name is required for '${operation}' operation`
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
collection = db.collection(collectionName);
|
|
127
|
+
validateCollection(collection);
|
|
128
|
+
}
|
|
129
|
+
signal?.throwIfAborted();
|
|
130
|
+
if (taskStore) {
|
|
131
|
+
const task = await taskStore.createTask({
|
|
132
|
+
ttl: taskTtl ?? void 0
|
|
133
|
+
});
|
|
134
|
+
(async () => {
|
|
135
|
+
try {
|
|
136
|
+
const result = await executeOperation(
|
|
137
|
+
operation,
|
|
138
|
+
collection,
|
|
139
|
+
db,
|
|
140
|
+
isReadOnlyMode,
|
|
141
|
+
args,
|
|
142
|
+
objectIdMode,
|
|
143
|
+
signal
|
|
144
|
+
);
|
|
145
|
+
await taskStore.storeTaskResult(task.taskId, "completed", result);
|
|
146
|
+
} catch (error) {
|
|
147
|
+
try {
|
|
148
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
149
|
+
await taskStore.storeTaskResult(task.taskId, "failed", {
|
|
150
|
+
content: [{ type: "text", text: message }],
|
|
151
|
+
isError: true
|
|
152
|
+
});
|
|
153
|
+
} catch {
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
})();
|
|
157
|
+
return { task };
|
|
158
|
+
}
|
|
159
|
+
return await executeOperation(
|
|
160
|
+
operation,
|
|
161
|
+
collection,
|
|
162
|
+
db,
|
|
163
|
+
isReadOnlyMode,
|
|
164
|
+
args,
|
|
165
|
+
objectIdMode,
|
|
166
|
+
signal
|
|
167
|
+
);
|
|
168
|
+
} catch (error) {
|
|
169
|
+
if (error instanceof McpError || signal?.aborted) {
|
|
170
|
+
throw error;
|
|
87
171
|
}
|
|
88
|
-
|
|
89
|
-
|
|
172
|
+
return toolExecutionError(
|
|
173
|
+
error instanceof Error ? error.message : "Unknown error"
|
|
174
|
+
);
|
|
90
175
|
}
|
|
176
|
+
}
|
|
177
|
+
async function executeOperation(operation, collection, db, isReadOnlyMode, args, objectIdMode, signal) {
|
|
91
178
|
switch (operation) {
|
|
92
179
|
case "query":
|
|
93
|
-
return handleQuery(collection, args, objectIdMode);
|
|
180
|
+
return handleQuery(collection, args, objectIdMode, signal);
|
|
94
181
|
case "aggregate":
|
|
95
|
-
return handleAggregate(
|
|
182
|
+
return handleAggregate(
|
|
183
|
+
collection,
|
|
184
|
+
args,
|
|
185
|
+
objectIdMode,
|
|
186
|
+
isReadOnlyMode,
|
|
187
|
+
signal
|
|
188
|
+
);
|
|
96
189
|
case "update":
|
|
97
|
-
return handleUpdate(collection, args, objectIdMode);
|
|
190
|
+
return handleUpdate(collection, args, objectIdMode, signal);
|
|
98
191
|
case "serverInfo":
|
|
99
|
-
return handleServerInfo(db, isReadOnlyMode, args);
|
|
192
|
+
return handleServerInfo(db, isReadOnlyMode, args, signal);
|
|
100
193
|
case "insert":
|
|
101
|
-
return handleInsert(collection, args, objectIdMode);
|
|
194
|
+
return handleInsert(collection, args, objectIdMode, signal);
|
|
102
195
|
case "createIndex":
|
|
103
|
-
return handleCreateIndex(collection, args, objectIdMode);
|
|
196
|
+
return handleCreateIndex(collection, args, objectIdMode, signal);
|
|
104
197
|
case "count":
|
|
105
|
-
return handleCount(collection, args, objectIdMode);
|
|
198
|
+
return handleCount(collection, args, objectIdMode, signal);
|
|
106
199
|
case "listCollections":
|
|
107
|
-
return handleListCollections(db, args, objectIdMode);
|
|
200
|
+
return handleListCollections(db, args, objectIdMode, signal);
|
|
108
201
|
default:
|
|
109
|
-
throw new
|
|
202
|
+
throw new McpError(ErrorCode.InvalidParams, `Unknown tool: ${operation}`);
|
|
110
203
|
}
|
|
111
204
|
}
|
|
112
205
|
function validateOperation(operation) {
|
|
@@ -121,7 +214,7 @@ function validateOperation(operation) {
|
|
|
121
214
|
"listCollections"
|
|
122
215
|
];
|
|
123
216
|
if (!validOperations.includes(operation)) {
|
|
124
|
-
throw new
|
|
217
|
+
throw new McpError(ErrorCode.InvalidParams, `Unknown tool: ${operation}`);
|
|
125
218
|
}
|
|
126
219
|
}
|
|
127
220
|
function validateCollection(collection) {
|
|
@@ -269,40 +362,64 @@ function formatResponse(data) {
|
|
|
269
362
|
]
|
|
270
363
|
};
|
|
271
364
|
}
|
|
365
|
+
function toolExecutionError(message) {
|
|
366
|
+
return {
|
|
367
|
+
content: [{ type: "text", text: message }],
|
|
368
|
+
isError: true
|
|
369
|
+
};
|
|
370
|
+
}
|
|
272
371
|
function handleError(error, operation, collectionName) {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
throw new Error(`Failed to ${operation} ${context}: ${error.message}`);
|
|
372
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
373
|
+
throw error;
|
|
276
374
|
}
|
|
277
|
-
|
|
375
|
+
const context = collectionName ? `collection ${collectionName}` : "operation";
|
|
376
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
377
|
+
return toolExecutionError(`Failed to ${operation} ${context}: ${message}`);
|
|
278
378
|
}
|
|
279
|
-
async function handleQuery(collection, args, objectIdMode = "auto") {
|
|
379
|
+
async function handleQuery(collection, args, objectIdMode = "auto", signal) {
|
|
280
380
|
if (!collection) {
|
|
281
381
|
throw new Error("Collection is required for query operation");
|
|
282
382
|
}
|
|
283
|
-
const { filter, projection,
|
|
383
|
+
const { filter, projection, explain, sort } = args;
|
|
384
|
+
const limit = args.limit || 10;
|
|
385
|
+
const skip = args.skip || 0;
|
|
284
386
|
const queryFilter = parseFilter(filter, objectIdMode);
|
|
285
387
|
try {
|
|
286
388
|
if (explain) {
|
|
389
|
+
signal?.throwIfAborted();
|
|
287
390
|
const explainResult = await collection.find(queryFilter, {
|
|
288
391
|
projection,
|
|
289
|
-
limit
|
|
392
|
+
limit,
|
|
393
|
+
skip,
|
|
290
394
|
sort
|
|
291
395
|
}).explain(explain);
|
|
292
396
|
return formatResponse(explainResult);
|
|
293
397
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
398
|
+
signal?.throwIfAborted();
|
|
399
|
+
const [total, results] = await Promise.all([
|
|
400
|
+
collection.countDocuments(queryFilter),
|
|
401
|
+
collection.find(queryFilter, {
|
|
402
|
+
projection,
|
|
403
|
+
limit,
|
|
404
|
+
skip,
|
|
405
|
+
sort
|
|
406
|
+
}).toArray()
|
|
407
|
+
]);
|
|
408
|
+
return formatResponse({
|
|
409
|
+
results,
|
|
410
|
+
metadata: {
|
|
411
|
+
total,
|
|
412
|
+
returned: results.length,
|
|
413
|
+
skip,
|
|
414
|
+
limit,
|
|
415
|
+
hasMore: skip + results.length < total
|
|
416
|
+
}
|
|
298
417
|
});
|
|
299
|
-
const results = await cursor.toArray();
|
|
300
|
-
return formatResponse(results);
|
|
301
418
|
} catch (error) {
|
|
302
419
|
return handleError(error, "query", collection.collectionName);
|
|
303
420
|
}
|
|
304
421
|
}
|
|
305
|
-
async function handleAggregate(collection, args, objectIdMode = "auto") {
|
|
422
|
+
async function handleAggregate(collection, args, objectIdMode = "auto", isReadOnlyMode = false, signal) {
|
|
306
423
|
if (!collection) {
|
|
307
424
|
throw new Error("Collection is required for aggregate operation");
|
|
308
425
|
}
|
|
@@ -310,6 +427,14 @@ async function handleAggregate(collection, args, objectIdMode = "auto") {
|
|
|
310
427
|
if (!Array.isArray(pipeline)) {
|
|
311
428
|
throw new Error("Pipeline must be an array");
|
|
312
429
|
}
|
|
430
|
+
if (isReadOnlyMode) {
|
|
431
|
+
const forbidden = findForbiddenAggOperator(pipeline);
|
|
432
|
+
if (forbidden) {
|
|
433
|
+
throw new Error(
|
|
434
|
+
`ReadonlyError: Aggregation operator '${forbidden}' is not allowed in read-only mode`
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
313
438
|
const processedPipeline = pipeline.map((stage) => {
|
|
314
439
|
if (typeof stage === "object" && stage !== null) {
|
|
315
440
|
return processObjectIdInFilter(
|
|
@@ -321,6 +446,7 @@ async function handleAggregate(collection, args, objectIdMode = "auto") {
|
|
|
321
446
|
});
|
|
322
447
|
try {
|
|
323
448
|
if (explain) {
|
|
449
|
+
signal?.throwIfAborted();
|
|
324
450
|
const explainResult = await collection.aggregate(processedPipeline, {
|
|
325
451
|
explain: {
|
|
326
452
|
verbosity: explain
|
|
@@ -328,13 +454,19 @@ async function handleAggregate(collection, args, objectIdMode = "auto") {
|
|
|
328
454
|
}).toArray();
|
|
329
455
|
return formatResponse(explainResult);
|
|
330
456
|
}
|
|
457
|
+
signal?.throwIfAborted();
|
|
331
458
|
const results = await collection.aggregate(processedPipeline).toArray();
|
|
332
|
-
return formatResponse(
|
|
459
|
+
return formatResponse({
|
|
460
|
+
results,
|
|
461
|
+
metadata: {
|
|
462
|
+
returned: results.length
|
|
463
|
+
}
|
|
464
|
+
});
|
|
333
465
|
} catch (error) {
|
|
334
466
|
return handleError(error, "aggregate", collection.collectionName);
|
|
335
467
|
}
|
|
336
468
|
}
|
|
337
|
-
async function handleUpdate(collection, args, objectIdMode = "auto") {
|
|
469
|
+
async function handleUpdate(collection, args, objectIdMode = "auto", signal) {
|
|
338
470
|
if (!collection) {
|
|
339
471
|
throw new Error("Collection is required for update operation");
|
|
340
472
|
}
|
|
@@ -375,6 +507,7 @@ async function handleUpdate(collection, args, objectIdMode = "auto") {
|
|
|
375
507
|
multi: !!multi
|
|
376
508
|
};
|
|
377
509
|
const updateMethod = options.multi ? "updateMany" : "updateOne";
|
|
510
|
+
signal?.throwIfAborted();
|
|
378
511
|
const result = await collection[updateMethod](
|
|
379
512
|
queryFilter,
|
|
380
513
|
processedUpdate,
|
|
@@ -390,35 +523,33 @@ async function handleUpdate(collection, args, objectIdMode = "auto") {
|
|
|
390
523
|
return handleError(error, "update", collection.collectionName);
|
|
391
524
|
}
|
|
392
525
|
}
|
|
393
|
-
async function handleServerInfo(db, isReadOnlyMode, args) {
|
|
526
|
+
async function handleServerInfo(db, isReadOnlyMode, args, signal) {
|
|
394
527
|
const { includeDebugInfo } = args;
|
|
395
528
|
try {
|
|
529
|
+
signal?.throwIfAborted();
|
|
396
530
|
const buildInfo = await db.command({ buildInfo: 1 });
|
|
397
|
-
let serverStatus = null;
|
|
398
|
-
if (includeDebugInfo) {
|
|
399
|
-
serverStatus = await db.command({ serverStatus: 1 });
|
|
400
|
-
}
|
|
401
531
|
const serverInfo = {
|
|
402
532
|
version: buildInfo.version,
|
|
403
533
|
gitVersion: buildInfo.gitVersion,
|
|
404
|
-
modules: buildInfo.modules,
|
|
405
|
-
allocator: buildInfo.allocator,
|
|
406
|
-
javascriptEngine: buildInfo.javascriptEngine,
|
|
407
|
-
sysInfo: buildInfo.sysInfo,
|
|
408
534
|
storageEngines: buildInfo.storageEngines,
|
|
409
|
-
debug: buildInfo.debug,
|
|
410
535
|
maxBsonObjectSize: buildInfo.maxBsonObjectSize,
|
|
411
|
-
openssl: buildInfo.openssl,
|
|
412
|
-
buildEnvironment: buildInfo.buildEnvironment,
|
|
413
536
|
bits: buildInfo.bits,
|
|
414
537
|
ok: buildInfo.ok,
|
|
415
|
-
status: {},
|
|
416
538
|
connectionInfo: {
|
|
417
539
|
readOnlyMode: isReadOnlyMode,
|
|
418
540
|
readPreference: isReadOnlyMode ? "secondary" : "primary"
|
|
419
541
|
}
|
|
420
542
|
};
|
|
421
|
-
if (
|
|
543
|
+
if (includeDebugInfo) {
|
|
544
|
+
signal?.throwIfAborted();
|
|
545
|
+
const serverStatus = await db.command({ serverStatus: 1 });
|
|
546
|
+
serverInfo.modules = buildInfo.modules;
|
|
547
|
+
serverInfo.allocator = buildInfo.allocator;
|
|
548
|
+
serverInfo.javascriptEngine = buildInfo.javascriptEngine;
|
|
549
|
+
serverInfo.sysInfo = buildInfo.sysInfo;
|
|
550
|
+
serverInfo.debug = buildInfo.debug;
|
|
551
|
+
serverInfo.openssl = buildInfo.openssl;
|
|
552
|
+
serverInfo.buildEnvironment = buildInfo.buildEnvironment;
|
|
422
553
|
serverInfo.status = {
|
|
423
554
|
host: serverStatus.host,
|
|
424
555
|
version: serverStatus.version,
|
|
@@ -440,7 +571,7 @@ async function handleServerInfo(db, isReadOnlyMode, args) {
|
|
|
440
571
|
return handleError(error, "get server information");
|
|
441
572
|
}
|
|
442
573
|
}
|
|
443
|
-
async function handleInsert(collection, args, objectIdMode = "auto") {
|
|
574
|
+
async function handleInsert(collection, args, objectIdMode = "auto", signal) {
|
|
444
575
|
if (!collection) {
|
|
445
576
|
throw new Error("Collection is required for insert operation");
|
|
446
577
|
}
|
|
@@ -466,6 +597,7 @@ async function handleInsert(collection, args, objectIdMode = "auto") {
|
|
|
466
597
|
writeConcern,
|
|
467
598
|
bypassDocumentValidation
|
|
468
599
|
};
|
|
600
|
+
signal?.throwIfAborted();
|
|
469
601
|
const result = await collection.insertMany(
|
|
470
602
|
processedDocuments,
|
|
471
603
|
options
|
|
@@ -488,7 +620,7 @@ async function handleInsert(collection, args, objectIdMode = "auto") {
|
|
|
488
620
|
return handleError(error, "insert", collection.collectionName);
|
|
489
621
|
}
|
|
490
622
|
}
|
|
491
|
-
async function handleCreateIndex(collection, args, objectIdMode = "auto") {
|
|
623
|
+
async function handleCreateIndex(collection, args, objectIdMode = "auto", signal) {
|
|
492
624
|
if (!collection) {
|
|
493
625
|
throw new Error("Collection is required for createIndex operation");
|
|
494
626
|
}
|
|
@@ -517,6 +649,7 @@ async function handleCreateIndex(collection, args, objectIdMode = "auto") {
|
|
|
517
649
|
const indexOptions = {
|
|
518
650
|
commitQuorum: typeof commitQuorum === "number" ? commitQuorum : void 0
|
|
519
651
|
};
|
|
652
|
+
signal?.throwIfAborted();
|
|
520
653
|
const result = await collection.createIndexes(
|
|
521
654
|
processedIndexes,
|
|
522
655
|
indexOptions
|
|
@@ -531,7 +664,7 @@ async function handleCreateIndex(collection, args, objectIdMode = "auto") {
|
|
|
531
664
|
return handleError(error, "create indexes", collection.collectionName);
|
|
532
665
|
}
|
|
533
666
|
}
|
|
534
|
-
async function handleCount(collection, args, objectIdMode = "auto") {
|
|
667
|
+
async function handleCount(collection, args, objectIdMode = "auto", signal) {
|
|
535
668
|
if (!collection) {
|
|
536
669
|
throw new Error("Collection is required for count operation");
|
|
537
670
|
}
|
|
@@ -551,6 +684,7 @@ async function handleCount(collection, args, objectIdMode = "auto") {
|
|
|
551
684
|
delete options[key];
|
|
552
685
|
}
|
|
553
686
|
}
|
|
687
|
+
signal?.throwIfAborted();
|
|
554
688
|
const count = await collection.countDocuments(countQuery, options);
|
|
555
689
|
return formatResponse({
|
|
556
690
|
count,
|
|
@@ -560,8 +694,10 @@ async function handleCount(collection, args, objectIdMode = "auto") {
|
|
|
560
694
|
return handleError(error, "count documents", collection.collectionName);
|
|
561
695
|
}
|
|
562
696
|
}
|
|
563
|
-
async function handleListCollections(db, args, objectIdMode = "auto") {
|
|
697
|
+
async function handleListCollections(db, args, objectIdMode = "auto", signal) {
|
|
564
698
|
const { nameOnly, filter } = args;
|
|
699
|
+
const skip = args.skip || 0;
|
|
700
|
+
const limit = args.limit || 20;
|
|
565
701
|
let processedFilter = filter;
|
|
566
702
|
if (filter && typeof filter === "object") {
|
|
567
703
|
processedFilter = processObjectIdInFilter(
|
|
@@ -571,20 +707,58 @@ async function handleListCollections(db, args, objectIdMode = "auto") {
|
|
|
571
707
|
}
|
|
572
708
|
try {
|
|
573
709
|
const options = processedFilter ? { filter: processedFilter } : {};
|
|
710
|
+
signal?.throwIfAborted();
|
|
574
711
|
const collections = await db.listCollections(options).toArray();
|
|
575
|
-
const
|
|
576
|
-
|
|
712
|
+
const allResults = nameOnly ? collections.map((collection) => collection.name) : collections;
|
|
713
|
+
const total = allResults.length;
|
|
714
|
+
const paged = allResults.slice(skip, skip + limit);
|
|
715
|
+
return formatResponse({
|
|
716
|
+
results: paged,
|
|
717
|
+
metadata: {
|
|
718
|
+
total,
|
|
719
|
+
returned: paged.length,
|
|
720
|
+
skip,
|
|
721
|
+
limit,
|
|
722
|
+
hasMore: skip + paged.length < total
|
|
723
|
+
}
|
|
724
|
+
});
|
|
577
725
|
} catch (error) {
|
|
578
726
|
return handleError(error, "list collections");
|
|
579
727
|
}
|
|
580
728
|
}
|
|
581
729
|
|
|
730
|
+
// src/utils/pagination.ts
|
|
731
|
+
var DEFAULT_PAGE_SIZE = 20;
|
|
732
|
+
function encodeCursor(offset) {
|
|
733
|
+
const data = { offset };
|
|
734
|
+
return Buffer.from(JSON.stringify(data)).toString("base64");
|
|
735
|
+
}
|
|
736
|
+
function decodeCursor(cursor) {
|
|
737
|
+
if (!cursor) return 0;
|
|
738
|
+
try {
|
|
739
|
+
const data = JSON.parse(
|
|
740
|
+
Buffer.from(cursor, "base64").toString("utf-8")
|
|
741
|
+
);
|
|
742
|
+
return typeof data.offset === "number" && data.offset >= 0 ? data.offset : 0;
|
|
743
|
+
} catch {
|
|
744
|
+
return 0;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
function paginate(items, cursor, pageSize = DEFAULT_PAGE_SIZE) {
|
|
748
|
+
const offset = decodeCursor(cursor);
|
|
749
|
+
const page = items.slice(offset, offset + pageSize);
|
|
750
|
+
const nextOffset = offset + pageSize;
|
|
751
|
+
const nextCursor = nextOffset < items.length ? encodeCursor(nextOffset) : void 0;
|
|
752
|
+
return { items: page, nextCursor };
|
|
753
|
+
}
|
|
754
|
+
|
|
582
755
|
// src/schemas/completion.ts
|
|
583
756
|
async function handleCompletionRequest({
|
|
584
757
|
request,
|
|
585
758
|
client,
|
|
586
759
|
db,
|
|
587
|
-
isReadOnlyMode
|
|
760
|
+
isReadOnlyMode,
|
|
761
|
+
signal
|
|
588
762
|
}) {
|
|
589
763
|
const { ref, argument } = request.params;
|
|
590
764
|
if (ref.type === "ref/prompt") {
|
|
@@ -593,7 +767,8 @@ async function handleCompletionRequest({
|
|
|
593
767
|
db,
|
|
594
768
|
isReadOnlyMode,
|
|
595
769
|
ref.name,
|
|
596
|
-
argument
|
|
770
|
+
argument,
|
|
771
|
+
signal
|
|
597
772
|
);
|
|
598
773
|
}
|
|
599
774
|
if (ref.type === "ref/resource") {
|
|
@@ -602,34 +777,46 @@ async function handleCompletionRequest({
|
|
|
602
777
|
db,
|
|
603
778
|
isReadOnlyMode,
|
|
604
779
|
ref.uri,
|
|
605
|
-
argument
|
|
780
|
+
argument,
|
|
781
|
+
signal
|
|
606
782
|
);
|
|
607
783
|
}
|
|
608
784
|
return emptyCompletionResult();
|
|
609
785
|
}
|
|
610
|
-
async function handlePromptCompletion(client, db, isReadOnlyMode, promptName, argument) {
|
|
786
|
+
async function handlePromptCompletion(client, db, isReadOnlyMode, promptName, argument, signal) {
|
|
611
787
|
if (!promptName) {
|
|
612
788
|
return emptyCompletionResult();
|
|
613
789
|
}
|
|
614
790
|
if (argument.name === "collection") {
|
|
615
|
-
return await completeCollectionNames(
|
|
791
|
+
return await completeCollectionNames(
|
|
792
|
+
argument.value,
|
|
793
|
+
db,
|
|
794
|
+
isReadOnlyMode,
|
|
795
|
+
signal
|
|
796
|
+
);
|
|
616
797
|
}
|
|
617
798
|
return emptyCompletionResult();
|
|
618
799
|
}
|
|
619
|
-
async function handleResourceCompletion(client, db, isReadOnlyMode, promptName, argument) {
|
|
800
|
+
async function handleResourceCompletion(client, db, isReadOnlyMode, promptName, argument, signal) {
|
|
620
801
|
if (!promptName) {
|
|
621
802
|
return emptyCompletionResult();
|
|
622
803
|
}
|
|
623
804
|
if (argument.name === "collection") {
|
|
624
|
-
return await completeCollectionNames(
|
|
805
|
+
return await completeCollectionNames(
|
|
806
|
+
argument.value,
|
|
807
|
+
db,
|
|
808
|
+
isReadOnlyMode,
|
|
809
|
+
signal
|
|
810
|
+
);
|
|
625
811
|
}
|
|
626
812
|
return emptyCompletionResult();
|
|
627
813
|
}
|
|
628
|
-
async function completeCollectionNames(partialValue, db, isReadOnlyMode) {
|
|
814
|
+
async function completeCollectionNames(partialValue, db, isReadOnlyMode, signal) {
|
|
629
815
|
try {
|
|
630
816
|
console.warn(
|
|
631
817
|
`Completing collection names with partial value: ${partialValue}`
|
|
632
818
|
);
|
|
819
|
+
signal?.throwIfAborted();
|
|
633
820
|
const collections = await db.listCollections().toArray();
|
|
634
821
|
const matchingCollections = collections.map(
|
|
635
822
|
(c) => c.name
|
|
@@ -641,14 +828,17 @@ async function completeCollectionNames(partialValue, db, isReadOnlyMode) {
|
|
|
641
828
|
)
|
|
642
829
|
).sort();
|
|
643
830
|
console.warn(`Found ${matchingCollections.length} matching collections`);
|
|
644
|
-
const
|
|
645
|
-
const limitedResults =
|
|
646
|
-
|
|
831
|
+
const PAGE_SIZE = 100;
|
|
832
|
+
const { items: limitedResults, nextCursor } = paginate(
|
|
833
|
+
matchingCollections,
|
|
834
|
+
void 0,
|
|
835
|
+
PAGE_SIZE
|
|
836
|
+
);
|
|
647
837
|
return {
|
|
648
838
|
completion: {
|
|
649
839
|
values: limitedResults,
|
|
650
840
|
total: matchingCollections.length,
|
|
651
|
-
hasMore
|
|
841
|
+
hasMore: !!nextCursor
|
|
652
842
|
}
|
|
653
843
|
};
|
|
654
844
|
} catch (error) {
|
|
@@ -671,12 +861,14 @@ async function handlePingRequest({
|
|
|
671
861
|
request,
|
|
672
862
|
client,
|
|
673
863
|
db,
|
|
674
|
-
isReadOnlyMode
|
|
864
|
+
isReadOnlyMode,
|
|
865
|
+
signal
|
|
675
866
|
}) {
|
|
676
867
|
try {
|
|
677
868
|
if (!client) {
|
|
678
869
|
throw new Error("MongoDB connection is not available");
|
|
679
870
|
}
|
|
871
|
+
signal?.throwIfAborted();
|
|
680
872
|
const pong = await db.command({ ping: 1 });
|
|
681
873
|
if (pong.ok !== 1) {
|
|
682
874
|
throw new Error(`MongoDB ping failed: ${pong.errmsg}`);
|
|
@@ -695,7 +887,8 @@ async function handleListPromptsRequest({
|
|
|
695
887
|
request,
|
|
696
888
|
client,
|
|
697
889
|
db,
|
|
698
|
-
isReadOnlyMode
|
|
890
|
+
isReadOnlyMode,
|
|
891
|
+
signal
|
|
699
892
|
}) {
|
|
700
893
|
return {
|
|
701
894
|
prompts: [
|
|
@@ -717,7 +910,9 @@ async function handleGetPromptRequest({
|
|
|
717
910
|
request,
|
|
718
911
|
client,
|
|
719
912
|
db,
|
|
720
|
-
isReadOnlyMode
|
|
913
|
+
isReadOnlyMode,
|
|
914
|
+
signal,
|
|
915
|
+
sendProgress
|
|
721
916
|
}) {
|
|
722
917
|
const { name, arguments: args = {} } = request.params;
|
|
723
918
|
if (name !== "analyze_collection") {
|
|
@@ -732,8 +927,14 @@ async function handleGetPromptRequest({
|
|
|
732
927
|
if (collection.collectionName.startsWith("system.")) {
|
|
733
928
|
throw new Error("Access to system collections is not allowed");
|
|
734
929
|
}
|
|
930
|
+
await sendProgress?.(1, 3, "Getting schema");
|
|
931
|
+
signal?.throwIfAborted();
|
|
735
932
|
const schemaSample = await collection.findOne({});
|
|
933
|
+
await sendProgress?.(2, 3, "Getting stats");
|
|
934
|
+
signal?.throwIfAborted();
|
|
736
935
|
const stats = await collection.aggregate([{ $collStats: { count: {} } }]).toArray();
|
|
936
|
+
await sendProgress?.(3, 3, "Fetching samples");
|
|
937
|
+
signal?.throwIfAborted();
|
|
737
938
|
const sampleDocs = await collection.find({}).limit(5).toArray();
|
|
738
939
|
const documentCount = stats[0]?.count ?? "unknown";
|
|
739
940
|
return {
|
|
@@ -864,7 +1065,9 @@ async function handleReadResourceRequest({
|
|
|
864
1065
|
request,
|
|
865
1066
|
client,
|
|
866
1067
|
db,
|
|
867
|
-
isReadOnlyMode
|
|
1068
|
+
isReadOnlyMode,
|
|
1069
|
+
signal,
|
|
1070
|
+
sendProgress
|
|
868
1071
|
}) {
|
|
869
1072
|
const url = new URL(request.params.uri);
|
|
870
1073
|
const collectionName = url.pathname.replace(/^\//, "");
|
|
@@ -872,18 +1075,25 @@ async function handleReadResourceRequest({
|
|
|
872
1075
|
const collection = db.collection(collectionName);
|
|
873
1076
|
const sampleSize = 100;
|
|
874
1077
|
let sampleDocuments = [];
|
|
1078
|
+
await sendProgress?.(1, 4, "Sampling documents");
|
|
875
1079
|
try {
|
|
1080
|
+
signal?.throwIfAborted();
|
|
876
1081
|
sampleDocuments = await collection.aggregate([{ $sample: { size: sampleSize } }]).toArray();
|
|
877
1082
|
} catch (sampleError) {
|
|
878
1083
|
console.warn(
|
|
879
1084
|
`$sample aggregation failed for ${collectionName}, falling back to sequential scan: ${sampleError}`
|
|
880
1085
|
);
|
|
1086
|
+
signal?.throwIfAborted();
|
|
881
1087
|
sampleDocuments = await collection.find({}).limit(sampleSize).toArray();
|
|
882
1088
|
}
|
|
1089
|
+
await sendProgress?.(2, 4, "Getting indexes");
|
|
1090
|
+
signal?.throwIfAborted();
|
|
883
1091
|
const indexes = await collection.indexes();
|
|
884
1092
|
const inferredSchema = inferSchemaFromSamples(sampleDocuments);
|
|
1093
|
+
await sendProgress?.(3, 4, "Counting documents");
|
|
885
1094
|
let documentCount = null;
|
|
886
1095
|
try {
|
|
1096
|
+
signal?.throwIfAborted();
|
|
887
1097
|
documentCount = await Promise.race([
|
|
888
1098
|
collection.countDocuments(),
|
|
889
1099
|
new Promise(
|
|
@@ -898,12 +1108,14 @@ async function handleReadResourceRequest({
|
|
|
898
1108
|
`Count operation failed or timed out for ${collectionName}: ${countError}`
|
|
899
1109
|
);
|
|
900
1110
|
try {
|
|
1111
|
+
signal?.throwIfAborted();
|
|
901
1112
|
const stats = await db.command({ collStats: collectionName });
|
|
902
1113
|
documentCount = stats.count;
|
|
903
1114
|
} catch {
|
|
904
1115
|
documentCount = "unknown (count operation timed out)";
|
|
905
1116
|
}
|
|
906
1117
|
}
|
|
1118
|
+
await sendProgress?.(4, 4, "Done");
|
|
907
1119
|
const schema = {
|
|
908
1120
|
type: "collection",
|
|
909
1121
|
name: collectionName,
|
|
@@ -940,17 +1152,23 @@ async function handleListResourcesRequest({
|
|
|
940
1152
|
request,
|
|
941
1153
|
client,
|
|
942
1154
|
db,
|
|
943
|
-
isReadOnlyMode
|
|
1155
|
+
isReadOnlyMode,
|
|
1156
|
+
signal
|
|
944
1157
|
}) {
|
|
945
1158
|
try {
|
|
1159
|
+
signal?.throwIfAborted();
|
|
946
1160
|
const collections = await db.listCollections().toArray();
|
|
1161
|
+
const allResources = collections.map((collection) => ({
|
|
1162
|
+
uri: `mongodb:///${collection.name}`,
|
|
1163
|
+
mimeType: "application/json",
|
|
1164
|
+
name: collection.name,
|
|
1165
|
+
description: `MongoDB collection: ${collection.name}`
|
|
1166
|
+
}));
|
|
1167
|
+
const cursor = request.params?.cursor;
|
|
1168
|
+
const { items, nextCursor } = paginate(allResources, cursor);
|
|
947
1169
|
return {
|
|
948
|
-
resources:
|
|
949
|
-
|
|
950
|
-
mimeType: "application/json",
|
|
951
|
-
name: collection.name,
|
|
952
|
-
description: `MongoDB collection: ${collection.name}`
|
|
953
|
-
}))
|
|
1170
|
+
resources: items,
|
|
1171
|
+
...nextCursor ? { nextCursor } : {}
|
|
954
1172
|
};
|
|
955
1173
|
} catch (error) {
|
|
956
1174
|
if (error instanceof Error) {
|
|
@@ -965,7 +1183,8 @@ async function handleListResourceTemplatesRequest({
|
|
|
965
1183
|
request,
|
|
966
1184
|
client,
|
|
967
1185
|
db,
|
|
968
|
-
isReadOnlyMode
|
|
1186
|
+
isReadOnlyMode,
|
|
1187
|
+
signal
|
|
969
1188
|
}) {
|
|
970
1189
|
return {
|
|
971
1190
|
resourceTemplates: [
|
|
@@ -1011,13 +1230,15 @@ async function handleListToolsRequest({
|
|
|
1011
1230
|
request,
|
|
1012
1231
|
client,
|
|
1013
1232
|
db,
|
|
1014
|
-
isReadOnlyMode
|
|
1233
|
+
isReadOnlyMode,
|
|
1234
|
+
signal
|
|
1015
1235
|
}) {
|
|
1016
1236
|
return {
|
|
1017
1237
|
tools: [
|
|
1018
1238
|
{
|
|
1019
1239
|
name: "query",
|
|
1020
1240
|
description: "Execute a MongoDB query with optional execution plan analysis",
|
|
1241
|
+
execution: { taskSupport: "optional" },
|
|
1021
1242
|
inputSchema: {
|
|
1022
1243
|
type: "object",
|
|
1023
1244
|
properties: {
|
|
@@ -1038,6 +1259,11 @@ async function handleListToolsRequest({
|
|
|
1038
1259
|
description: "Maximum number of documents to return",
|
|
1039
1260
|
default: 10
|
|
1040
1261
|
},
|
|
1262
|
+
skip: {
|
|
1263
|
+
type: "number",
|
|
1264
|
+
description: "Number of documents to skip before returning results",
|
|
1265
|
+
default: 0
|
|
1266
|
+
},
|
|
1041
1267
|
explain: {
|
|
1042
1268
|
type: "string",
|
|
1043
1269
|
description: "Optional: Get query execution information",
|
|
@@ -1056,6 +1282,7 @@ async function handleListToolsRequest({
|
|
|
1056
1282
|
{
|
|
1057
1283
|
name: "aggregate",
|
|
1058
1284
|
description: "Execute a MongoDB aggregation pipeline with optional execution plan analysis",
|
|
1285
|
+
execution: { taskSupport: "optional" },
|
|
1059
1286
|
inputSchema: {
|
|
1060
1287
|
type: "object",
|
|
1061
1288
|
properties: {
|
|
@@ -1088,6 +1315,7 @@ async function handleListToolsRequest({
|
|
|
1088
1315
|
{
|
|
1089
1316
|
name: "update",
|
|
1090
1317
|
description: "Update documents in a MongoDB collection",
|
|
1318
|
+
execution: { taskSupport: "optional" },
|
|
1091
1319
|
inputSchema: {
|
|
1092
1320
|
type: "object",
|
|
1093
1321
|
properties: {
|
|
@@ -1124,6 +1352,7 @@ async function handleListToolsRequest({
|
|
|
1124
1352
|
{
|
|
1125
1353
|
name: "serverInfo",
|
|
1126
1354
|
description: "Get MongoDB server information including version, storage engine, and other details",
|
|
1355
|
+
execution: { taskSupport: "optional" },
|
|
1127
1356
|
inputSchema: {
|
|
1128
1357
|
type: "object",
|
|
1129
1358
|
properties: {
|
|
@@ -1137,6 +1366,7 @@ async function handleListToolsRequest({
|
|
|
1137
1366
|
{
|
|
1138
1367
|
name: "insert",
|
|
1139
1368
|
description: "Insert one or more documents into a MongoDB collection",
|
|
1369
|
+
execution: { taskSupport: "optional" },
|
|
1140
1370
|
inputSchema: {
|
|
1141
1371
|
type: "object",
|
|
1142
1372
|
properties: {
|
|
@@ -1174,6 +1404,7 @@ async function handleListToolsRequest({
|
|
|
1174
1404
|
{
|
|
1175
1405
|
name: "createIndex",
|
|
1176
1406
|
description: "Create one or more indexes on a MongoDB collection",
|
|
1407
|
+
execution: { taskSupport: "optional" },
|
|
1177
1408
|
inputSchema: {
|
|
1178
1409
|
type: "object",
|
|
1179
1410
|
properties: {
|
|
@@ -1240,6 +1471,7 @@ async function handleListToolsRequest({
|
|
|
1240
1471
|
{
|
|
1241
1472
|
name: "count",
|
|
1242
1473
|
description: "Count documents in a collection matching a query",
|
|
1474
|
+
execution: { taskSupport: "optional" },
|
|
1243
1475
|
inputSchema: {
|
|
1244
1476
|
type: "object",
|
|
1245
1477
|
properties: {
|
|
@@ -1270,6 +1502,7 @@ async function handleListToolsRequest({
|
|
|
1270
1502
|
{
|
|
1271
1503
|
name: "listCollections",
|
|
1272
1504
|
description: "List all collections in the MongoDB database",
|
|
1505
|
+
execution: { taskSupport: "optional" },
|
|
1273
1506
|
inputSchema: {
|
|
1274
1507
|
type: "object",
|
|
1275
1508
|
properties: {
|
|
@@ -1281,6 +1514,16 @@ async function handleListToolsRequest({
|
|
|
1281
1514
|
type: "object",
|
|
1282
1515
|
description: "Filter for collections"
|
|
1283
1516
|
},
|
|
1517
|
+
skip: {
|
|
1518
|
+
type: "number",
|
|
1519
|
+
description: "Number of collections to skip before returning results",
|
|
1520
|
+
default: 0
|
|
1521
|
+
},
|
|
1522
|
+
limit: {
|
|
1523
|
+
type: "number",
|
|
1524
|
+
description: "Maximum number of collections to return",
|
|
1525
|
+
default: 20
|
|
1526
|
+
},
|
|
1284
1527
|
objectIdMode: {
|
|
1285
1528
|
type: "string",
|
|
1286
1529
|
description: "Control how 24-character hex strings are handled",
|
|
@@ -1295,11 +1538,24 @@ async function handleListToolsRequest({
|
|
|
1295
1538
|
}
|
|
1296
1539
|
|
|
1297
1540
|
// src/server.ts
|
|
1541
|
+
function createSendProgress(extra) {
|
|
1542
|
+
const progressToken = extra._meta?.progressToken;
|
|
1543
|
+
if (progressToken === void 0) return void 0;
|
|
1544
|
+
return (progress, total, message) => extra.sendNotification({
|
|
1545
|
+
method: "notifications/progress",
|
|
1546
|
+
params: { progressToken, progress, total, message }
|
|
1547
|
+
});
|
|
1548
|
+
}
|
|
1298
1549
|
function createServer(client, db, isReadOnlyMode = false, options = {}) {
|
|
1550
|
+
const taskStore = new InMemoryTaskStore();
|
|
1551
|
+
const taskMessageQueue = new InMemoryTaskMessageQueue();
|
|
1299
1552
|
const server = new Server(
|
|
1300
1553
|
{
|
|
1301
1554
|
name: "mongodb",
|
|
1302
|
-
|
|
1555
|
+
title: "MongoDB MCP Server",
|
|
1556
|
+
version: "2.1.1",
|
|
1557
|
+
description: "MCP server for MongoDB: query, aggregate, and manage collections with read-only mode, progress notifications, cancellation, and task support",
|
|
1558
|
+
websiteUrl: "https://github.com/kiliczsh/mcp-mongo-server",
|
|
1303
1559
|
...options
|
|
1304
1560
|
},
|
|
1305
1561
|
{
|
|
@@ -1307,46 +1563,111 @@ function createServer(client, db, isReadOnlyMode = false, options = {}) {
|
|
|
1307
1563
|
completions: {},
|
|
1308
1564
|
resources: {},
|
|
1309
1565
|
tools: {},
|
|
1310
|
-
prompts: {}
|
|
1566
|
+
prompts: {},
|
|
1567
|
+
tasks: {
|
|
1568
|
+
list: {},
|
|
1569
|
+
cancel: {},
|
|
1570
|
+
requests: { tools: { call: {} } }
|
|
1571
|
+
}
|
|
1311
1572
|
},
|
|
1573
|
+
taskStore,
|
|
1574
|
+
taskMessageQueue,
|
|
1312
1575
|
...options
|
|
1313
1576
|
}
|
|
1314
1577
|
);
|
|
1315
1578
|
server.setRequestHandler(
|
|
1316
1579
|
PingRequestSchema,
|
|
1317
|
-
(request) => handlePingRequest({
|
|
1580
|
+
(request, extra) => handlePingRequest({
|
|
1581
|
+
request,
|
|
1582
|
+
client,
|
|
1583
|
+
db,
|
|
1584
|
+
isReadOnlyMode,
|
|
1585
|
+
signal: extra.signal
|
|
1586
|
+
})
|
|
1318
1587
|
);
|
|
1319
1588
|
server.setRequestHandler(
|
|
1320
1589
|
ListResourcesRequestSchema,
|
|
1321
|
-
(request) => handleListResourcesRequest({
|
|
1590
|
+
(request, extra) => handleListResourcesRequest({
|
|
1591
|
+
request,
|
|
1592
|
+
client,
|
|
1593
|
+
db,
|
|
1594
|
+
isReadOnlyMode,
|
|
1595
|
+
signal: extra.signal
|
|
1596
|
+
})
|
|
1322
1597
|
);
|
|
1323
1598
|
server.setRequestHandler(
|
|
1324
1599
|
ReadResourceRequestSchema,
|
|
1325
|
-
(request) => handleReadResourceRequest({
|
|
1600
|
+
(request, extra) => handleReadResourceRequest({
|
|
1601
|
+
request,
|
|
1602
|
+
client,
|
|
1603
|
+
db,
|
|
1604
|
+
isReadOnlyMode,
|
|
1605
|
+
signal: extra.signal,
|
|
1606
|
+
sendProgress: createSendProgress(extra)
|
|
1607
|
+
})
|
|
1326
1608
|
);
|
|
1327
1609
|
server.setRequestHandler(
|
|
1328
1610
|
ListToolsRequestSchema,
|
|
1329
|
-
(request) => handleListToolsRequest({
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1611
|
+
(request, extra) => handleListToolsRequest({
|
|
1612
|
+
request,
|
|
1613
|
+
client,
|
|
1614
|
+
db,
|
|
1615
|
+
isReadOnlyMode,
|
|
1616
|
+
signal: extra.signal
|
|
1617
|
+
})
|
|
1334
1618
|
);
|
|
1619
|
+
server.setRequestHandler(CallToolRequestSchema, (request, extra) => {
|
|
1620
|
+
const hasTaskParams = !!request.params.task;
|
|
1621
|
+
return handleCallToolRequest({
|
|
1622
|
+
request,
|
|
1623
|
+
client,
|
|
1624
|
+
db,
|
|
1625
|
+
isReadOnlyMode,
|
|
1626
|
+
signal: extra.signal,
|
|
1627
|
+
taskStore: hasTaskParams ? extra.taskStore : void 0,
|
|
1628
|
+
taskTtl: hasTaskParams ? extra.taskRequestedTtl : void 0
|
|
1629
|
+
});
|
|
1630
|
+
});
|
|
1335
1631
|
server.setRequestHandler(
|
|
1336
1632
|
ListPromptsRequestSchema,
|
|
1337
|
-
(request) => handleListPromptsRequest({
|
|
1633
|
+
(request, extra) => handleListPromptsRequest({
|
|
1634
|
+
request,
|
|
1635
|
+
client,
|
|
1636
|
+
db,
|
|
1637
|
+
isReadOnlyMode,
|
|
1638
|
+
signal: extra.signal
|
|
1639
|
+
})
|
|
1338
1640
|
);
|
|
1339
1641
|
server.setRequestHandler(
|
|
1340
1642
|
GetPromptRequestSchema,
|
|
1341
|
-
(request) => handleGetPromptRequest({
|
|
1643
|
+
(request, extra) => handleGetPromptRequest({
|
|
1644
|
+
request,
|
|
1645
|
+
client,
|
|
1646
|
+
db,
|
|
1647
|
+
isReadOnlyMode,
|
|
1648
|
+
signal: extra.signal,
|
|
1649
|
+
sendProgress: createSendProgress(extra)
|
|
1650
|
+
})
|
|
1342
1651
|
);
|
|
1343
1652
|
server.setRequestHandler(
|
|
1344
1653
|
ListResourceTemplatesRequestSchema,
|
|
1345
|
-
(request) => handleListResourceTemplatesRequest({
|
|
1654
|
+
(request, extra) => handleListResourceTemplatesRequest({
|
|
1655
|
+
request,
|
|
1656
|
+
client,
|
|
1657
|
+
db,
|
|
1658
|
+
isReadOnlyMode,
|
|
1659
|
+
signal: extra.signal
|
|
1660
|
+
})
|
|
1346
1661
|
);
|
|
1347
1662
|
server.setRequestHandler(
|
|
1348
1663
|
CompleteRequestSchema,
|
|
1349
|
-
(request) => handleCompletionRequest({
|
|
1664
|
+
(request, extra) => handleCompletionRequest({
|
|
1665
|
+
request,
|
|
1666
|
+
client,
|
|
1667
|
+
db,
|
|
1668
|
+
isReadOnlyMode,
|
|
1669
|
+
signal: extra.signal
|
|
1670
|
+
})
|
|
1350
1671
|
);
|
|
1351
1672
|
return server;
|
|
1352
1673
|
}
|
|
@@ -1359,6 +1680,7 @@ async function main() {
|
|
|
1359
1680
|
let readOnlyMode = process.env.MCP_MONGODB_READONLY === "true" || false;
|
|
1360
1681
|
let transportMode = "stdio";
|
|
1361
1682
|
let port = Number(process.env.MCP_PORT) || 3001;
|
|
1683
|
+
const allowedOrigins = (process.env.MCP_HTTP_ALLOWED_ORIGINS || "").split(",").map((origin) => origin.trim()).filter(Boolean);
|
|
1362
1684
|
for (let i = 0; i < args.length; i++) {
|
|
1363
1685
|
if (args[i] === "--read-only" || args[i] === "-r") {
|
|
1364
1686
|
readOnlyMode = true;
|
|
@@ -1375,6 +1697,10 @@ async function main() {
|
|
|
1375
1697
|
console.error("Invalid port number.");
|
|
1376
1698
|
process.exit(1);
|
|
1377
1699
|
}
|
|
1700
|
+
} else if (args[i] === "--allowed-origins") {
|
|
1701
|
+
allowedOrigins.push(
|
|
1702
|
+
...(args[++i] || "").split(",").map((origin) => origin.trim()).filter(Boolean)
|
|
1703
|
+
);
|
|
1378
1704
|
} else if (!connectionUrl) {
|
|
1379
1705
|
connectionUrl = args[i];
|
|
1380
1706
|
}
|
|
@@ -1411,7 +1737,7 @@ async function main() {
|
|
|
1411
1737
|
process.exit(1);
|
|
1412
1738
|
}
|
|
1413
1739
|
if (transportMode === "http") {
|
|
1414
|
-
await startHttpServer(client, db, isReadOnlyMode, port);
|
|
1740
|
+
await startHttpServer(client, db, isReadOnlyMode, port, allowedOrigins);
|
|
1415
1741
|
} else {
|
|
1416
1742
|
await startStdioServer(client, db, isReadOnlyMode);
|
|
1417
1743
|
}
|
|
@@ -1429,7 +1755,21 @@ async function startStdioServer(client, db, isReadOnlyMode) {
|
|
|
1429
1755
|
await server.connect(transport);
|
|
1430
1756
|
console.warn("Server connected successfully via stdio");
|
|
1431
1757
|
}
|
|
1432
|
-
|
|
1758
|
+
function isOriginAllowed(origin, allowedOrigins) {
|
|
1759
|
+
if (!origin) {
|
|
1760
|
+
return true;
|
|
1761
|
+
}
|
|
1762
|
+
if (allowedOrigins.includes(origin)) {
|
|
1763
|
+
return true;
|
|
1764
|
+
}
|
|
1765
|
+
try {
|
|
1766
|
+
const { hostname } = new URL(origin);
|
|
1767
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
|
|
1768
|
+
} catch {
|
|
1769
|
+
return false;
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
async function startHttpServer(client, db, isReadOnlyMode, port, allowedOrigins) {
|
|
1433
1773
|
const app = createMcpExpressApp({ host: "0.0.0.0" });
|
|
1434
1774
|
app.use((req, res, next) => {
|
|
1435
1775
|
const start = Date.now();
|
|
@@ -1448,6 +1788,21 @@ async function startHttpServer(client, db, isReadOnlyMode, port) {
|
|
|
1448
1788
|
});
|
|
1449
1789
|
next();
|
|
1450
1790
|
});
|
|
1791
|
+
app.use((req, res, next) => {
|
|
1792
|
+
const origin = req.headers.origin;
|
|
1793
|
+
if (!isOriginAllowed(origin, allowedOrigins)) {
|
|
1794
|
+
res.status(403).json({
|
|
1795
|
+
jsonrpc: "2.0",
|
|
1796
|
+
error: {
|
|
1797
|
+
code: -32603,
|
|
1798
|
+
message: "Forbidden: origin not allowed"
|
|
1799
|
+
},
|
|
1800
|
+
id: null
|
|
1801
|
+
});
|
|
1802
|
+
return;
|
|
1803
|
+
}
|
|
1804
|
+
next();
|
|
1805
|
+
});
|
|
1451
1806
|
app.post("/mcp", async (req, res) => {
|
|
1452
1807
|
const server = createServer(client, db, isReadOnlyMode);
|
|
1453
1808
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-mongo-server",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"description": "A Model Context Protocol server for MongoDB connections",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -23,14 +23,14 @@
|
|
|
23
23
|
"inspector": "mcp-inspector --config inspector-config.json --server mongodb"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
27
|
-
"mongodb": "^7.
|
|
28
|
-
"zod": "^
|
|
26
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
27
|
+
"mongodb": "^7.5.0",
|
|
28
|
+
"zod": "^4.4.3"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
|
-
"@biomejs/biome": "^2.
|
|
32
|
-
"@modelcontextprotocol/inspector": "^0.
|
|
33
|
-
"@types/node": "^
|
|
31
|
+
"@biomejs/biome": "^2.5.5",
|
|
32
|
+
"@modelcontextprotocol/inspector": "^1.0.0",
|
|
33
|
+
"@types/node": "^26.1.1",
|
|
34
34
|
"tsup": "^8.5.1",
|
|
35
35
|
"typescript": "^5.9.3"
|
|
36
36
|
},
|
|
@@ -56,5 +56,21 @@
|
|
|
56
56
|
"type": "git",
|
|
57
57
|
"url": "https://github.com/kiliczsh/mcp-mongo-server"
|
|
58
58
|
},
|
|
59
|
-
"homepage": "https://github.com/kiliczsh/mcp-mongo-server"
|
|
59
|
+
"homepage": "https://github.com/kiliczsh/mcp-mongo-server",
|
|
60
|
+
"overrides": {
|
|
61
|
+
"qs": "^6.15.2",
|
|
62
|
+
"hono": "^4.12.18",
|
|
63
|
+
"@hono/node-server": "^1.19.10",
|
|
64
|
+
"express-rate-limit": "^8.2.2",
|
|
65
|
+
"fast-uri": "^3.1.2",
|
|
66
|
+
"ws": "^8.21.1",
|
|
67
|
+
"shell-quote": "^1.10.0",
|
|
68
|
+
"minimatch": "^3.1.3",
|
|
69
|
+
"brace-expansion": "^1.1.13",
|
|
70
|
+
"rollup": "^4.59.0",
|
|
71
|
+
"picomatch": "^4.0.4",
|
|
72
|
+
"body-parser": "^2.3.0",
|
|
73
|
+
"ip-address": "^10.2.0",
|
|
74
|
+
"esbuild": "^0.28.1"
|
|
75
|
+
}
|
|
60
76
|
}
|