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