mcp-mongo-server 1.1.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +276 -118
- package/build/index.js +44 -1023
- package/build/mongo.js +33 -0
- package/build/schemas/call.js +507 -0
- package/build/schemas/completion.js +112 -0
- package/build/schemas/ping.js +20 -0
- package/build/schemas/prompts.js +71 -0
- package/build/schemas/resource.js +62 -0
- package/build/schemas/templates.js +33 -0
- package/build/schemas/tools.js +278 -0
- package/build/server.js +65 -0
- package/package.json +1 -1
- package/build/types.js +0 -169
package/build/mongo.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { MongoClient, ReadPreference } from "mongodb";
|
|
2
|
+
/**
|
|
3
|
+
* Initialize MongoDB connection
|
|
4
|
+
* @param url MongoDB connection string
|
|
5
|
+
* @param readOnly Whether to connect in read-only mode
|
|
6
|
+
* @returns Object containing client, db, connection status, and read-only mode
|
|
7
|
+
*/
|
|
8
|
+
export async function connectToMongoDB(url, readOnly) {
|
|
9
|
+
try {
|
|
10
|
+
const options = readOnly
|
|
11
|
+
? { readPreference: ReadPreference.SECONDARY }
|
|
12
|
+
: {};
|
|
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
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
console.error("Failed to connect to MongoDB:", error);
|
|
26
|
+
return {
|
|
27
|
+
client: null,
|
|
28
|
+
db: null,
|
|
29
|
+
isConnected: false,
|
|
30
|
+
isReadOnlyMode: readOnly,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
import { ObjectId } from "mongodb";
|
|
2
|
+
// Define operations that require a collection
|
|
3
|
+
const COLLECTION_OPERATIONS = [
|
|
4
|
+
"query",
|
|
5
|
+
"aggregate",
|
|
6
|
+
"update",
|
|
7
|
+
"insert",
|
|
8
|
+
"createIndex",
|
|
9
|
+
"count",
|
|
10
|
+
];
|
|
11
|
+
// Define write operations that are blocked in read-only mode
|
|
12
|
+
const WRITE_OPERATIONS = ["update", "insert", "createIndex"];
|
|
13
|
+
export async function handleCallToolRequest({ request, client, db, isReadOnlyMode, }) {
|
|
14
|
+
const { name, arguments: args = {} } = request.params;
|
|
15
|
+
const operation = name;
|
|
16
|
+
// Extract ObjectId conversion mode from args (default to 'auto')
|
|
17
|
+
const objectIdMode = args.objectIdMode || "auto";
|
|
18
|
+
// Create new args object without objectIdMode property
|
|
19
|
+
const filteredArgs = {};
|
|
20
|
+
for (const [key, value] of Object.entries(args)) {
|
|
21
|
+
if (key !== "objectIdMode") {
|
|
22
|
+
filteredArgs[key] = value;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
// Replace the original args with the filtered version
|
|
26
|
+
Object.assign(args, filteredArgs);
|
|
27
|
+
// Validate operation name
|
|
28
|
+
validateOperation(operation);
|
|
29
|
+
// Check if operation is allowed in read-only mode
|
|
30
|
+
checkReadOnlyMode(operation, isReadOnlyMode);
|
|
31
|
+
// Get collection only if the operation requires it
|
|
32
|
+
let collection = null;
|
|
33
|
+
if (COLLECTION_OPERATIONS.includes(operation)) {
|
|
34
|
+
const collectionName = args.collection;
|
|
35
|
+
if (!collectionName) {
|
|
36
|
+
throw new Error(`Collection name is required for '${operation}' operation`);
|
|
37
|
+
}
|
|
38
|
+
collection = db.collection(collectionName);
|
|
39
|
+
// Validate collection
|
|
40
|
+
validateCollection(collection);
|
|
41
|
+
}
|
|
42
|
+
// Route to the appropriate handler based on operation name
|
|
43
|
+
switch (operation) {
|
|
44
|
+
case "query":
|
|
45
|
+
return handleQuery(collection, args, objectIdMode);
|
|
46
|
+
case "aggregate":
|
|
47
|
+
return handleAggregate(collection, args, objectIdMode);
|
|
48
|
+
case "update":
|
|
49
|
+
return handleUpdate(collection, args, objectIdMode);
|
|
50
|
+
case "serverInfo":
|
|
51
|
+
return handleServerInfo(db, isReadOnlyMode, args);
|
|
52
|
+
case "insert":
|
|
53
|
+
return handleInsert(collection, args, objectIdMode);
|
|
54
|
+
case "createIndex":
|
|
55
|
+
return handleCreateIndex(collection, args, objectIdMode);
|
|
56
|
+
case "count":
|
|
57
|
+
return handleCount(collection, args, objectIdMode);
|
|
58
|
+
case "listCollections":
|
|
59
|
+
return handleListCollections(db, args, objectIdMode);
|
|
60
|
+
default:
|
|
61
|
+
throw new Error(`Unknown operation: ${operation}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// Helper functions
|
|
65
|
+
function validateOperation(operation) {
|
|
66
|
+
const validOperations = [
|
|
67
|
+
"query",
|
|
68
|
+
"aggregate",
|
|
69
|
+
"update",
|
|
70
|
+
"serverInfo",
|
|
71
|
+
"insert",
|
|
72
|
+
"createIndex",
|
|
73
|
+
"count",
|
|
74
|
+
"listCollections",
|
|
75
|
+
];
|
|
76
|
+
if (!validOperations.includes(operation)) {
|
|
77
|
+
throw new Error(`Unknown operation: ${operation}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function validateCollection(collection) {
|
|
81
|
+
if (!collection.collectionName) {
|
|
82
|
+
throw new Error("Collection name cannot be empty");
|
|
83
|
+
}
|
|
84
|
+
if (collection.collectionName.startsWith("system.")) {
|
|
85
|
+
throw new Error("Access to system collections is not allowed");
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function checkReadOnlyMode(operation, isReadOnlyMode) {
|
|
89
|
+
if (isReadOnlyMode && WRITE_OPERATIONS.includes(operation)) {
|
|
90
|
+
throw new Error(`ReadonlyError: Operation '${operation}' is not allowed in read-only mode`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function parseFilter(filter, objectIdMode = "auto") {
|
|
94
|
+
if (!filter) {
|
|
95
|
+
return {};
|
|
96
|
+
}
|
|
97
|
+
if (typeof filter === "string") {
|
|
98
|
+
try {
|
|
99
|
+
return processObjectIdInFilter(JSON.parse(filter), objectIdMode);
|
|
100
|
+
}
|
|
101
|
+
catch (e) {
|
|
102
|
+
throw new Error("Invalid filter format: must be a valid JSON object");
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (typeof filter === "object" && filter !== null && !Array.isArray(filter)) {
|
|
106
|
+
// Process the filter to convert potential ObjectId strings
|
|
107
|
+
return processObjectIdInFilter(filter, objectIdMode);
|
|
108
|
+
}
|
|
109
|
+
throw new Error("Query filter must be a plain object or ObjectId");
|
|
110
|
+
}
|
|
111
|
+
// Helper function to check if a field should be treated as an ObjectId based on its name
|
|
112
|
+
function isObjectIdField(fieldName) {
|
|
113
|
+
// Convert field name to lowercase for case-insensitive comparison
|
|
114
|
+
const lowerFieldName = fieldName.toLowerCase();
|
|
115
|
+
// Consider fields like _id, id, xxxId, xxx_id as potential ObjectId fields
|
|
116
|
+
return (lowerFieldName === "_id" ||
|
|
117
|
+
lowerFieldName === "id" ||
|
|
118
|
+
lowerFieldName.endsWith("id") ||
|
|
119
|
+
lowerFieldName.endsWith("_id"));
|
|
120
|
+
}
|
|
121
|
+
// Helper function to process potential ObjectId strings in filters
|
|
122
|
+
function processObjectIdInFilter(filter, objectIdMode = "auto") {
|
|
123
|
+
// If objectIdMode is "none", don't convert any strings to ObjectIds
|
|
124
|
+
if (objectIdMode === "none") {
|
|
125
|
+
return filter;
|
|
126
|
+
}
|
|
127
|
+
const result = {};
|
|
128
|
+
for (const [key, value] of Object.entries(filter)) {
|
|
129
|
+
if (typeof value === "string" && isObjectIdString(value)) {
|
|
130
|
+
// Convert string to ObjectId if either:
|
|
131
|
+
// 1. objectIdMode is "force" (convert all 24-char hex strings)
|
|
132
|
+
// 2. objectIdMode is "auto" AND the field name suggests it's an ObjectId
|
|
133
|
+
if (objectIdMode === "force" ||
|
|
134
|
+
(objectIdMode === "auto" && isObjectIdField(key))) {
|
|
135
|
+
result[key] = new ObjectId(value);
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
result[key] = value;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
else if (typeof value === "object" && value !== null) {
|
|
142
|
+
if (Array.isArray(value)) {
|
|
143
|
+
// For arrays, apply the same logic to each item
|
|
144
|
+
if (objectIdMode === "force" ||
|
|
145
|
+
(objectIdMode === "auto" && isObjectIdField(key))) {
|
|
146
|
+
result[key] = value.map((item) => typeof item === "string" && isObjectIdString(item)
|
|
147
|
+
? new ObjectId(item)
|
|
148
|
+
: item);
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
result[key] = value;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
// Process nested objects
|
|
156
|
+
result[key] = processObjectIdInFilter(value, objectIdMode);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
result[key] = value;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return result;
|
|
164
|
+
}
|
|
165
|
+
// Helper function to check if a string appears to be an ObjectId
|
|
166
|
+
function isObjectIdString(str) {
|
|
167
|
+
// MongoDB ObjectId is typically a 24-character hex string
|
|
168
|
+
return /^[0-9a-fA-F]{24}$/.test(str);
|
|
169
|
+
}
|
|
170
|
+
function formatResponse(data) {
|
|
171
|
+
return {
|
|
172
|
+
content: [
|
|
173
|
+
{
|
|
174
|
+
type: "text",
|
|
175
|
+
text: JSON.stringify(data, null, 2),
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function handleError(error, operation, collectionName) {
|
|
181
|
+
const context = collectionName ? `collection ${collectionName}` : "operation";
|
|
182
|
+
if (error instanceof Error) {
|
|
183
|
+
throw new Error(`Failed to ${operation} ${context}: ${error.message}`);
|
|
184
|
+
}
|
|
185
|
+
throw new Error(`Failed to ${operation} ${context}: Unknown error`);
|
|
186
|
+
}
|
|
187
|
+
// Operation handlers
|
|
188
|
+
async function handleQuery(collection, args, objectIdMode = "auto") {
|
|
189
|
+
if (!collection) {
|
|
190
|
+
throw new Error("Collection is required for query operation");
|
|
191
|
+
}
|
|
192
|
+
const { filter, projection, limit, explain } = args;
|
|
193
|
+
const queryFilter = parseFilter(filter, objectIdMode);
|
|
194
|
+
try {
|
|
195
|
+
if (explain) {
|
|
196
|
+
const explainResult = await collection
|
|
197
|
+
.find(queryFilter, {
|
|
198
|
+
projection,
|
|
199
|
+
limit: limit || 100,
|
|
200
|
+
})
|
|
201
|
+
.explain(explain);
|
|
202
|
+
return formatResponse(explainResult);
|
|
203
|
+
}
|
|
204
|
+
const cursor = collection.find(queryFilter, {
|
|
205
|
+
projection,
|
|
206
|
+
limit: limit || 100,
|
|
207
|
+
});
|
|
208
|
+
const results = await cursor.toArray();
|
|
209
|
+
return formatResponse(results);
|
|
210
|
+
}
|
|
211
|
+
catch (error) {
|
|
212
|
+
return handleError(error, "query", collection.collectionName);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
async function handleAggregate(collection, args, objectIdMode = "auto") {
|
|
216
|
+
if (!collection) {
|
|
217
|
+
throw new Error("Collection is required for aggregate operation");
|
|
218
|
+
}
|
|
219
|
+
const { pipeline, explain } = args;
|
|
220
|
+
if (!Array.isArray(pipeline)) {
|
|
221
|
+
throw new Error("Pipeline must be an array");
|
|
222
|
+
}
|
|
223
|
+
// Process any ObjectId strings in the pipeline
|
|
224
|
+
const processedPipeline = pipeline.map((stage) => {
|
|
225
|
+
if (typeof stage === "object" && stage !== null) {
|
|
226
|
+
return processObjectIdInFilter(stage, objectIdMode);
|
|
227
|
+
}
|
|
228
|
+
return stage;
|
|
229
|
+
});
|
|
230
|
+
try {
|
|
231
|
+
if (explain) {
|
|
232
|
+
const explainResult = await collection
|
|
233
|
+
.aggregate(processedPipeline, {
|
|
234
|
+
explain: {
|
|
235
|
+
verbosity: explain,
|
|
236
|
+
},
|
|
237
|
+
})
|
|
238
|
+
.toArray();
|
|
239
|
+
return formatResponse(explainResult);
|
|
240
|
+
}
|
|
241
|
+
const results = await collection.aggregate(processedPipeline).toArray();
|
|
242
|
+
return formatResponse(results);
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
return handleError(error, "aggregate", collection.collectionName);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
async function handleUpdate(collection, args, objectIdMode = "auto") {
|
|
249
|
+
if (!collection) {
|
|
250
|
+
throw new Error("Collection is required for update operation");
|
|
251
|
+
}
|
|
252
|
+
const { filter, update, upsert, multi } = args;
|
|
253
|
+
const queryFilter = parseFilter(filter, objectIdMode);
|
|
254
|
+
// Process update object for potential ObjectId strings
|
|
255
|
+
let processedUpdate = update;
|
|
256
|
+
if (update && typeof update === "object" && !Array.isArray(update)) {
|
|
257
|
+
processedUpdate = processObjectIdInFilter(update, objectIdMode);
|
|
258
|
+
}
|
|
259
|
+
// Validate update operations
|
|
260
|
+
if (!processedUpdate ||
|
|
261
|
+
typeof processedUpdate !== "object" ||
|
|
262
|
+
Array.isArray(processedUpdate)) {
|
|
263
|
+
throw new Error("Update must be a valid MongoDB update document");
|
|
264
|
+
}
|
|
265
|
+
// Check if update operations use valid operators
|
|
266
|
+
const validUpdateOperators = [
|
|
267
|
+
"$set",
|
|
268
|
+
"$unset",
|
|
269
|
+
"$inc",
|
|
270
|
+
"$push",
|
|
271
|
+
"$pull",
|
|
272
|
+
"$addToSet",
|
|
273
|
+
"$pop",
|
|
274
|
+
"$rename",
|
|
275
|
+
"$mul",
|
|
276
|
+
];
|
|
277
|
+
const hasValidOperator = Object.keys(processedUpdate).some((key) => validUpdateOperators.includes(key));
|
|
278
|
+
if (!hasValidOperator) {
|
|
279
|
+
throw new Error("Update must include at least one valid update operator ($set, $unset, etc.)");
|
|
280
|
+
}
|
|
281
|
+
try {
|
|
282
|
+
const options = {
|
|
283
|
+
upsert: !!upsert,
|
|
284
|
+
multi: !!multi,
|
|
285
|
+
};
|
|
286
|
+
// Use updateOne or updateMany based on multi option
|
|
287
|
+
const updateMethod = options.multi ? "updateMany" : "updateOne";
|
|
288
|
+
const result = await collection[updateMethod](queryFilter, processedUpdate, options);
|
|
289
|
+
return formatResponse({
|
|
290
|
+
matchedCount: result.matchedCount,
|
|
291
|
+
modifiedCount: result.modifiedCount,
|
|
292
|
+
upsertedCount: result.upsertedCount,
|
|
293
|
+
upsertedId: result.upsertedId,
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
catch (error) {
|
|
297
|
+
return handleError(error, "update", collection.collectionName);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
async function handleServerInfo(db, isReadOnlyMode, args) {
|
|
301
|
+
const { includeDebugInfo } = args;
|
|
302
|
+
try {
|
|
303
|
+
// Get basic server information using buildInfo command
|
|
304
|
+
const buildInfo = await db.command({ buildInfo: 1 });
|
|
305
|
+
// Get additional server status if debug info is requested
|
|
306
|
+
let serverStatus = null;
|
|
307
|
+
if (includeDebugInfo) {
|
|
308
|
+
serverStatus = await db.command({ serverStatus: 1 });
|
|
309
|
+
}
|
|
310
|
+
// Construct the response
|
|
311
|
+
const serverInfo = {
|
|
312
|
+
version: buildInfo.version,
|
|
313
|
+
gitVersion: buildInfo.gitVersion,
|
|
314
|
+
modules: buildInfo.modules,
|
|
315
|
+
allocator: buildInfo.allocator,
|
|
316
|
+
javascriptEngine: buildInfo.javascriptEngine,
|
|
317
|
+
sysInfo: buildInfo.sysInfo,
|
|
318
|
+
storageEngines: buildInfo.storageEngines,
|
|
319
|
+
debug: buildInfo.debug,
|
|
320
|
+
maxBsonObjectSize: buildInfo.maxBsonObjectSize,
|
|
321
|
+
openssl: buildInfo.openssl,
|
|
322
|
+
buildEnvironment: buildInfo.buildEnvironment,
|
|
323
|
+
bits: buildInfo.bits,
|
|
324
|
+
ok: buildInfo.ok,
|
|
325
|
+
status: {},
|
|
326
|
+
connectionInfo: {
|
|
327
|
+
readOnlyMode: isReadOnlyMode,
|
|
328
|
+
readPreference: isReadOnlyMode ? "secondary" : "primary",
|
|
329
|
+
},
|
|
330
|
+
};
|
|
331
|
+
// Add server status information if requested
|
|
332
|
+
if (serverStatus) {
|
|
333
|
+
serverInfo.status = {
|
|
334
|
+
host: serverStatus.host,
|
|
335
|
+
version: serverStatus.version,
|
|
336
|
+
process: serverStatus.process,
|
|
337
|
+
pid: serverStatus.pid,
|
|
338
|
+
uptime: serverStatus.uptime,
|
|
339
|
+
uptimeMillis: serverStatus.uptimeMillis,
|
|
340
|
+
uptimeEstimate: serverStatus.uptimeEstimate,
|
|
341
|
+
localTime: serverStatus.localTime,
|
|
342
|
+
connections: serverStatus.connections,
|
|
343
|
+
network: serverStatus.network,
|
|
344
|
+
memory: serverStatus.mem,
|
|
345
|
+
storageEngine: serverStatus.storageEngine,
|
|
346
|
+
security: serverStatus.security,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
return formatResponse(serverInfo);
|
|
350
|
+
}
|
|
351
|
+
catch (error) {
|
|
352
|
+
return handleError(error, "get server information");
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
async function handleInsert(collection, args, objectIdMode = "auto") {
|
|
356
|
+
if (!collection) {
|
|
357
|
+
throw new Error("Collection is required for insert operation");
|
|
358
|
+
}
|
|
359
|
+
const { documents, ordered, writeConcern, bypassDocumentValidation } = args;
|
|
360
|
+
// Validate documents array
|
|
361
|
+
if (!Array.isArray(documents)) {
|
|
362
|
+
throw new Error("Documents must be an array");
|
|
363
|
+
}
|
|
364
|
+
if (documents.length === 0) {
|
|
365
|
+
throw new Error("Documents array cannot be empty");
|
|
366
|
+
}
|
|
367
|
+
if (!documents.every((doc) => doc && typeof doc === "object" && !Array.isArray(doc))) {
|
|
368
|
+
throw new Error("Each document must be a valid MongoDB document object");
|
|
369
|
+
}
|
|
370
|
+
// Process ObjectId strings in documents
|
|
371
|
+
const processedDocuments = documents.map((doc) => processObjectIdInFilter(doc, objectIdMode));
|
|
372
|
+
try {
|
|
373
|
+
// Type the options object correctly for BulkWriteOptions
|
|
374
|
+
const options = {
|
|
375
|
+
ordered: ordered !== false, // default to true if not specified
|
|
376
|
+
writeConcern: writeConcern,
|
|
377
|
+
bypassDocumentValidation: bypassDocumentValidation,
|
|
378
|
+
};
|
|
379
|
+
// Use insertMany for consistency, it works for single documents too
|
|
380
|
+
const result = await collection.insertMany(processedDocuments, options);
|
|
381
|
+
return formatResponse({
|
|
382
|
+
acknowledged: result.acknowledged,
|
|
383
|
+
insertedCount: result.insertedCount,
|
|
384
|
+
insertedIds: result.insertedIds,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
catch (error) {
|
|
388
|
+
// Handle bulk write errors specially to provide more detail
|
|
389
|
+
if (error instanceof Error && error.name === "BulkWriteError") {
|
|
390
|
+
const bulkError = error;
|
|
391
|
+
return formatResponse({
|
|
392
|
+
error: "Bulk write error occurred",
|
|
393
|
+
writeErrors: bulkError.writeErrors,
|
|
394
|
+
insertedCount: bulkError.result?.nInserted || 0,
|
|
395
|
+
failedCount: bulkError.result?.nFailedInserts || 0,
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
return handleError(error, "insert", collection.collectionName);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
async function handleCreateIndex(collection, args, objectIdMode = "auto") {
|
|
402
|
+
if (!collection) {
|
|
403
|
+
throw new Error("Collection is required for createIndex operation");
|
|
404
|
+
}
|
|
405
|
+
const { indexes, commitQuorum, writeConcern } = args;
|
|
406
|
+
// Validate indexes array
|
|
407
|
+
if (!Array.isArray(indexes) || indexes.length === 0) {
|
|
408
|
+
throw new Error("Indexes must be a non-empty array");
|
|
409
|
+
}
|
|
410
|
+
// Validate writeConcern
|
|
411
|
+
if (writeConcern &&
|
|
412
|
+
(typeof writeConcern !== "object" || Array.isArray(writeConcern))) {
|
|
413
|
+
throw new Error("Write concern must be a valid MongoDB write concern object");
|
|
414
|
+
}
|
|
415
|
+
// Validate commitQuorum
|
|
416
|
+
if (commitQuorum &&
|
|
417
|
+
typeof commitQuorum !== "string" &&
|
|
418
|
+
typeof commitQuorum !== "number") {
|
|
419
|
+
throw new Error("Commit quorum must be a string or number");
|
|
420
|
+
}
|
|
421
|
+
// Process ObjectId strings in indexes
|
|
422
|
+
const processedIndexes = indexes.map((index) => {
|
|
423
|
+
if (index && typeof index === "object") {
|
|
424
|
+
return processObjectIdInFilter(index, objectIdMode);
|
|
425
|
+
}
|
|
426
|
+
return index;
|
|
427
|
+
});
|
|
428
|
+
try {
|
|
429
|
+
// Properly type createIndexes options
|
|
430
|
+
const indexOptions = {
|
|
431
|
+
commitQuorum: typeof commitQuorum === "number" ? commitQuorum : undefined,
|
|
432
|
+
};
|
|
433
|
+
const result = await collection.createIndexes(processedIndexes, indexOptions);
|
|
434
|
+
// Type assertion for createIndexes result
|
|
435
|
+
return formatResponse({
|
|
436
|
+
acknowledged: result.acknowledged,
|
|
437
|
+
createdIndexes: result.createdIndexes,
|
|
438
|
+
numIndexesBefore: result
|
|
439
|
+
.numIndexesBefore,
|
|
440
|
+
numIndexesAfter: result
|
|
441
|
+
.numIndexesAfter,
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
catch (error) {
|
|
445
|
+
return handleError(error, "create indexes", collection.collectionName);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
async function handleCount(collection, args, objectIdMode = "auto") {
|
|
449
|
+
if (!collection) {
|
|
450
|
+
throw new Error("Collection is required for count operation");
|
|
451
|
+
}
|
|
452
|
+
const { query, limit, skip, hint, readConcern, maxTimeMS, collation } = args;
|
|
453
|
+
const countQuery = parseFilter(query, objectIdMode);
|
|
454
|
+
try {
|
|
455
|
+
// Build options object, removing undefined values
|
|
456
|
+
const options = {
|
|
457
|
+
limit: typeof limit === "number" ? limit : undefined,
|
|
458
|
+
skip: typeof skip === "number" ? skip : undefined,
|
|
459
|
+
hint: typeof hint === "object" && hint !== null
|
|
460
|
+
? hint
|
|
461
|
+
: undefined,
|
|
462
|
+
readConcern: typeof readConcern === "object" && readConcern !== null
|
|
463
|
+
? readConcern
|
|
464
|
+
: undefined,
|
|
465
|
+
maxTimeMS: typeof maxTimeMS === "number" ? maxTimeMS : undefined,
|
|
466
|
+
collation: typeof collation === "object" && collation !== null
|
|
467
|
+
? collation
|
|
468
|
+
: undefined,
|
|
469
|
+
};
|
|
470
|
+
// Remove undefined options
|
|
471
|
+
for (const key of Object.keys(options)) {
|
|
472
|
+
if (options[key] === undefined) {
|
|
473
|
+
delete options[key];
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
// Execute count operation
|
|
477
|
+
const count = await collection.countDocuments(countQuery, options);
|
|
478
|
+
return formatResponse({
|
|
479
|
+
count,
|
|
480
|
+
ok: 1,
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
catch (error) {
|
|
484
|
+
return handleError(error, "count documents", collection.collectionName);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
async function handleListCollections(db, args, objectIdMode = "auto") {
|
|
488
|
+
const { nameOnly, filter } = args;
|
|
489
|
+
// Process ObjectId strings in filter if present
|
|
490
|
+
let processedFilter = filter;
|
|
491
|
+
if (filter && typeof filter === "object") {
|
|
492
|
+
processedFilter = processObjectIdInFilter(filter, objectIdMode);
|
|
493
|
+
}
|
|
494
|
+
try {
|
|
495
|
+
// Get the list of collections
|
|
496
|
+
const options = processedFilter ? { filter: processedFilter } : {};
|
|
497
|
+
const collections = await db.listCollections(options).toArray();
|
|
498
|
+
// If nameOnly is true, return only the collection names
|
|
499
|
+
const result = nameOnly
|
|
500
|
+
? collections.map((collection) => collection.name)
|
|
501
|
+
: collections;
|
|
502
|
+
return formatResponse(result);
|
|
503
|
+
}
|
|
504
|
+
catch (error) {
|
|
505
|
+
return handleError(error, "list collections");
|
|
506
|
+
}
|
|
507
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handles completion requests from the Model Context Protocol
|
|
3
|
+
* @param options The request options
|
|
4
|
+
* @returns Completion result with matching values
|
|
5
|
+
*/
|
|
6
|
+
export async function handleCompletionRequest({ request, client, db, isReadOnlyMode, }) {
|
|
7
|
+
const { ref, argument } = request.params;
|
|
8
|
+
// Handle completions for prompts
|
|
9
|
+
if (ref.type === "ref/prompt") {
|
|
10
|
+
return handlePromptCompletion(client, db, isReadOnlyMode, ref.name, argument);
|
|
11
|
+
}
|
|
12
|
+
// Handle completions for resources
|
|
13
|
+
if (ref.type === "ref/resource") {
|
|
14
|
+
return handleResourceCompletion(client, db, isReadOnlyMode, ref.uri, argument);
|
|
15
|
+
}
|
|
16
|
+
// Default empty response if reference type is not supported
|
|
17
|
+
return emptyCompletionResult();
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Handle completion requests for prompts
|
|
21
|
+
* @param client MongoDB client
|
|
22
|
+
* @param db MongoDB database
|
|
23
|
+
* @param isReadOnlyMode Whether the database is in read-only mode
|
|
24
|
+
* @param promptName Name of the prompt
|
|
25
|
+
* @param argument Argument being completed
|
|
26
|
+
* @returns Completion result
|
|
27
|
+
*/
|
|
28
|
+
async function handlePromptCompletion(client, db, isReadOnlyMode, promptName, argument) {
|
|
29
|
+
if (!promptName) {
|
|
30
|
+
return emptyCompletionResult();
|
|
31
|
+
}
|
|
32
|
+
// Handle collection name completions
|
|
33
|
+
if (argument.name === "collection") {
|
|
34
|
+
return await completeCollectionNames(argument.value, db, isReadOnlyMode);
|
|
35
|
+
}
|
|
36
|
+
// Add other prompt completions here as needed
|
|
37
|
+
return emptyCompletionResult();
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Handle completion requests for resources
|
|
41
|
+
* @param client MongoDB client
|
|
42
|
+
* @param db MongoDB database
|
|
43
|
+
* @param isReadOnlyMode Whether the database is in read-only mode
|
|
44
|
+
* @param uri Resource URI
|
|
45
|
+
* @param argument Argument being completed
|
|
46
|
+
* @returns Completion result
|
|
47
|
+
*/
|
|
48
|
+
async function handleResourceCompletion(client, db, isReadOnlyMode, promptName, argument) {
|
|
49
|
+
if (!promptName) {
|
|
50
|
+
return emptyCompletionResult();
|
|
51
|
+
}
|
|
52
|
+
// Handle collection name completions
|
|
53
|
+
if (argument.name === "collection") {
|
|
54
|
+
return await completeCollectionNames(argument.value, db, isReadOnlyMode);
|
|
55
|
+
}
|
|
56
|
+
// Add other prompt completions here as needed
|
|
57
|
+
return emptyCompletionResult();
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Get collection name completions for a partial value
|
|
61
|
+
* @param partialValue Partial collection name
|
|
62
|
+
* @param db MongoDB database
|
|
63
|
+
* @param isReadOnlyMode Whether the database is in read-only mode
|
|
64
|
+
* @returns Completion result with matching collection names
|
|
65
|
+
*/
|
|
66
|
+
async function completeCollectionNames(partialValue, db, isReadOnlyMode) {
|
|
67
|
+
try {
|
|
68
|
+
console.warn(`Completing collection names with partial value: ${partialValue}`);
|
|
69
|
+
// Get list of collections
|
|
70
|
+
const collections = await db.listCollections().toArray();
|
|
71
|
+
// Filter collections by partial value (case insensitive)
|
|
72
|
+
const matchingCollections = collections
|
|
73
|
+
.map((c) => c.name)
|
|
74
|
+
.filter((name) =>
|
|
75
|
+
// Filter out system collections
|
|
76
|
+
!name.startsWith("system.") &&
|
|
77
|
+
// Match partial value
|
|
78
|
+
name.toLowerCase().includes(partialValue.toLowerCase()))
|
|
79
|
+
// Sort alphabetically
|
|
80
|
+
.sort();
|
|
81
|
+
console.warn(`Found ${matchingCollections.length} matching collections`);
|
|
82
|
+
// Limit to 100 items as per spec
|
|
83
|
+
const MAX_ITEMS = 100;
|
|
84
|
+
const limitedResults = matchingCollections.slice(0, MAX_ITEMS);
|
|
85
|
+
const hasMore = matchingCollections.length > MAX_ITEMS;
|
|
86
|
+
return {
|
|
87
|
+
completion: {
|
|
88
|
+
values: limitedResults,
|
|
89
|
+
total: matchingCollections.length,
|
|
90
|
+
hasMore,
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
console.error("Error completing collection names:", error);
|
|
96
|
+
// Return empty result on error
|
|
97
|
+
return emptyCompletionResult();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Create an empty completion result
|
|
102
|
+
* @returns Empty completion result object
|
|
103
|
+
*/
|
|
104
|
+
function emptyCompletionResult() {
|
|
105
|
+
return {
|
|
106
|
+
completion: {
|
|
107
|
+
values: [],
|
|
108
|
+
total: 0,
|
|
109
|
+
hasMore: false,
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export async function handlePingRequest({ request, client, db, isReadOnlyMode, }) {
|
|
2
|
+
try {
|
|
3
|
+
// Check MongoDB connection
|
|
4
|
+
if (!client) {
|
|
5
|
+
throw new Error("MongoDB connection is not available");
|
|
6
|
+
}
|
|
7
|
+
// Ping MongoDB to verify connection
|
|
8
|
+
const pong = await db.command({ ping: 1 });
|
|
9
|
+
if (pong.ok !== 1) {
|
|
10
|
+
throw new Error(`MongoDB ping failed: ${pong.errmsg}`);
|
|
11
|
+
}
|
|
12
|
+
return {};
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
if (error instanceof Error) {
|
|
16
|
+
throw new Error(`MongoDB ping failed: ${error.message}`);
|
|
17
|
+
}
|
|
18
|
+
throw new Error("MongoDB ping failed: Unknown error");
|
|
19
|
+
}
|
|
20
|
+
}
|