mcp-mongo-server 3.0.2 → 3.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 +37 -4
- package/build/index.js +2038 -1524
- package/package.json +3 -1
package/build/index.js
CHANGED
|
@@ -7,334 +7,1313 @@ import { createMcpHandler } from "@modelcontextprotocol/server";
|
|
|
7
7
|
import { serveStdio } from "@modelcontextprotocol/server/stdio";
|
|
8
8
|
import { createMcpExpressApp } from "@modelcontextprotocol/express";
|
|
9
9
|
|
|
10
|
-
// src/
|
|
10
|
+
// src/connections.ts
|
|
11
|
+
import { randomUUID } from "crypto";
|
|
12
|
+
import { ProtocolError, ProtocolErrorCode } from "@modelcontextprotocol/server";
|
|
11
13
|
import { MongoClient } from "mongodb";
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
function isRecord(value) {
|
|
15
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
16
|
+
}
|
|
17
|
+
var Connections = class {
|
|
18
|
+
cursorScope = randomUUID();
|
|
19
|
+
named;
|
|
20
|
+
entries = /* @__PURE__ */ new Map();
|
|
21
|
+
closing;
|
|
22
|
+
constructor(legacyUri, configuration, readOnly) {
|
|
23
|
+
this.named = configuration !== void 0;
|
|
24
|
+
let config = {};
|
|
25
|
+
if (configuration !== void 0) {
|
|
26
|
+
try {
|
|
27
|
+
config = JSON.parse(configuration);
|
|
28
|
+
} catch {
|
|
29
|
+
throw new Error("MCP_MONGODB_CONNECTIONS must be valid JSON");
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (!isRecord(config))
|
|
33
|
+
throw new Error("MCP_MONGODB_CONNECTIONS must be an object");
|
|
34
|
+
if (legacyUri && Object.hasOwn(config, "default")) {
|
|
35
|
+
throw new Error("The legacy URI and named default connection conflict");
|
|
36
|
+
}
|
|
37
|
+
const entries = legacyUri ? { ...config, default: { uri: legacyUri } } : config;
|
|
38
|
+
if (!Object.hasOwn(entries, "default"))
|
|
39
|
+
throw new Error("A default MongoDB connection is required");
|
|
40
|
+
for (const [id, entry] of Object.entries(entries)) {
|
|
41
|
+
if (!/^[A-Za-z0-9_-]+$/.test(id))
|
|
42
|
+
throw new Error("Invalid connection ID");
|
|
43
|
+
if (!isRecord(entry) || Object.keys(entry).some(
|
|
44
|
+
(key) => !["uri", "databases", "readOnly"].includes(key)
|
|
45
|
+
)) {
|
|
46
|
+
throw new Error(`Invalid configuration for connection '${id}'`);
|
|
47
|
+
}
|
|
48
|
+
const { uri, databases, readOnly: localReadOnly } = entry;
|
|
49
|
+
if (typeof uri !== "string" || !/^mongodb(?:\+srv)?:\/\//.test(uri)) {
|
|
50
|
+
throw new Error(`Invalid MongoDB URI for connection '${id}'`);
|
|
51
|
+
}
|
|
52
|
+
if (localReadOnly !== void 0 && typeof localReadOnly !== "boolean") {
|
|
53
|
+
throw new Error(`Invalid readOnly setting for connection '${id}'`);
|
|
54
|
+
}
|
|
55
|
+
let client;
|
|
56
|
+
const query = uri.indexOf("?");
|
|
57
|
+
const uriOptions = new URLSearchParams(
|
|
58
|
+
query < 0 ? "" : uri.slice(query + 1)
|
|
59
|
+
);
|
|
60
|
+
const hasSelectionTimeout = [...uriOptions.keys()].some(
|
|
61
|
+
(key) => key.toLowerCase() === "serverselectiontimeoutms"
|
|
62
|
+
);
|
|
63
|
+
const selectionTimeout = this.named && !hasSelectionTimeout ? 5e3 : void 0;
|
|
64
|
+
try {
|
|
65
|
+
client = new MongoClient(uri, {
|
|
66
|
+
serverSelectionTimeoutMS: selectionTimeout
|
|
67
|
+
});
|
|
68
|
+
} catch {
|
|
69
|
+
throw new Error(`Invalid MongoDB URI for connection '${id}'`);
|
|
70
|
+
}
|
|
71
|
+
const defaultDatabase = client.db().databaseName;
|
|
72
|
+
if (databases !== void 0 && (!Array.isArray(databases) || !databases.includes(defaultDatabase) || databases.some(
|
|
73
|
+
(name) => typeof name !== "string" || !name.trim() || /[/\\. "$]/.test(name) || name.includes("\0")
|
|
74
|
+
))) {
|
|
75
|
+
throw new Error(`Invalid database allowlist for connection '${id}'`);
|
|
76
|
+
}
|
|
77
|
+
this.entries.set(id, {
|
|
78
|
+
connectionId: id,
|
|
79
|
+
uri,
|
|
80
|
+
selectionTimeout,
|
|
81
|
+
client,
|
|
82
|
+
defaultDatabase,
|
|
83
|
+
databases: databases === void 0 ? [defaultDatabase] : [...new Set(databases)].sort(),
|
|
84
|
+
readOnly: readOnly || localReadOnly === true
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
connection(id = "default") {
|
|
89
|
+
const entry = typeof id === "string" ? this.entries.get(id) : void 0;
|
|
90
|
+
if (!entry) throw new Error("Unknown or invalid MongoDB connection ID");
|
|
91
|
+
return entry;
|
|
92
|
+
}
|
|
93
|
+
list() {
|
|
94
|
+
return [...this.entries.keys()].sort().map((id) => {
|
|
95
|
+
const { connectionId, defaultDatabase, readOnly } = this.connection(id);
|
|
96
|
+
return { connectionId, defaultDatabase, readOnly };
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
allowedDatabases(id) {
|
|
100
|
+
return this.connection(id).databases;
|
|
101
|
+
}
|
|
102
|
+
async resolve({ connectionId, database } = {}) {
|
|
103
|
+
const entry = this.connection(connectionId);
|
|
104
|
+
const name = database === void 0 ? entry.defaultDatabase : database;
|
|
105
|
+
if (typeof name !== "string" || !entry.databases.includes(name)) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
`Database is invalid or not allowed on connection '${entry.connectionId}'`
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
if (this.closing) throw new Error("MongoDB connections are shutting down");
|
|
111
|
+
if (!entry.pending) {
|
|
112
|
+
const client2 = entry.client ?? new MongoClient(entry.uri, {
|
|
113
|
+
serverSelectionTimeoutMS: entry.selectionTimeout
|
|
114
|
+
});
|
|
115
|
+
entry.client = client2;
|
|
116
|
+
entry.pending = client2.connect().catch(async () => {
|
|
117
|
+
await client2.close().catch(() => void 0);
|
|
118
|
+
entry.client = void 0;
|
|
119
|
+
entry.pending = void 0;
|
|
120
|
+
throw new Error(
|
|
121
|
+
`Failed to connect to MongoDB connection '${entry.connectionId}'`
|
|
122
|
+
);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
const client = await entry.pending;
|
|
18
126
|
return {
|
|
19
127
|
client,
|
|
20
|
-
db,
|
|
21
|
-
|
|
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
|
|
128
|
+
db: client.db(name),
|
|
129
|
+
isReadOnlyMode: entry.readOnly,
|
|
130
|
+
connectionId: entry.connectionId
|
|
31
131
|
};
|
|
32
132
|
}
|
|
133
|
+
async listDatabases(connectionId) {
|
|
134
|
+
const { db } = await this.resolve({ connectionId });
|
|
135
|
+
const allowed = this.allowedDatabases(connectionId);
|
|
136
|
+
const { databases } = await db.admin().listDatabases({
|
|
137
|
+
nameOnly: true,
|
|
138
|
+
authorizedDatabases: true,
|
|
139
|
+
filter: { name: { $in: [...allowed] } }
|
|
140
|
+
});
|
|
141
|
+
return databases.map(({ name }) => name).filter((name) => allowed.includes(name)).sort();
|
|
142
|
+
}
|
|
143
|
+
async close() {
|
|
144
|
+
this.closing ??= Promise.allSettled(
|
|
145
|
+
[...this.entries.values()].map(async ({ client, pending }) => {
|
|
146
|
+
await pending?.catch(() => void 0);
|
|
147
|
+
await client?.close();
|
|
148
|
+
})
|
|
149
|
+
).then(() => void 0);
|
|
150
|
+
await this.closing;
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
function resourceSelectors(uri) {
|
|
154
|
+
let url;
|
|
155
|
+
try {
|
|
156
|
+
url = new URL(uri);
|
|
157
|
+
if (url.protocol !== "mongodb:") throw new Error("Invalid scheme");
|
|
158
|
+
decodeURIComponent(url.search);
|
|
159
|
+
} catch {
|
|
160
|
+
throw new ProtocolError(
|
|
161
|
+
ProtocolErrorCode.InvalidParams,
|
|
162
|
+
"Invalid MongoDB resource URI"
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
const selectors = {};
|
|
166
|
+
for (const key of ["connectionId", "database"]) {
|
|
167
|
+
const values = url.searchParams.getAll(key);
|
|
168
|
+
if (values.length > 1) {
|
|
169
|
+
throw new ProtocolError(
|
|
170
|
+
ProtocolErrorCode.InvalidParams,
|
|
171
|
+
`Duplicate resource selector '${key}'`
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
if (values.length === 1) selectors[key] = values[0];
|
|
175
|
+
}
|
|
176
|
+
return selectors;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// src/query-limits.ts
|
|
180
|
+
function readQueryLimits(env) {
|
|
181
|
+
function positiveInteger(key) {
|
|
182
|
+
const raw = env[key];
|
|
183
|
+
if (raw === void 0) return void 0;
|
|
184
|
+
const value = Number(raw);
|
|
185
|
+
if (!/^\d+$/.test(raw) || !Number.isSafeInteger(value) || value <= 0) {
|
|
186
|
+
throw new Error(`${key} must be a positive integer`);
|
|
187
|
+
}
|
|
188
|
+
return value;
|
|
189
|
+
}
|
|
190
|
+
const bytes = positiveInteger("MCP_MONGODB_MAX_RESULT_BYTES");
|
|
191
|
+
if (bytes === 1) {
|
|
192
|
+
throw new Error("MCP_MONGODB_MAX_RESULT_BYTES must be at least 2 bytes");
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
documents: positiveInteger("MCP_MONGODB_MAX_RESULT_DOCUMENTS"),
|
|
196
|
+
bytes,
|
|
197
|
+
timeoutMS: positiveInteger("MCP_MONGODB_QUERY_TIMEOUT_MS")
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
async function readResultPage(cursor, limits, pageSize = Infinity, signal) {
|
|
201
|
+
const results = [];
|
|
202
|
+
const documentCap = limits.documents ?? Infinity;
|
|
203
|
+
const maxRows = Math.min(documentCap, pageSize);
|
|
204
|
+
let bytes = 2;
|
|
205
|
+
try {
|
|
206
|
+
while (true) {
|
|
207
|
+
signal?.throwIfAborted();
|
|
208
|
+
const row = await cursor.next();
|
|
209
|
+
signal?.throwIfAborted();
|
|
210
|
+
if (row === null) return { results, hasMore: false };
|
|
211
|
+
if (results.length === maxRows) {
|
|
212
|
+
return {
|
|
213
|
+
results,
|
|
214
|
+
hasMore: true,
|
|
215
|
+
...documentCap < pageSize ? { truncated: "documents" } : {}
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
if (limits.bytes !== void 0) {
|
|
219
|
+
bytes += Buffer.byteLength(JSON.stringify(row), "utf8") + (results.length ? 1 : 0);
|
|
220
|
+
if (bytes > limits.bytes) {
|
|
221
|
+
if (!results.length) {
|
|
222
|
+
throw new Error(
|
|
223
|
+
"First result exceeds the byte limit. Use a narrower projection or pipeline."
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
return { results, hasMore: true, truncated: "bytes" };
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
results.push(row);
|
|
230
|
+
}
|
|
231
|
+
} finally {
|
|
232
|
+
await cursor.close();
|
|
233
|
+
}
|
|
33
234
|
}
|
|
34
235
|
|
|
35
236
|
// src/server.ts
|
|
36
|
-
import {
|
|
237
|
+
import {
|
|
238
|
+
ProtocolError as ProtocolError4,
|
|
239
|
+
ProtocolErrorCode as ProtocolErrorCode4,
|
|
240
|
+
Server
|
|
241
|
+
} from "@modelcontextprotocol/server";
|
|
37
242
|
|
|
38
243
|
// src/schemas/call.ts
|
|
39
|
-
import { ProtocolError, ProtocolErrorCode } from "@modelcontextprotocol/server";
|
|
244
|
+
import { ProtocolError as ProtocolError3, ProtocolErrorCode as ProtocolErrorCode3 } from "@modelcontextprotocol/server";
|
|
245
|
+
import { ObjectId as ObjectId2 } from "mongodb";
|
|
246
|
+
|
|
247
|
+
// src/schemas/resource.ts
|
|
248
|
+
import {
|
|
249
|
+
ProtocolError as ProtocolError2,
|
|
250
|
+
ProtocolErrorCode as ProtocolErrorCode2
|
|
251
|
+
} from "@modelcontextprotocol/server";
|
|
40
252
|
import { ObjectId } from "mongodb";
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
"
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
"$function",
|
|
54
|
-
"$accumulator",
|
|
55
|
-
"$where"
|
|
56
|
-
]);
|
|
57
|
-
var READONLY_FORBIDDEN_AGG_OPERATORS = /* @__PURE__ */ new Set([
|
|
58
|
-
...WRITE_AGG_STAGES,
|
|
59
|
-
...SERVER_JS_AGG_OPERATORS
|
|
60
|
-
]);
|
|
61
|
-
function findAggOperator(value, operators, depth = 0) {
|
|
62
|
-
if (depth > MAX_OBJECT_DEPTH) {
|
|
63
|
-
throw new Error(
|
|
64
|
-
`Object nesting exceeds the maximum depth of ${MAX_OBJECT_DEPTH}`
|
|
253
|
+
|
|
254
|
+
// src/utils/pagination.ts
|
|
255
|
+
var DEFAULT_PAGE_SIZE = 20;
|
|
256
|
+
function encodeCursor(offset) {
|
|
257
|
+
const data = { offset };
|
|
258
|
+
return Buffer.from(JSON.stringify(data)).toString("base64");
|
|
259
|
+
}
|
|
260
|
+
function decodeCursor(cursor) {
|
|
261
|
+
if (!cursor) return 0;
|
|
262
|
+
try {
|
|
263
|
+
const data = JSON.parse(
|
|
264
|
+
Buffer.from(cursor, "base64").toString("utf-8")
|
|
65
265
|
);
|
|
266
|
+
return typeof data.offset === "number" && data.offset >= 0 ? data.offset : 0;
|
|
267
|
+
} catch {
|
|
268
|
+
return 0;
|
|
66
269
|
}
|
|
270
|
+
}
|
|
271
|
+
function paginate(items, cursor, pageSize = DEFAULT_PAGE_SIZE) {
|
|
272
|
+
const offset = decodeCursor(cursor);
|
|
273
|
+
const page = items.slice(offset, offset + pageSize);
|
|
274
|
+
const nextOffset = offset + pageSize;
|
|
275
|
+
const nextCursor = nextOffset < items.length ? encodeCursor(nextOffset) : void 0;
|
|
276
|
+
return { items: page, nextCursor };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// src/schemas/resource.ts
|
|
280
|
+
function detectMongoType(value) {
|
|
281
|
+
if (value === null) return "null";
|
|
282
|
+
if (value === void 0) return "undefined";
|
|
283
|
+
if (value instanceof ObjectId) return "ObjectId";
|
|
284
|
+
if (value instanceof Date) return "Date";
|
|
67
285
|
if (Array.isArray(value)) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
286
|
+
if (value.length === 0) return "Array";
|
|
287
|
+
const elementTypes = new Set(value.map((item) => detectMongoType(item)));
|
|
288
|
+
if (elementTypes.size === 1) {
|
|
289
|
+
return `Array<${Array.from(elementTypes)[0]}>`;
|
|
71
290
|
}
|
|
72
|
-
return
|
|
291
|
+
return "Array<mixed>";
|
|
73
292
|
}
|
|
74
|
-
if (
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
293
|
+
if (typeof value === "object") {
|
|
294
|
+
return "Document";
|
|
295
|
+
}
|
|
296
|
+
return typeof value;
|
|
297
|
+
}
|
|
298
|
+
function inferSchemaFromSamples(documents) {
|
|
299
|
+
if (!documents || documents.length === 0) {
|
|
300
|
+
return { fields: [] };
|
|
301
|
+
}
|
|
302
|
+
const fieldMap = /* @__PURE__ */ new Map();
|
|
303
|
+
for (const doc of documents) {
|
|
304
|
+
for (const [key, value] of Object.entries(doc)) {
|
|
305
|
+
if (!fieldMap.has(key)) {
|
|
306
|
+
fieldMap.set(key, {
|
|
307
|
+
name: key,
|
|
308
|
+
types: /* @__PURE__ */ new Set([detectMongoType(value)]),
|
|
309
|
+
nullable: false,
|
|
310
|
+
// Store sample values for complex types
|
|
311
|
+
samples: [value]
|
|
312
|
+
});
|
|
313
|
+
} else {
|
|
314
|
+
const fieldInfo = fieldMap.get(key);
|
|
315
|
+
fieldInfo.types.add(detectMongoType(value));
|
|
316
|
+
if (fieldInfo.samples.length < 3 && !fieldInfo.samples.some(
|
|
317
|
+
(sample) => JSON.stringify(sample) === JSON.stringify(value)
|
|
318
|
+
)) {
|
|
319
|
+
fieldInfo.samples.push(value);
|
|
320
|
+
}
|
|
78
321
|
}
|
|
79
|
-
const found = findAggOperator(nested, operators, depth + 1);
|
|
80
|
-
if (found) return found;
|
|
81
322
|
}
|
|
82
323
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
);
|
|
324
|
+
for (const doc of documents) {
|
|
325
|
+
for (const [key] of fieldMap.entries()) {
|
|
326
|
+
if (!(key in doc)) {
|
|
327
|
+
const fieldInfo = fieldMap.get(key);
|
|
328
|
+
fieldInfo.nullable = true;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
91
331
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if (
|
|
332
|
+
for (const [key, fieldInfo] of fieldMap.entries()) {
|
|
333
|
+
if (fieldInfo.types.has("Document")) {
|
|
334
|
+
const nestedDocs = documents.filter(
|
|
335
|
+
(doc) => doc[key] && typeof doc[key] === "object" && !Array.isArray(doc[key])
|
|
336
|
+
).map((doc) => doc[key]);
|
|
337
|
+
if (nestedDocs.length > 0) {
|
|
338
|
+
fieldInfo.nestedSchema = inferSchemaFromSamples(nestedDocs);
|
|
339
|
+
}
|
|
98
340
|
}
|
|
99
|
-
return null;
|
|
100
|
-
};
|
|
101
|
-
for (const stage of pipeline) {
|
|
102
|
-
if (!stage || typeof stage !== "object") continue;
|
|
103
|
-
const s = stage;
|
|
104
|
-
const out = foreignDb(s.$out);
|
|
105
|
-
if (out) return out;
|
|
106
|
-
const merge = s.$merge;
|
|
107
|
-
const mergeInto = foreignDb(merge?.into ?? s.$merge);
|
|
108
|
-
if (mergeInto) return mergeInto;
|
|
109
|
-
const lookup = s.$lookup;
|
|
110
|
-
const lookupFrom = foreignDb(lookup?.from);
|
|
111
|
-
if (lookupFrom) return lookupFrom;
|
|
112
341
|
}
|
|
113
|
-
|
|
342
|
+
const fields = Array.from(fieldMap.values()).map((fieldInfo) => {
|
|
343
|
+
const result = {
|
|
344
|
+
name: fieldInfo.name,
|
|
345
|
+
types: Array.from(fieldInfo.types),
|
|
346
|
+
nullable: fieldInfo.nullable,
|
|
347
|
+
prevalence: `${Math.round(
|
|
348
|
+
documents.filter((doc) => fieldInfo.name in doc).length / documents.length * 100
|
|
349
|
+
)}%`,
|
|
350
|
+
examples: []
|
|
351
|
+
};
|
|
352
|
+
if (fieldInfo.nestedSchema) {
|
|
353
|
+
result.nestedSchema = fieldInfo.nestedSchema;
|
|
354
|
+
}
|
|
355
|
+
const sampleValues = fieldInfo.samples.map((sample) => {
|
|
356
|
+
if (sample instanceof ObjectId) return sample.toString();
|
|
357
|
+
if (sample instanceof Date) return sample.toISOString();
|
|
358
|
+
if (typeof sample === "object") {
|
|
359
|
+
return Array.isArray(sample) ? "[...]" : "{...}";
|
|
360
|
+
}
|
|
361
|
+
return sample;
|
|
362
|
+
});
|
|
363
|
+
result.examples = sampleValues;
|
|
364
|
+
return result;
|
|
365
|
+
});
|
|
366
|
+
return { fields };
|
|
114
367
|
}
|
|
115
|
-
async function
|
|
368
|
+
async function handleReadResourceRequest({
|
|
116
369
|
request,
|
|
117
370
|
client,
|
|
118
371
|
db,
|
|
119
372
|
isReadOnlyMode,
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
signal
|
|
373
|
+
signal,
|
|
374
|
+
sendProgress
|
|
123
375
|
}) {
|
|
124
|
-
|
|
125
|
-
const operation = name;
|
|
126
|
-
const objectIdMode = args.objectIdMode || "auto";
|
|
127
|
-
const filteredArgs = {};
|
|
128
|
-
for (const [key, value] of Object.entries(args)) {
|
|
129
|
-
if (key !== "objectIdMode") {
|
|
130
|
-
filteredArgs[key] = value;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
if (args.sort) {
|
|
134
|
-
args.sort = parseSort(args.sort);
|
|
135
|
-
}
|
|
136
|
-
Object.assign(args, filteredArgs);
|
|
137
|
-
validateOperation(operation);
|
|
376
|
+
let collectionName;
|
|
138
377
|
try {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
`Collection name is required for '${operation}' operation`
|
|
146
|
-
);
|
|
147
|
-
}
|
|
148
|
-
collection = db.collection(collectionName);
|
|
149
|
-
validateCollection(collection);
|
|
378
|
+
const url = new URL(request.params.uri);
|
|
379
|
+
if (url.protocol !== "mongodb:") {
|
|
380
|
+
throw new ProtocolError2(
|
|
381
|
+
ProtocolErrorCode2.InvalidParams,
|
|
382
|
+
`Unsupported resource URI scheme '${url.protocol}': expected 'mongodb:'`
|
|
383
|
+
);
|
|
150
384
|
}
|
|
151
|
-
|
|
152
|
-
return await executeOperation(
|
|
153
|
-
operation,
|
|
154
|
-
collection,
|
|
155
|
-
db,
|
|
156
|
-
isReadOnlyMode,
|
|
157
|
-
allowCrossDb,
|
|
158
|
-
allowServerJs,
|
|
159
|
-
args,
|
|
160
|
-
objectIdMode,
|
|
161
|
-
signal
|
|
162
|
-
);
|
|
385
|
+
collectionName = decodeURIComponent(url.pathname.replace(/^\//, ""));
|
|
163
386
|
} catch (error) {
|
|
164
|
-
if (error instanceof
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
error instanceof Error ? error.message : "Unknown error"
|
|
387
|
+
if (error instanceof ProtocolError2) throw error;
|
|
388
|
+
throw new ProtocolError2(
|
|
389
|
+
ProtocolErrorCode2.InvalidParams,
|
|
390
|
+
`Invalid resource URI: ${request.params.uri}`
|
|
169
391
|
);
|
|
170
392
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
return handleQuery(
|
|
176
|
-
collection,
|
|
177
|
-
args,
|
|
178
|
-
objectIdMode,
|
|
179
|
-
allowServerJs && !isReadOnlyMode,
|
|
180
|
-
signal
|
|
181
|
-
);
|
|
182
|
-
case "aggregate":
|
|
183
|
-
return handleAggregate(
|
|
184
|
-
collection,
|
|
185
|
-
args,
|
|
186
|
-
objectIdMode,
|
|
187
|
-
isReadOnlyMode,
|
|
188
|
-
allowCrossDb,
|
|
189
|
-
allowServerJs,
|
|
190
|
-
signal
|
|
191
|
-
);
|
|
192
|
-
case "update":
|
|
193
|
-
return handleUpdate(collection, args, objectIdMode, allowServerJs, signal);
|
|
194
|
-
case "serverInfo":
|
|
195
|
-
return handleServerInfo(db, isReadOnlyMode, args, signal);
|
|
196
|
-
case "insert":
|
|
197
|
-
return handleInsert(collection, args, objectIdMode, signal);
|
|
198
|
-
case "createIndex":
|
|
199
|
-
return handleCreateIndex(collection, args, objectIdMode, signal);
|
|
200
|
-
case "count":
|
|
201
|
-
return handleCount(
|
|
202
|
-
collection,
|
|
203
|
-
args,
|
|
204
|
-
objectIdMode,
|
|
205
|
-
allowServerJs && !isReadOnlyMode,
|
|
206
|
-
signal
|
|
207
|
-
);
|
|
208
|
-
case "listCollections":
|
|
209
|
-
return handleListCollections(db, args, objectIdMode, signal);
|
|
210
|
-
case "convertTime":
|
|
211
|
-
return handleConvertTime(args);
|
|
212
|
-
default:
|
|
213
|
-
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Unknown tool: ${operation}`);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
function validateOperation(operation) {
|
|
217
|
-
const validOperations = [
|
|
218
|
-
"query",
|
|
219
|
-
"aggregate",
|
|
220
|
-
"update",
|
|
221
|
-
"serverInfo",
|
|
222
|
-
"insert",
|
|
223
|
-
"createIndex",
|
|
224
|
-
"count",
|
|
225
|
-
"listCollections",
|
|
226
|
-
"convertTime"
|
|
227
|
-
];
|
|
228
|
-
if (!validOperations.includes(operation)) {
|
|
229
|
-
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Unknown tool: ${operation}`);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
function validateCollection(collection) {
|
|
233
|
-
if (!collection.collectionName) {
|
|
234
|
-
throw new Error("Collection name cannot be empty");
|
|
235
|
-
}
|
|
236
|
-
if (collection.collectionName.toLowerCase().startsWith("system.")) {
|
|
237
|
-
throw new Error("Access to system collections is not allowed");
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
function checkReadOnlyMode(operation, isReadOnlyMode) {
|
|
241
|
-
if (isReadOnlyMode && WRITE_OPERATIONS.includes(operation)) {
|
|
242
|
-
throw new Error(
|
|
243
|
-
`ReadonlyError: Operation '${operation}' is not allowed in read-only mode`
|
|
393
|
+
if (!collectionName) {
|
|
394
|
+
throw new ProtocolError2(
|
|
395
|
+
ProtocolErrorCode2.InvalidParams,
|
|
396
|
+
"Resource URI is missing a collection name"
|
|
244
397
|
);
|
|
245
398
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
399
|
+
if (collectionName.toLowerCase().startsWith("system.")) {
|
|
400
|
+
throw new ProtocolError2(
|
|
401
|
+
ProtocolErrorCode2.InvalidParams,
|
|
402
|
+
"Access to system collections is not allowed"
|
|
403
|
+
);
|
|
251
404
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
405
|
+
try {
|
|
406
|
+
const schema = await buildCollectionSchema(
|
|
407
|
+
db,
|
|
408
|
+
db.collection(collectionName),
|
|
409
|
+
signal,
|
|
410
|
+
sendProgress
|
|
411
|
+
);
|
|
412
|
+
return {
|
|
413
|
+
contents: [
|
|
414
|
+
{
|
|
415
|
+
uri: request.params.uri,
|
|
416
|
+
mimeType: "application/json",
|
|
417
|
+
text: JSON.stringify(schema, null, 2)
|
|
418
|
+
}
|
|
419
|
+
]
|
|
420
|
+
};
|
|
421
|
+
} catch (error) {
|
|
422
|
+
signal?.throwIfAborted();
|
|
423
|
+
if (error instanceof Error) {
|
|
424
|
+
throw new Error(
|
|
425
|
+
`Failed to read collection ${collectionName}: ${error.message}`
|
|
426
|
+
);
|
|
256
427
|
}
|
|
428
|
+
throw new Error(
|
|
429
|
+
`Failed to read collection ${collectionName}: Unknown error`
|
|
430
|
+
);
|
|
257
431
|
}
|
|
258
|
-
return Object.keys(validSort).length > 0 ? validSort : null;
|
|
259
432
|
}
|
|
260
|
-
function
|
|
261
|
-
|
|
262
|
-
|
|
433
|
+
async function buildCollectionSchema(db, collection, signal, sendProgress) {
|
|
434
|
+
const collectionName = collection.collectionName;
|
|
435
|
+
const sampleSize = 100;
|
|
436
|
+
let sampleDocuments = [];
|
|
437
|
+
await sendProgress?.(1, 4, "Sampling documents");
|
|
438
|
+
try {
|
|
439
|
+
sampleDocuments = await collection.aggregate([{ $sample: { size: sampleSize } }], { signal }).toArray();
|
|
440
|
+
} catch (sampleError) {
|
|
441
|
+
signal?.throwIfAborted();
|
|
442
|
+
console.warn(
|
|
443
|
+
`$sample aggregation failed for ${collectionName}, falling back to sequential scan: ${sampleError}`
|
|
444
|
+
);
|
|
445
|
+
signal?.throwIfAborted();
|
|
446
|
+
sampleDocuments = await collection.find({}, { signal }).limit(sampleSize).toArray();
|
|
263
447
|
}
|
|
264
|
-
|
|
448
|
+
await sendProgress?.(2, 4, "Getting indexes");
|
|
449
|
+
signal?.throwIfAborted();
|
|
450
|
+
const indexOptions = {
|
|
451
|
+
signal
|
|
452
|
+
};
|
|
453
|
+
const indexes = await collection.indexes(indexOptions);
|
|
454
|
+
const inferredSchema = inferSchemaFromSamples(sampleDocuments);
|
|
455
|
+
await sendProgress?.(3, 4, "Counting documents");
|
|
456
|
+
let documentCount = null;
|
|
457
|
+
try {
|
|
458
|
+
signal?.throwIfAborted();
|
|
459
|
+
documentCount = await collection.countDocuments(
|
|
460
|
+
{},
|
|
461
|
+
{ timeoutMS: 5e3, signal }
|
|
462
|
+
);
|
|
463
|
+
} catch (countError) {
|
|
464
|
+
signal?.throwIfAborted();
|
|
465
|
+
console.warn(
|
|
466
|
+
`Count operation failed or timed out for ${collectionName}: ${countError}`
|
|
467
|
+
);
|
|
265
468
|
try {
|
|
266
|
-
|
|
469
|
+
signal?.throwIfAborted();
|
|
470
|
+
const stats = await db.command(
|
|
471
|
+
{ collStats: collectionName },
|
|
472
|
+
{ timeoutMS: 5e3, signal }
|
|
473
|
+
);
|
|
474
|
+
documentCount = stats.count;
|
|
267
475
|
} catch {
|
|
268
|
-
|
|
476
|
+
signal?.throwIfAborted();
|
|
477
|
+
documentCount = "unknown (count operation timed out)";
|
|
269
478
|
}
|
|
270
479
|
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
480
|
+
await sendProgress?.(4, 4, "Done");
|
|
481
|
+
return {
|
|
482
|
+
type: "collection",
|
|
483
|
+
name: collectionName,
|
|
484
|
+
fields: inferredSchema.fields,
|
|
485
|
+
indexes: indexes.map((idx) => ({
|
|
486
|
+
name: idx.name,
|
|
487
|
+
keys: idx.key
|
|
488
|
+
})),
|
|
489
|
+
documentCount,
|
|
490
|
+
sampleSize: sampleDocuments.length,
|
|
491
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
492
|
+
};
|
|
282
493
|
}
|
|
283
|
-
function
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
return isISODateString(dateString) ? new Date(dateString) : item;
|
|
309
|
-
}
|
|
310
|
-
return item;
|
|
311
|
-
});
|
|
312
|
-
} else {
|
|
313
|
-
result2[key] = processObjectIdInFilter(
|
|
314
|
-
value,
|
|
315
|
-
"none",
|
|
316
|
-
depth + 1
|
|
317
|
-
);
|
|
318
|
-
}
|
|
319
|
-
} else {
|
|
320
|
-
result2[key] = value;
|
|
321
|
-
}
|
|
494
|
+
async function handleListResourcesRequest({
|
|
495
|
+
request,
|
|
496
|
+
client,
|
|
497
|
+
db,
|
|
498
|
+
isReadOnlyMode,
|
|
499
|
+
signal
|
|
500
|
+
}) {
|
|
501
|
+
try {
|
|
502
|
+
signal?.throwIfAborted();
|
|
503
|
+
const collections = await db.listCollections().toArray();
|
|
504
|
+
const allResources = collections.map((collection) => ({
|
|
505
|
+
uri: `mongodb:///${encodeURIComponent(collection.name)}`,
|
|
506
|
+
mimeType: "application/json",
|
|
507
|
+
name: collection.name,
|
|
508
|
+
description: `MongoDB collection: ${collection.name}`
|
|
509
|
+
}));
|
|
510
|
+
const cursor = request.params?.cursor;
|
|
511
|
+
const { items, nextCursor } = paginate(allResources, cursor);
|
|
512
|
+
return {
|
|
513
|
+
resources: items,
|
|
514
|
+
...nextCursor ? { nextCursor } : {}
|
|
515
|
+
};
|
|
516
|
+
} catch (error) {
|
|
517
|
+
if (error instanceof Error) {
|
|
518
|
+
throw new Error(`Failed to list collections: ${error.message}`);
|
|
322
519
|
}
|
|
323
|
-
|
|
520
|
+
throw new Error("Failed to list collections: Unknown error");
|
|
324
521
|
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// src/schemas/tools.ts
|
|
525
|
+
var WRITE_OPERATIONS = /* @__PURE__ */ new Set(["update", "insert", "createIndex"]);
|
|
526
|
+
async function handleListToolsRequest({
|
|
527
|
+
request,
|
|
528
|
+
client,
|
|
529
|
+
db,
|
|
530
|
+
isReadOnlyMode,
|
|
531
|
+
signal,
|
|
532
|
+
connectionSelection = false,
|
|
533
|
+
hasWritableConnection = !isReadOnlyMode,
|
|
534
|
+
queryLimits = {}
|
|
535
|
+
}) {
|
|
536
|
+
const result = {
|
|
537
|
+
tools: [
|
|
538
|
+
{
|
|
539
|
+
name: "query",
|
|
540
|
+
description: "Find documents in one collection using a filter, projection, or sort. Use aggregate for grouping or joins. Request an execution plan to inspect the query.",
|
|
541
|
+
inputSchema: {
|
|
542
|
+
type: "object",
|
|
543
|
+
properties: {
|
|
544
|
+
collection: {
|
|
545
|
+
type: "string",
|
|
546
|
+
description: "Name of the collection to query"
|
|
547
|
+
},
|
|
548
|
+
filter: {
|
|
549
|
+
type: "object",
|
|
550
|
+
description: "MongoDB query filter. Supports date strings in ISO format ('2025-01-01T00:00:00Z') and ISODate('2025-01-01T00:00:00Z') notation"
|
|
551
|
+
},
|
|
552
|
+
projection: {
|
|
553
|
+
type: "object",
|
|
554
|
+
description: "Fields to include/exclude"
|
|
555
|
+
},
|
|
556
|
+
limit: {
|
|
557
|
+
type: "number",
|
|
558
|
+
description: "Requested page size. Configured server caps may reduce the number returned.",
|
|
559
|
+
default: 10
|
|
560
|
+
},
|
|
561
|
+
includeTotal: {
|
|
562
|
+
type: "boolean",
|
|
563
|
+
description: "Include the exact match count. Set false to skip counting.",
|
|
564
|
+
default: true
|
|
565
|
+
},
|
|
566
|
+
skip: {
|
|
567
|
+
type: "number",
|
|
568
|
+
description: "Number of documents to skip before returning results",
|
|
569
|
+
default: 0
|
|
570
|
+
},
|
|
571
|
+
sort: {
|
|
572
|
+
type: "object",
|
|
573
|
+
description: "Sort order as a field-to-direction map, e.g. { orderDate: -1 } for descending or { name: 1 } for ascending"
|
|
574
|
+
},
|
|
575
|
+
explain: {
|
|
576
|
+
type: "string",
|
|
577
|
+
description: "Optional: Get query execution information",
|
|
578
|
+
enum: ["queryPlanner", "executionStats", "allPlansExecution"]
|
|
579
|
+
},
|
|
580
|
+
objectIdMode: {
|
|
581
|
+
type: "string",
|
|
582
|
+
description: "Control how 24-character hex strings are handled",
|
|
583
|
+
enum: ["auto", "none", "force"],
|
|
584
|
+
default: "auto"
|
|
585
|
+
}
|
|
586
|
+
},
|
|
587
|
+
required: ["collection"]
|
|
588
|
+
}
|
|
589
|
+
},
|
|
590
|
+
{
|
|
591
|
+
name: "aggregate",
|
|
592
|
+
description: "Run a MongoDB aggregation pipeline for grouping, joins, or multi-stage transformations. Request an execution plan to inspect the pipeline.",
|
|
593
|
+
inputSchema: {
|
|
594
|
+
type: "object",
|
|
595
|
+
properties: {
|
|
596
|
+
collection: {
|
|
597
|
+
type: "string",
|
|
598
|
+
description: "Name of the collection to aggregate"
|
|
599
|
+
},
|
|
600
|
+
pipeline: {
|
|
601
|
+
type: "array",
|
|
602
|
+
description: "Aggregation pipeline stages. Supports date strings in ISO format ('2025-01-01T00:00:00Z') and ISODate('2025-01-01T00:00:00Z') notation",
|
|
603
|
+
items: {
|
|
604
|
+
type: "object"
|
|
605
|
+
}
|
|
606
|
+
},
|
|
607
|
+
explain: {
|
|
608
|
+
type: "string",
|
|
609
|
+
description: "Optional: Get aggregation execution information (queryPlanner, executionStats, or allPlansExecution)",
|
|
610
|
+
enum: ["queryPlanner", "executionStats", "allPlansExecution"]
|
|
611
|
+
},
|
|
612
|
+
objectIdMode: {
|
|
613
|
+
type: "string",
|
|
614
|
+
description: "Control how 24-character hex strings are handled",
|
|
615
|
+
enum: ["auto", "none", "force"],
|
|
616
|
+
default: "auto"
|
|
617
|
+
}
|
|
618
|
+
},
|
|
619
|
+
required: ["collection", "pipeline"]
|
|
620
|
+
}
|
|
621
|
+
},
|
|
622
|
+
{
|
|
623
|
+
name: "update",
|
|
624
|
+
description: "Modify matching documents using operators such as $set, $unset, or $inc. Enable multi to update all matches or upsert to create a document when the filter matches none.",
|
|
625
|
+
inputSchema: {
|
|
626
|
+
type: "object",
|
|
627
|
+
properties: {
|
|
628
|
+
collection: {
|
|
629
|
+
type: "string",
|
|
630
|
+
description: "Name of the collection to update"
|
|
631
|
+
},
|
|
632
|
+
filter: {
|
|
633
|
+
type: "object",
|
|
634
|
+
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"
|
|
635
|
+
},
|
|
636
|
+
update: {
|
|
637
|
+
type: "object",
|
|
638
|
+
description: "Update operations to apply ($set, $unset, $inc, etc.)"
|
|
639
|
+
},
|
|
640
|
+
upsert: {
|
|
641
|
+
type: "boolean",
|
|
642
|
+
description: "Create a new document if no documents match the filter"
|
|
643
|
+
},
|
|
644
|
+
multi: {
|
|
645
|
+
type: "boolean",
|
|
646
|
+
description: "Update multiple documents that match the filter"
|
|
647
|
+
},
|
|
648
|
+
objectIdMode: {
|
|
649
|
+
type: "string",
|
|
650
|
+
description: "Control how 24-character hex strings are handled",
|
|
651
|
+
enum: ["auto", "none", "force"],
|
|
652
|
+
default: "auto"
|
|
653
|
+
}
|
|
654
|
+
},
|
|
655
|
+
required: ["collection", "filter", "update"]
|
|
656
|
+
}
|
|
657
|
+
},
|
|
658
|
+
{
|
|
659
|
+
name: "serverInfo",
|
|
660
|
+
description: "Inspect MongoDB server metadata such as version and storage engine. Enable includeDebugInfo for additional diagnostics.",
|
|
661
|
+
inputSchema: {
|
|
662
|
+
type: "object",
|
|
663
|
+
properties: {
|
|
664
|
+
includeDebugInfo: {
|
|
665
|
+
type: "boolean",
|
|
666
|
+
description: "Include additional debug information about the server"
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
},
|
|
671
|
+
{
|
|
672
|
+
name: "insert",
|
|
673
|
+
description: "Insert new documents into a MongoDB collection. Use update to modify existing records.",
|
|
674
|
+
inputSchema: {
|
|
675
|
+
type: "object",
|
|
676
|
+
properties: {
|
|
677
|
+
collection: {
|
|
678
|
+
type: "string",
|
|
679
|
+
description: "Name of the collection to insert into"
|
|
680
|
+
},
|
|
681
|
+
documents: {
|
|
682
|
+
type: "array",
|
|
683
|
+
description: "Array of documents to insert",
|
|
684
|
+
items: { type: "object" }
|
|
685
|
+
},
|
|
686
|
+
ordered: {
|
|
687
|
+
type: "boolean",
|
|
688
|
+
description: "If true, perform ordered insert. If false, insert unordered"
|
|
689
|
+
},
|
|
690
|
+
writeConcern: {
|
|
691
|
+
type: "object",
|
|
692
|
+
description: "Write concern for the insert operation"
|
|
693
|
+
},
|
|
694
|
+
bypassDocumentValidation: {
|
|
695
|
+
type: "boolean",
|
|
696
|
+
description: "Allow insert to bypass schema validation"
|
|
697
|
+
},
|
|
698
|
+
objectIdMode: {
|
|
699
|
+
type: "string",
|
|
700
|
+
description: "Control how 24-character hex strings are handled",
|
|
701
|
+
enum: ["auto", "none", "force"],
|
|
702
|
+
default: "auto"
|
|
703
|
+
}
|
|
704
|
+
},
|
|
705
|
+
required: ["collection", "documents"]
|
|
706
|
+
}
|
|
707
|
+
},
|
|
708
|
+
{
|
|
709
|
+
name: "createIndex",
|
|
710
|
+
description: "Create indexes to improve query performance, enforce uniqueness, or expire documents with a TTL index.",
|
|
711
|
+
inputSchema: {
|
|
712
|
+
type: "object",
|
|
713
|
+
properties: {
|
|
714
|
+
collection: {
|
|
715
|
+
type: "string",
|
|
716
|
+
description: "Name of the collection to create indexes on"
|
|
717
|
+
},
|
|
718
|
+
indexes: {
|
|
719
|
+
type: "array",
|
|
720
|
+
description: "Array of index specifications",
|
|
721
|
+
items: {
|
|
722
|
+
type: "object",
|
|
723
|
+
properties: {
|
|
724
|
+
key: {
|
|
725
|
+
type: "object",
|
|
726
|
+
description: "Index key pattern, e.g. { field: 1 }"
|
|
727
|
+
},
|
|
728
|
+
name: {
|
|
729
|
+
type: "string",
|
|
730
|
+
description: "Optional: Name of the index"
|
|
731
|
+
},
|
|
732
|
+
unique: {
|
|
733
|
+
type: "boolean",
|
|
734
|
+
description: "Optional: Creates a unique index"
|
|
735
|
+
},
|
|
736
|
+
sparse: {
|
|
737
|
+
type: "boolean",
|
|
738
|
+
description: "Optional: Creates a sparse index"
|
|
739
|
+
},
|
|
740
|
+
background: {
|
|
741
|
+
type: "boolean",
|
|
742
|
+
description: "Optional: Builds index in background"
|
|
743
|
+
},
|
|
744
|
+
expireAfterSeconds: {
|
|
745
|
+
type: "number",
|
|
746
|
+
description: "TTL in seconds for documents"
|
|
747
|
+
},
|
|
748
|
+
partialFilterExpression: {
|
|
749
|
+
type: "object",
|
|
750
|
+
description: "Filter expression for partial indexes"
|
|
751
|
+
}
|
|
752
|
+
},
|
|
753
|
+
required: ["key"]
|
|
754
|
+
}
|
|
755
|
+
},
|
|
756
|
+
writeConcern: {
|
|
757
|
+
type: "object",
|
|
758
|
+
description: "Write concern for index creation"
|
|
759
|
+
},
|
|
760
|
+
commitQuorum: {
|
|
761
|
+
type: ["string", "number"],
|
|
762
|
+
description: "Replica-set commit quorum as a member count or a mode such as majority"
|
|
763
|
+
},
|
|
764
|
+
objectIdMode: {
|
|
765
|
+
type: "string",
|
|
766
|
+
description: "Control how 24-character hex strings are handled",
|
|
767
|
+
enum: ["auto", "none", "force"],
|
|
768
|
+
default: "auto"
|
|
769
|
+
}
|
|
770
|
+
},
|
|
771
|
+
required: ["collection", "indexes"]
|
|
772
|
+
}
|
|
773
|
+
},
|
|
774
|
+
{
|
|
775
|
+
name: "count",
|
|
776
|
+
description: "Count documents matching a filter in one collection. Use aggregate for counts grouped by field.",
|
|
777
|
+
inputSchema: {
|
|
778
|
+
type: "object",
|
|
779
|
+
properties: {
|
|
780
|
+
collection: { type: "string", description: "Collection name" },
|
|
781
|
+
query: { type: "object", description: "Query filter to count" },
|
|
782
|
+
limit: { type: "integer", description: "Max documents to count" },
|
|
783
|
+
skip: {
|
|
784
|
+
type: "integer",
|
|
785
|
+
description: "Docs to skip before counting"
|
|
786
|
+
},
|
|
787
|
+
hint: { type: "object", description: "Index hint" },
|
|
788
|
+
readConcern: { type: "object", description: "Read concern option" },
|
|
789
|
+
maxTimeMS: { type: "integer", description: "Max execution time" },
|
|
790
|
+
collation: {
|
|
791
|
+
type: "object",
|
|
792
|
+
description: "Collation rules for comparison"
|
|
793
|
+
},
|
|
794
|
+
objectIdMode: {
|
|
795
|
+
type: "string",
|
|
796
|
+
description: "Control how 24-character hex strings are handled",
|
|
797
|
+
enum: ["auto", "none", "force"],
|
|
798
|
+
default: "auto"
|
|
799
|
+
}
|
|
800
|
+
},
|
|
801
|
+
required: ["collection"]
|
|
802
|
+
}
|
|
803
|
+
},
|
|
804
|
+
{
|
|
805
|
+
name: "listCollections",
|
|
806
|
+
description: "List collections in the connected database. Filter by name or other collection properties and request names only for a compact result.",
|
|
807
|
+
inputSchema: {
|
|
808
|
+
type: "object",
|
|
809
|
+
properties: {
|
|
810
|
+
nameOnly: {
|
|
811
|
+
type: "boolean",
|
|
812
|
+
description: "If true, return only collection names"
|
|
813
|
+
},
|
|
814
|
+
filter: {
|
|
815
|
+
type: "object",
|
|
816
|
+
description: "Filter for collections"
|
|
817
|
+
},
|
|
818
|
+
skip: {
|
|
819
|
+
type: "number",
|
|
820
|
+
description: "Number of collections to skip before returning results",
|
|
821
|
+
default: 0
|
|
822
|
+
},
|
|
823
|
+
limit: {
|
|
824
|
+
type: "number",
|
|
825
|
+
description: "Maximum number of collections to return",
|
|
826
|
+
default: 20
|
|
827
|
+
},
|
|
828
|
+
objectIdMode: {
|
|
829
|
+
type: "string",
|
|
830
|
+
description: "Control how 24-character hex strings are handled",
|
|
831
|
+
enum: ["auto", "none", "force"],
|
|
832
|
+
default: "auto"
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
},
|
|
837
|
+
...[
|
|
838
|
+
["listIndexes", "List a collection's indexes, keys, and options."],
|
|
839
|
+
[
|
|
840
|
+
"collectionSchema",
|
|
841
|
+
"Infer a collection schema from sample documents and include its indexes."
|
|
842
|
+
]
|
|
843
|
+
].map(([name, description]) => ({
|
|
844
|
+
name,
|
|
845
|
+
description,
|
|
846
|
+
inputSchema: {
|
|
847
|
+
type: "object",
|
|
848
|
+
properties: {
|
|
849
|
+
collection: {
|
|
850
|
+
type: "string",
|
|
851
|
+
description: "Name of the collection to inspect"
|
|
852
|
+
}
|
|
853
|
+
},
|
|
854
|
+
required: ["collection"]
|
|
855
|
+
}
|
|
856
|
+
})),
|
|
857
|
+
{
|
|
858
|
+
name: "convertTime",
|
|
859
|
+
description: "Convert a Unix timestamp or date string to multiple formats (UTC ISO 8601, GMT, Unix seconds/milliseconds) and report the server's current time and timezone. Useful for building unambiguous date queries when the server and user are in different timezones. Omit 'input' to get the current time.",
|
|
860
|
+
inputSchema: {
|
|
861
|
+
type: "object",
|
|
862
|
+
properties: {
|
|
863
|
+
input: {
|
|
864
|
+
type: "string",
|
|
865
|
+
description: "A Unix timestamp (seconds or milliseconds, as a number or numeric string) or a date string (ISO 8601, with or without a timezone offset). Omit to use the current server time."
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
]
|
|
871
|
+
};
|
|
872
|
+
const configuredLimits = [
|
|
873
|
+
queryLimits.documents !== void 0 && `${queryLimits.documents} documents`,
|
|
874
|
+
queryLimits.bytes !== void 0 && `${queryLimits.bytes} bytes in the results array`,
|
|
875
|
+
queryLimits.timeoutMS !== void 0 && `${queryLimits.timeoutMS} ms per MongoDB operation`
|
|
876
|
+
].filter(Boolean);
|
|
877
|
+
result.tools = result.tools.filter(
|
|
878
|
+
({ name }) => hasWritableConnection || !WRITE_OPERATIONS.has(name)
|
|
879
|
+
);
|
|
880
|
+
const { tools } = result;
|
|
881
|
+
for (const tool of tools) {
|
|
882
|
+
if (WRITE_OPERATIONS.has(tool.name)) {
|
|
883
|
+
tool.annotations = { ...tool.annotations, readOnlyHint: false };
|
|
884
|
+
if (connectionSelection) {
|
|
885
|
+
tool.description += " Select a writable connection with listConnections.";
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
if (tool.name === "aggregate" && !hasWritableConnection) {
|
|
889
|
+
tool.annotations = { ...tool.annotations, readOnlyHint: true };
|
|
890
|
+
}
|
|
891
|
+
if ((tool.name === "query" || tool.name === "aggregate") && configuredLimits.length) {
|
|
892
|
+
tool.description += ` Configured limits include ${configuredLimits.join(", ")}.`;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
if (connectionSelection) {
|
|
896
|
+
const connectionId = {
|
|
897
|
+
type: "string",
|
|
898
|
+
description: "Configured connection ID. Defaults to default."
|
|
899
|
+
};
|
|
900
|
+
const database = {
|
|
901
|
+
type: "string",
|
|
902
|
+
description: "Allowed database name. Defaults to the database in the connection URI."
|
|
903
|
+
};
|
|
904
|
+
for (const tool of tools) {
|
|
905
|
+
if (tool.name !== "convertTime") {
|
|
906
|
+
tool.inputSchema.properties = {
|
|
907
|
+
...tool.inputSchema.properties,
|
|
908
|
+
connectionId,
|
|
909
|
+
database
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
tools.push(
|
|
914
|
+
{
|
|
915
|
+
name: "listConnections",
|
|
916
|
+
description: "List configured connection IDs, default databases, and read-only policies.",
|
|
917
|
+
inputSchema: { type: "object", properties: {} }
|
|
918
|
+
},
|
|
919
|
+
{
|
|
920
|
+
name: "listDatabases",
|
|
921
|
+
description: "List existing authorized databases on a configured connection.",
|
|
922
|
+
inputSchema: { type: "object", properties: { connectionId } }
|
|
923
|
+
}
|
|
924
|
+
);
|
|
925
|
+
}
|
|
926
|
+
return result;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
// src/schemas/call.ts
|
|
930
|
+
var COLLECTION_OPERATIONS = [
|
|
931
|
+
"query",
|
|
932
|
+
"aggregate",
|
|
933
|
+
"update",
|
|
934
|
+
"insert",
|
|
935
|
+
"createIndex",
|
|
936
|
+
"count",
|
|
937
|
+
"listIndexes",
|
|
938
|
+
"collectionSchema"
|
|
939
|
+
];
|
|
940
|
+
var MAX_OBJECT_DEPTH = 100;
|
|
941
|
+
var WRITE_AGG_STAGES = /* @__PURE__ */ new Set(["$out", "$merge"]);
|
|
942
|
+
var SERVER_JS_AGG_OPERATORS = /* @__PURE__ */ new Set([
|
|
943
|
+
"$function",
|
|
944
|
+
"$accumulator",
|
|
945
|
+
"$where"
|
|
946
|
+
]);
|
|
947
|
+
var READONLY_FORBIDDEN_AGG_OPERATORS = /* @__PURE__ */ new Set([
|
|
948
|
+
...WRITE_AGG_STAGES,
|
|
949
|
+
...SERVER_JS_AGG_OPERATORS
|
|
950
|
+
]);
|
|
951
|
+
function findAggOperator(value, operators, depth = 0) {
|
|
952
|
+
if (depth > MAX_OBJECT_DEPTH) {
|
|
953
|
+
throw new Error(
|
|
954
|
+
`Object nesting exceeds the maximum depth of ${MAX_OBJECT_DEPTH}`
|
|
955
|
+
);
|
|
956
|
+
}
|
|
957
|
+
if (Array.isArray(value)) {
|
|
958
|
+
for (const item of value) {
|
|
959
|
+
const found = findAggOperator(item, operators, depth + 1);
|
|
960
|
+
if (found) return found;
|
|
961
|
+
}
|
|
962
|
+
return null;
|
|
963
|
+
}
|
|
964
|
+
if (value && typeof value === "object") {
|
|
965
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
966
|
+
if (operators.has(key)) {
|
|
967
|
+
return key;
|
|
968
|
+
}
|
|
969
|
+
const found = findAggOperator(nested, operators, depth + 1);
|
|
970
|
+
if (found) return found;
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
return null;
|
|
974
|
+
}
|
|
975
|
+
function assertNoServerJs(value) {
|
|
976
|
+
const operator = findAggOperator(value, SERVER_JS_AGG_OPERATORS);
|
|
977
|
+
if (operator) {
|
|
978
|
+
throw new Error(
|
|
979
|
+
`Server-side JavaScript operator '${operator}' is disabled by the current server policy.`
|
|
980
|
+
);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
function findCrossDbTarget(pipeline, connectedDb) {
|
|
984
|
+
const foreignDb = (target) => {
|
|
985
|
+
if (target && typeof target === "object" && "db" in target) {
|
|
986
|
+
const db = target.db;
|
|
987
|
+
if (typeof db === "string" && db && db !== connectedDb) return db;
|
|
988
|
+
}
|
|
989
|
+
return null;
|
|
990
|
+
};
|
|
991
|
+
for (const stage of pipeline) {
|
|
992
|
+
if (!stage || typeof stage !== "object") continue;
|
|
993
|
+
const s = stage;
|
|
994
|
+
const out = foreignDb(s.$out);
|
|
995
|
+
if (out) return out;
|
|
996
|
+
const merge = s.$merge;
|
|
997
|
+
const mergeInto = foreignDb(merge?.into ?? s.$merge);
|
|
998
|
+
if (mergeInto) return mergeInto;
|
|
999
|
+
const lookup = s.$lookup;
|
|
1000
|
+
const lookupFrom = foreignDb(lookup?.from);
|
|
1001
|
+
if (lookupFrom) return lookupFrom;
|
|
1002
|
+
}
|
|
1003
|
+
return null;
|
|
1004
|
+
}
|
|
1005
|
+
async function handleCallToolRequest({
|
|
1006
|
+
request,
|
|
1007
|
+
client,
|
|
1008
|
+
db,
|
|
1009
|
+
isReadOnlyMode,
|
|
1010
|
+
allowCrossDb = false,
|
|
1011
|
+
allowServerJs = false,
|
|
1012
|
+
connections: connections2,
|
|
1013
|
+
queryLimits = {},
|
|
1014
|
+
signal
|
|
1015
|
+
}) {
|
|
1016
|
+
const { name, arguments: args = {} } = request.params;
|
|
1017
|
+
const operation = name;
|
|
1018
|
+
const objectIdMode = args.objectIdMode || "auto";
|
|
1019
|
+
const filteredArgs = {};
|
|
1020
|
+
for (const [key, value] of Object.entries(args)) {
|
|
1021
|
+
if (key !== "objectIdMode") {
|
|
1022
|
+
filteredArgs[key] = value;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
if (args.sort) {
|
|
1026
|
+
args.sort = parseSort(args.sort);
|
|
1027
|
+
}
|
|
1028
|
+
Object.assign(args, filteredArgs);
|
|
1029
|
+
if (!connections2 || name !== "listConnections" && name !== "listDatabases") {
|
|
1030
|
+
validateOperation(operation);
|
|
1031
|
+
}
|
|
1032
|
+
try {
|
|
1033
|
+
signal?.throwIfAborted();
|
|
1034
|
+
if (connections2 && name === "listConnections") {
|
|
1035
|
+
return formatResponse({ connections: connections2.list() });
|
|
1036
|
+
}
|
|
1037
|
+
if (connections2 && name === "listDatabases") {
|
|
1038
|
+
return formatResponse({
|
|
1039
|
+
databases: await connections2.listDatabases(args.connectionId)
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
if (connections2 && operation !== "convertTime") {
|
|
1043
|
+
const target = await connections2.resolve(args);
|
|
1044
|
+
db = target.db;
|
|
1045
|
+
isReadOnlyMode = target.isReadOnlyMode;
|
|
1046
|
+
if (connections2.named && operation === "aggregate" && Array.isArray(args.pipeline)) {
|
|
1047
|
+
const allowed = connections2.allowedDatabases(target.connectionId);
|
|
1048
|
+
checkNamedAggregationDatabases(
|
|
1049
|
+
args.pipeline,
|
|
1050
|
+
db.databaseName,
|
|
1051
|
+
allowCrossDb ? allowed : [db.databaseName]
|
|
1052
|
+
);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
checkReadOnlyMode(operation, isReadOnlyMode);
|
|
1056
|
+
let collection = null;
|
|
1057
|
+
if (COLLECTION_OPERATIONS.includes(operation)) {
|
|
1058
|
+
const collectionName = args.collection;
|
|
1059
|
+
if (!collectionName) {
|
|
1060
|
+
throw new Error(
|
|
1061
|
+
`Collection name is required for '${operation}' operation`
|
|
1062
|
+
);
|
|
1063
|
+
}
|
|
1064
|
+
collection = db.collection(collectionName);
|
|
1065
|
+
validateCollection(collection);
|
|
1066
|
+
}
|
|
1067
|
+
signal?.throwIfAborted();
|
|
1068
|
+
if (collection && (operation === "listIndexes" || operation === "collectionSchema")) {
|
|
1069
|
+
const target = `${db.databaseName}.${collection.collectionName}`;
|
|
1070
|
+
try {
|
|
1071
|
+
const options = { signal, timeoutMS: queryLimits.timeoutMS };
|
|
1072
|
+
return formatResponse(
|
|
1073
|
+
operation === "listIndexes" ? { indexes: await collection.indexes(options) } : await buildCollectionSchema(db, collection, signal)
|
|
1074
|
+
);
|
|
1075
|
+
} catch (error) {
|
|
1076
|
+
signal?.throwIfAborted();
|
|
1077
|
+
return handleError(error, operation, target);
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
return await executeOperation(
|
|
1081
|
+
operation,
|
|
1082
|
+
collection,
|
|
1083
|
+
db,
|
|
1084
|
+
isReadOnlyMode,
|
|
1085
|
+
allowCrossDb,
|
|
1086
|
+
allowServerJs,
|
|
1087
|
+
args,
|
|
1088
|
+
objectIdMode,
|
|
1089
|
+
queryLimits,
|
|
1090
|
+
signal
|
|
1091
|
+
);
|
|
1092
|
+
} catch (error) {
|
|
1093
|
+
if (error instanceof ProtocolError3 || signal?.aborted) {
|
|
1094
|
+
throw error;
|
|
1095
|
+
}
|
|
1096
|
+
return toolExecutionError(
|
|
1097
|
+
error instanceof Error ? error.message : "Unknown error"
|
|
1098
|
+
);
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
function checkNamedAggregationDatabases(pipeline, database, allowed, depth = 0) {
|
|
1102
|
+
if (depth > MAX_OBJECT_DEPTH)
|
|
1103
|
+
throw new Error("Aggregation pipeline nesting is too deep");
|
|
1104
|
+
for (const stage of pipeline) {
|
|
1105
|
+
if (!stage || typeof stage !== "object") continue;
|
|
1106
|
+
const { $out, $merge, $lookup, $unionWith, $facet } = stage;
|
|
1107
|
+
for (const target of [$out, $merge, $lookup, $unionWith]) {
|
|
1108
|
+
if (!target || typeof target !== "object") continue;
|
|
1109
|
+
const value = target;
|
|
1110
|
+
for (const reference of [value, value.into, value.from]) {
|
|
1111
|
+
if (!reference || typeof reference !== "object" || !("db" in reference))
|
|
1112
|
+
continue;
|
|
1113
|
+
if (typeof reference.db !== "string" || !allowed.includes(reference.db)) {
|
|
1114
|
+
throw new Error(
|
|
1115
|
+
`Aggregation references a database outside the allowed scope of '${database}'`
|
|
1116
|
+
);
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
if (Array.isArray(value.pipeline))
|
|
1120
|
+
checkNamedAggregationDatabases(
|
|
1121
|
+
value.pipeline,
|
|
1122
|
+
database,
|
|
1123
|
+
allowed,
|
|
1124
|
+
depth + 1
|
|
1125
|
+
);
|
|
1126
|
+
}
|
|
1127
|
+
if ($facet && typeof $facet === "object") {
|
|
1128
|
+
for (const nested of Object.values($facet)) {
|
|
1129
|
+
if (Array.isArray(nested))
|
|
1130
|
+
checkNamedAggregationDatabases(nested, database, allowed, depth + 1);
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
async function executeOperation(operation, collection, db, isReadOnlyMode, allowCrossDb, allowServerJs, args, objectIdMode, queryLimits, signal) {
|
|
1136
|
+
switch (operation) {
|
|
1137
|
+
case "query":
|
|
1138
|
+
return handleQuery(
|
|
1139
|
+
collection,
|
|
1140
|
+
args,
|
|
1141
|
+
objectIdMode,
|
|
1142
|
+
allowServerJs && !isReadOnlyMode,
|
|
1143
|
+
queryLimits,
|
|
1144
|
+
signal
|
|
1145
|
+
);
|
|
1146
|
+
case "aggregate":
|
|
1147
|
+
return handleAggregate(
|
|
1148
|
+
collection,
|
|
1149
|
+
args,
|
|
1150
|
+
objectIdMode,
|
|
1151
|
+
isReadOnlyMode,
|
|
1152
|
+
allowCrossDb,
|
|
1153
|
+
allowServerJs,
|
|
1154
|
+
queryLimits,
|
|
1155
|
+
signal
|
|
1156
|
+
);
|
|
1157
|
+
case "update":
|
|
1158
|
+
return handleUpdate(
|
|
1159
|
+
collection,
|
|
1160
|
+
args,
|
|
1161
|
+
objectIdMode,
|
|
1162
|
+
allowServerJs,
|
|
1163
|
+
signal
|
|
1164
|
+
);
|
|
1165
|
+
case "serverInfo":
|
|
1166
|
+
return handleServerInfo(db, isReadOnlyMode, args, signal);
|
|
1167
|
+
case "insert":
|
|
1168
|
+
return handleInsert(collection, args, objectIdMode, signal);
|
|
1169
|
+
case "createIndex":
|
|
1170
|
+
return handleCreateIndex(collection, args, objectIdMode, signal);
|
|
1171
|
+
case "count":
|
|
1172
|
+
return handleCount(
|
|
1173
|
+
collection,
|
|
1174
|
+
args,
|
|
1175
|
+
objectIdMode,
|
|
1176
|
+
allowServerJs && !isReadOnlyMode,
|
|
1177
|
+
signal
|
|
1178
|
+
);
|
|
1179
|
+
case "listCollections":
|
|
1180
|
+
return handleListCollections(db, args, objectIdMode, signal);
|
|
1181
|
+
case "convertTime":
|
|
1182
|
+
return handleConvertTime(args);
|
|
1183
|
+
default:
|
|
1184
|
+
throw new ProtocolError3(
|
|
1185
|
+
ProtocolErrorCode3.InvalidParams,
|
|
1186
|
+
`Unknown tool: ${operation}`
|
|
1187
|
+
);
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
function validateOperation(operation) {
|
|
1191
|
+
const validOperations = [
|
|
1192
|
+
"query",
|
|
1193
|
+
"aggregate",
|
|
1194
|
+
"update",
|
|
1195
|
+
"serverInfo",
|
|
1196
|
+
"insert",
|
|
1197
|
+
"createIndex",
|
|
1198
|
+
"count",
|
|
1199
|
+
"listCollections",
|
|
1200
|
+
"listIndexes",
|
|
1201
|
+
"collectionSchema",
|
|
1202
|
+
"convertTime"
|
|
1203
|
+
];
|
|
1204
|
+
if (!validOperations.includes(operation)) {
|
|
1205
|
+
throw new ProtocolError3(
|
|
1206
|
+
ProtocolErrorCode3.InvalidParams,
|
|
1207
|
+
`Unknown tool: ${operation}`
|
|
1208
|
+
);
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
function validateCollection(collection) {
|
|
1212
|
+
if (!collection.collectionName) {
|
|
1213
|
+
throw new Error("Collection name cannot be empty");
|
|
1214
|
+
}
|
|
1215
|
+
if (collection.collectionName.toLowerCase().startsWith("system.")) {
|
|
1216
|
+
throw new Error("Access to system collections is not allowed");
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
function checkReadOnlyMode(operation, isReadOnlyMode) {
|
|
1220
|
+
if (isReadOnlyMode && WRITE_OPERATIONS.has(operation)) {
|
|
1221
|
+
throw new Error(
|
|
1222
|
+
`ReadonlyError: Operation '${operation}' is not allowed in read-only mode`
|
|
1223
|
+
);
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
function parseSort(sort) {
|
|
1227
|
+
if (!sort) return null;
|
|
1228
|
+
if (typeof sort !== "object" || sort === null || Array.isArray(sort)) {
|
|
1229
|
+
return null;
|
|
1230
|
+
}
|
|
1231
|
+
const validSort = {};
|
|
1232
|
+
for (const [key, value] of Object.entries(sort)) {
|
|
1233
|
+
if (typeof value === "number" && (value === 1 || value === -1)) {
|
|
1234
|
+
validSort[key] = value;
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
return Object.keys(validSort).length > 0 ? validSort : null;
|
|
1238
|
+
}
|
|
1239
|
+
function parseFilter(filter, objectIdMode = "auto") {
|
|
1240
|
+
if (!filter) {
|
|
1241
|
+
return {};
|
|
1242
|
+
}
|
|
1243
|
+
if (typeof filter === "string") {
|
|
1244
|
+
try {
|
|
1245
|
+
return processObjectIdInFilter(JSON.parse(filter), objectIdMode);
|
|
1246
|
+
} catch {
|
|
1247
|
+
throw new Error("Invalid filter format: must be a valid JSON object");
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
if (typeof filter === "object" && filter !== null && !Array.isArray(filter)) {
|
|
1251
|
+
return processObjectIdInFilter(
|
|
1252
|
+
filter,
|
|
1253
|
+
objectIdMode
|
|
1254
|
+
);
|
|
1255
|
+
}
|
|
1256
|
+
throw new Error("Query filter must be a plain object or ObjectId");
|
|
1257
|
+
}
|
|
1258
|
+
function isObjectIdField(fieldName) {
|
|
1259
|
+
const lowerFieldName = fieldName.toLowerCase();
|
|
1260
|
+
return lowerFieldName === "_id" || lowerFieldName === "id" || lowerFieldName.endsWith("id") || lowerFieldName.endsWith("_id");
|
|
1261
|
+
}
|
|
1262
|
+
function processObjectIdInFilter(filter, objectIdMode = "auto", depth = 0) {
|
|
1263
|
+
if (depth > MAX_OBJECT_DEPTH) {
|
|
1264
|
+
throw new Error(
|
|
1265
|
+
`Object nesting exceeds the maximum depth of ${MAX_OBJECT_DEPTH}`
|
|
1266
|
+
);
|
|
1267
|
+
}
|
|
1268
|
+
if (objectIdMode === "none") {
|
|
1269
|
+
const result2 = {};
|
|
1270
|
+
for (const [key, value] of Object.entries(filter)) {
|
|
1271
|
+
if (typeof value === "string" && isISODateString(value)) {
|
|
1272
|
+
result2[key] = new Date(value);
|
|
1273
|
+
} else if (typeof value === "string" && value.startsWith("ISODate(") && value.endsWith(")")) {
|
|
1274
|
+
const dateString = value.substring(8, value.length - 2);
|
|
1275
|
+
if (isISODateString(dateString)) {
|
|
1276
|
+
result2[key] = new Date(dateString);
|
|
1277
|
+
} else {
|
|
1278
|
+
result2[key] = value;
|
|
1279
|
+
}
|
|
1280
|
+
} else if (typeof value === "object" && value !== null) {
|
|
1281
|
+
if (Array.isArray(value)) {
|
|
1282
|
+
result2[key] = value.map((item) => {
|
|
1283
|
+
if (typeof item === "string" && isISODateString(item)) {
|
|
1284
|
+
return new Date(item);
|
|
1285
|
+
} else if (typeof item === "string" && item.startsWith("ISODate(") && item.endsWith(")")) {
|
|
1286
|
+
const dateString = item.substring(8, item.length - 2);
|
|
1287
|
+
return isISODateString(dateString) ? new Date(dateString) : item;
|
|
1288
|
+
}
|
|
1289
|
+
return item;
|
|
1290
|
+
});
|
|
1291
|
+
} else {
|
|
1292
|
+
result2[key] = processObjectIdInFilter(
|
|
1293
|
+
value,
|
|
1294
|
+
"none",
|
|
1295
|
+
depth + 1
|
|
1296
|
+
);
|
|
1297
|
+
}
|
|
1298
|
+
} else {
|
|
1299
|
+
result2[key] = value;
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
return result2;
|
|
1303
|
+
}
|
|
1304
|
+
const result = {};
|
|
1305
|
+
for (const [key, value] of Object.entries(filter)) {
|
|
1306
|
+
if (typeof value === "string" && isObjectIdString(value)) {
|
|
1307
|
+
if (objectIdMode === "force" || objectIdMode === "auto" && isObjectIdField(key)) {
|
|
1308
|
+
result[key] = new ObjectId2(value);
|
|
1309
|
+
} else {
|
|
1310
|
+
result[key] = value;
|
|
1311
|
+
}
|
|
1312
|
+
} else if (typeof value === "string" && isISODateString(value)) {
|
|
1313
|
+
result[key] = new Date(value);
|
|
1314
|
+
} else if (typeof value === "string" && value.startsWith("ISODate(") && value.endsWith(")")) {
|
|
1315
|
+
const dateString = value.substring(8, value.length - 2);
|
|
1316
|
+
if (isISODateString(dateString)) {
|
|
338
1317
|
result[key] = new Date(dateString);
|
|
339
1318
|
} else {
|
|
340
1319
|
result[key] = value;
|
|
@@ -343,7 +1322,7 @@ function processObjectIdInFilter(filter, objectIdMode = "auto", depth = 0) {
|
|
|
343
1322
|
if (Array.isArray(value)) {
|
|
344
1323
|
result[key] = value.map((item) => {
|
|
345
1324
|
if (typeof item === "string" && isObjectIdString(item) && (objectIdMode === "force" || objectIdMode === "auto" && isObjectIdField(key))) {
|
|
346
|
-
return new
|
|
1325
|
+
return new ObjectId2(item);
|
|
347
1326
|
} else if (typeof item === "string" && isISODateString(item)) {
|
|
348
1327
|
return new Date(item);
|
|
349
1328
|
} else if (typeof item === "string" && item.startsWith("ISODate(") && item.endsWith(")")) {
|
|
@@ -361,1294 +1340,760 @@ function processObjectIdInFilter(filter, objectIdMode = "auto", depth = 0) {
|
|
|
361
1340
|
}
|
|
362
1341
|
} else {
|
|
363
1342
|
result[key] = value;
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
return result;
|
|
367
|
-
}
|
|
368
|
-
function isObjectIdString(str) {
|
|
369
|
-
return /^[0-9a-fA-F]{24}$/.test(str);
|
|
370
|
-
}
|
|
371
|
-
function isISODateString(str) {
|
|
372
|
-
return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,3})?(Z|[+-]\d{2}:\d{2})$/.test(
|
|
373
|
-
str
|
|
374
|
-
);
|
|
375
|
-
}
|
|
376
|
-
function formatResponse(data) {
|
|
377
|
-
return {
|
|
378
|
-
content: [
|
|
379
|
-
{
|
|
380
|
-
type: "text",
|
|
381
|
-
text: JSON.stringify(data, null, 2)
|
|
382
|
-
}
|
|
383
|
-
]
|
|
384
|
-
};
|
|
385
|
-
}
|
|
386
|
-
function toolExecutionError(message) {
|
|
387
|
-
return {
|
|
388
|
-
content: [{ type: "text", text: message }],
|
|
389
|
-
isError: true
|
|
390
|
-
};
|
|
391
|
-
}
|
|
392
|
-
function handleError(error, operation, collectionName) {
|
|
393
|
-
if (error instanceof Error && error.name === "AbortError") {
|
|
394
|
-
throw error;
|
|
395
|
-
}
|
|
396
|
-
const context = collectionName ? `collection ${collectionName}` : "operation";
|
|
397
|
-
const message = error instanceof Error ? error.message : "Unknown error";
|
|
398
|
-
return toolExecutionError(`Failed to ${operation} ${context}: ${message}`);
|
|
399
|
-
}
|
|
400
|
-
async function handleQuery(collection, args, objectIdMode = "auto", allowServerJs = false, signal) {
|
|
401
|
-
if (!collection) {
|
|
402
|
-
throw new Error("Collection is required for query operation");
|
|
403
|
-
}
|
|
404
|
-
const { filter, projection, explain, sort } = args;
|
|
405
|
-
const limit = args.limit || 10;
|
|
406
|
-
const skip = args.skip || 0;
|
|
407
|
-
const queryFilter = parseFilter(filter, objectIdMode);
|
|
408
|
-
if (!allowServerJs) {
|
|
409
|
-
assertNoServerJs(queryFilter);
|
|
410
|
-
}
|
|
411
|
-
try {
|
|
412
|
-
if (explain) {
|
|
413
|
-
signal?.throwIfAborted();
|
|
414
|
-
const explainResult = await collection.find(queryFilter, {
|
|
415
|
-
projection,
|
|
416
|
-
limit,
|
|
417
|
-
skip,
|
|
418
|
-
sort
|
|
419
|
-
}).explain(explain);
|
|
420
|
-
return formatResponse(explainResult);
|
|
421
|
-
}
|
|
422
|
-
signal?.throwIfAborted();
|
|
423
|
-
const [total, results] = await Promise.all([
|
|
424
|
-
collection.countDocuments(queryFilter),
|
|
425
|
-
collection.find(queryFilter, {
|
|
426
|
-
projection,
|
|
427
|
-
limit,
|
|
428
|
-
skip,
|
|
429
|
-
sort
|
|
430
|
-
}).toArray()
|
|
431
|
-
]);
|
|
432
|
-
return formatResponse({
|
|
433
|
-
results,
|
|
434
|
-
metadata: {
|
|
435
|
-
total,
|
|
436
|
-
returned: results.length,
|
|
437
|
-
skip,
|
|
438
|
-
limit,
|
|
439
|
-
hasMore: skip + results.length < total
|
|
440
|
-
}
|
|
441
|
-
});
|
|
442
|
-
} catch (error) {
|
|
443
|
-
return handleError(error, "query", collection.collectionName);
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
async function handleAggregate(collection, args, objectIdMode = "auto", isReadOnlyMode = false, allowCrossDb = false, allowServerJs = false, signal) {
|
|
447
|
-
if (!collection) {
|
|
448
|
-
throw new Error("Collection is required for aggregate operation");
|
|
449
|
-
}
|
|
450
|
-
const { pipeline, explain } = args;
|
|
451
|
-
if (!Array.isArray(pipeline)) {
|
|
452
|
-
throw new Error("Pipeline must be an array");
|
|
453
|
-
}
|
|
454
|
-
if (isReadOnlyMode) {
|
|
455
|
-
const forbidden = findAggOperator(
|
|
456
|
-
pipeline,
|
|
457
|
-
READONLY_FORBIDDEN_AGG_OPERATORS
|
|
458
|
-
);
|
|
459
|
-
if (forbidden) {
|
|
460
|
-
throw new Error(
|
|
461
|
-
`ReadonlyError: Aggregation operator '${forbidden}' is not allowed in read-only mode`
|
|
462
|
-
);
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
if (!allowServerJs) {
|
|
466
|
-
assertNoServerJs(pipeline);
|
|
467
|
-
}
|
|
468
|
-
if (!allowCrossDb) {
|
|
469
|
-
const foreignDb = findCrossDbTarget(pipeline, collection.dbName);
|
|
470
|
-
if (foreignDb) {
|
|
471
|
-
throw new Error(
|
|
472
|
-
`Cross-database access to '${foreignDb}' is not allowed (connected database is '${collection.dbName}'). Enable it with --allow-cross-db or MCP_MONGODB_ALLOW_CROSS_DB=true.`
|
|
473
|
-
);
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
const processedPipeline = pipeline.map((stage) => {
|
|
477
|
-
if (typeof stage === "object" && stage !== null) {
|
|
478
|
-
return processObjectIdInFilter(
|
|
479
|
-
stage,
|
|
480
|
-
objectIdMode
|
|
481
|
-
);
|
|
482
|
-
}
|
|
483
|
-
return stage;
|
|
484
|
-
});
|
|
485
|
-
try {
|
|
486
|
-
if (explain) {
|
|
487
|
-
signal?.throwIfAborted();
|
|
488
|
-
const explainResult = await collection.aggregate(processedPipeline, {
|
|
489
|
-
explain: {
|
|
490
|
-
verbosity: explain
|
|
491
|
-
}
|
|
492
|
-
}).toArray();
|
|
493
|
-
return formatResponse(explainResult);
|
|
494
|
-
}
|
|
495
|
-
signal?.throwIfAborted();
|
|
496
|
-
const results = await collection.aggregate(processedPipeline).toArray();
|
|
497
|
-
return formatResponse({
|
|
498
|
-
results,
|
|
499
|
-
metadata: {
|
|
500
|
-
returned: results.length
|
|
501
|
-
}
|
|
502
|
-
});
|
|
503
|
-
} catch (error) {
|
|
504
|
-
return handleError(error, "aggregate", collection.collectionName);
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
async function handleUpdate(collection, args, objectIdMode = "auto", allowServerJs = false, signal) {
|
|
508
|
-
if (!collection) {
|
|
509
|
-
throw new Error("Collection is required for update operation");
|
|
510
|
-
}
|
|
511
|
-
const { filter, update, upsert, multi } = args;
|
|
512
|
-
const queryFilter = parseFilter(filter, objectIdMode);
|
|
513
|
-
if (!allowServerJs) {
|
|
514
|
-
assertNoServerJs(queryFilter);
|
|
515
|
-
}
|
|
516
|
-
let processedUpdate = update;
|
|
517
|
-
if (update && typeof update === "object" && !Array.isArray(update)) {
|
|
518
|
-
processedUpdate = processObjectIdInFilter(
|
|
519
|
-
update,
|
|
520
|
-
objectIdMode
|
|
521
|
-
);
|
|
522
|
-
}
|
|
523
|
-
if (!processedUpdate || typeof processedUpdate !== "object" || Array.isArray(processedUpdate)) {
|
|
524
|
-
throw new Error("Update must be a valid MongoDB update document");
|
|
525
|
-
}
|
|
526
|
-
const validUpdateOperators = [
|
|
527
|
-
"$set",
|
|
528
|
-
"$unset",
|
|
529
|
-
"$inc",
|
|
530
|
-
"$push",
|
|
531
|
-
"$pull",
|
|
532
|
-
"$addToSet",
|
|
533
|
-
"$pop",
|
|
534
|
-
"$rename",
|
|
535
|
-
"$mul"
|
|
536
|
-
];
|
|
537
|
-
const hasValidOperator = Object.keys(processedUpdate).some(
|
|
538
|
-
(key) => validUpdateOperators.includes(key)
|
|
539
|
-
);
|
|
540
|
-
if (!hasValidOperator) {
|
|
541
|
-
throw new Error(
|
|
542
|
-
"Update must include at least one valid update operator ($set, $unset, etc.)"
|
|
543
|
-
);
|
|
544
|
-
}
|
|
545
|
-
try {
|
|
546
|
-
const options = {
|
|
547
|
-
upsert: !!upsert,
|
|
548
|
-
multi: !!multi
|
|
549
|
-
};
|
|
550
|
-
const updateMethod = options.multi ? "updateMany" : "updateOne";
|
|
551
|
-
signal?.throwIfAborted();
|
|
552
|
-
const result = await collection[updateMethod](
|
|
553
|
-
queryFilter,
|
|
554
|
-
processedUpdate,
|
|
555
|
-
options
|
|
556
|
-
);
|
|
557
|
-
return formatResponse({
|
|
558
|
-
matchedCount: result.matchedCount,
|
|
559
|
-
modifiedCount: result.modifiedCount,
|
|
560
|
-
upsertedCount: result.upsertedCount,
|
|
561
|
-
upsertedId: result.upsertedId
|
|
562
|
-
});
|
|
563
|
-
} catch (error) {
|
|
564
|
-
return handleError(error, "update", collection.collectionName);
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
async function handleServerInfo(db, isReadOnlyMode, args, signal) {
|
|
568
|
-
const { includeDebugInfo } = args;
|
|
569
|
-
try {
|
|
570
|
-
signal?.throwIfAborted();
|
|
571
|
-
const buildInfo = await db.command({ buildInfo: 1 });
|
|
572
|
-
const serverInfo = {
|
|
573
|
-
version: buildInfo.version,
|
|
574
|
-
gitVersion: buildInfo.gitVersion,
|
|
575
|
-
storageEngines: buildInfo.storageEngines,
|
|
576
|
-
maxBsonObjectSize: buildInfo.maxBsonObjectSize,
|
|
577
|
-
bits: buildInfo.bits,
|
|
578
|
-
ok: buildInfo.ok,
|
|
579
|
-
connectionInfo: {
|
|
580
|
-
readOnlyMode: isReadOnlyMode
|
|
581
|
-
}
|
|
582
|
-
};
|
|
583
|
-
if (includeDebugInfo) {
|
|
584
|
-
signal?.throwIfAborted();
|
|
585
|
-
const serverStatus = await db.command({ serverStatus: 1 });
|
|
586
|
-
serverInfo.modules = buildInfo.modules;
|
|
587
|
-
serverInfo.allocator = buildInfo.allocator;
|
|
588
|
-
serverInfo.javascriptEngine = buildInfo.javascriptEngine;
|
|
589
|
-
serverInfo.sysInfo = buildInfo.sysInfo;
|
|
590
|
-
serverInfo.debug = buildInfo.debug;
|
|
591
|
-
serverInfo.openssl = buildInfo.openssl;
|
|
592
|
-
serverInfo.buildEnvironment = buildInfo.buildEnvironment;
|
|
593
|
-
serverInfo.status = {
|
|
594
|
-
host: serverStatus.host,
|
|
595
|
-
version: serverStatus.version,
|
|
596
|
-
process: serverStatus.process,
|
|
597
|
-
pid: serverStatus.pid,
|
|
598
|
-
uptime: serverStatus.uptime,
|
|
599
|
-
uptimeMillis: serverStatus.uptimeMillis,
|
|
600
|
-
uptimeEstimate: serverStatus.uptimeEstimate,
|
|
601
|
-
localTime: serverStatus.localTime,
|
|
602
|
-
connections: serverStatus.connections,
|
|
603
|
-
network: serverStatus.network,
|
|
604
|
-
memory: serverStatus.mem,
|
|
605
|
-
storageEngine: serverStatus.storageEngine,
|
|
606
|
-
security: serverStatus.security
|
|
607
|
-
};
|
|
608
|
-
}
|
|
609
|
-
return formatResponse(serverInfo);
|
|
610
|
-
} catch (error) {
|
|
611
|
-
return handleError(error, "get server information");
|
|
1343
|
+
}
|
|
612
1344
|
}
|
|
1345
|
+
return result;
|
|
613
1346
|
}
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
1347
|
+
function isObjectIdString(str) {
|
|
1348
|
+
return /^[0-9a-fA-F]{24}$/.test(str);
|
|
1349
|
+
}
|
|
1350
|
+
function isISODateString(str) {
|
|
1351
|
+
return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,3})?(Z|[+-]\d{2}:\d{2})$/.test(
|
|
1352
|
+
str
|
|
1353
|
+
);
|
|
1354
|
+
}
|
|
1355
|
+
function formatResponse(data) {
|
|
1356
|
+
return {
|
|
1357
|
+
content: [
|
|
1358
|
+
{
|
|
1359
|
+
type: "text",
|
|
1360
|
+
text: JSON.stringify(data, null, 2)
|
|
1361
|
+
}
|
|
1362
|
+
]
|
|
1363
|
+
};
|
|
1364
|
+
}
|
|
1365
|
+
function toolExecutionError(message) {
|
|
1366
|
+
return {
|
|
1367
|
+
content: [{ type: "text", text: message }],
|
|
1368
|
+
isError: true
|
|
1369
|
+
};
|
|
1370
|
+
}
|
|
1371
|
+
function handleError(error, operation, collectionName) {
|
|
1372
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
1373
|
+
throw error;
|
|
621
1374
|
}
|
|
622
|
-
|
|
623
|
-
|
|
1375
|
+
const context = collectionName ? `collection ${collectionName}` : "operation";
|
|
1376
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1377
|
+
return toolExecutionError(`Failed to ${operation} ${context}: ${message}`);
|
|
1378
|
+
}
|
|
1379
|
+
async function handleQuery(collection, args, objectIdMode = "auto", allowServerJs = false, limits = {}, signal) {
|
|
1380
|
+
if (!collection) {
|
|
1381
|
+
throw new Error("Collection is required for query operation");
|
|
624
1382
|
}
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
1383
|
+
const { filter, projection, explain, sort, includeTotal = true } = args;
|
|
1384
|
+
if (typeof includeTotal !== "boolean")
|
|
1385
|
+
throw new Error("includeTotal must be a boolean");
|
|
1386
|
+
const limit = args.limit || 10;
|
|
1387
|
+
const skip = args.skip || 0;
|
|
1388
|
+
const queryFilter = parseFilter(filter, objectIdMode);
|
|
1389
|
+
if (!allowServerJs) {
|
|
1390
|
+
assertNoServerJs(queryFilter);
|
|
629
1391
|
}
|
|
630
|
-
const processedDocuments = documents.map(
|
|
631
|
-
(doc) => processObjectIdInFilter(doc, objectIdMode)
|
|
632
|
-
);
|
|
633
1392
|
try {
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
1393
|
+
if (explain) {
|
|
1394
|
+
signal?.throwIfAborted();
|
|
1395
|
+
const explainResult = await collection.find(queryFilter, {
|
|
1396
|
+
projection,
|
|
1397
|
+
limit,
|
|
1398
|
+
skip,
|
|
1399
|
+
sort,
|
|
1400
|
+
signal,
|
|
1401
|
+
timeoutMS: limits.timeoutMS
|
|
1402
|
+
}).explain(explain);
|
|
1403
|
+
return formatResponse(explainResult);
|
|
1404
|
+
}
|
|
640
1405
|
signal?.throwIfAborted();
|
|
641
|
-
const
|
|
642
|
-
|
|
643
|
-
|
|
1406
|
+
const bounded = limits.documents !== void 0 || limits.bytes !== void 0 || !includeTotal;
|
|
1407
|
+
const pageSize = Math.abs(limit);
|
|
1408
|
+
if (bounded && !Number.isSafeInteger(limit)) {
|
|
1409
|
+
throw new Error("limit must be an integer");
|
|
1410
|
+
}
|
|
1411
|
+
const pageRows = Math.min(pageSize, limits.documents ?? Infinity);
|
|
1412
|
+
const fetchLimit = bounded ? Math.sign(limit) * Math.min(pageRows + 1, Number.MAX_SAFE_INTEGER) : limit;
|
|
1413
|
+
const cancelPeer = new AbortController();
|
|
1414
|
+
const operationSignal = signal ? AbortSignal.any([signal, cancelPeer.signal]) : cancelPeer.signal;
|
|
1415
|
+
const options = { signal: operationSignal, timeoutMS: limits.timeoutMS };
|
|
1416
|
+
const cursor = collection.find(queryFilter, {
|
|
1417
|
+
projection,
|
|
1418
|
+
limit: fetchLimit,
|
|
1419
|
+
skip,
|
|
1420
|
+
sort,
|
|
1421
|
+
...options
|
|
1422
|
+
});
|
|
1423
|
+
const read = bounded ? readResultPage(cursor, limits, pageSize, operationSignal) : cursor.toArray().then((results) => ({
|
|
1424
|
+
results,
|
|
1425
|
+
hasMore: false,
|
|
1426
|
+
truncated: void 0
|
|
1427
|
+
}));
|
|
1428
|
+
const count = includeTotal ? collection.countDocuments(queryFilter, options) : void 0;
|
|
1429
|
+
const [total, page] = await Promise.all([count, read]).catch(
|
|
1430
|
+
async (error) => {
|
|
1431
|
+
cancelPeer.abort(error);
|
|
1432
|
+
await Promise.allSettled([count, read]);
|
|
1433
|
+
throw error;
|
|
1434
|
+
}
|
|
644
1435
|
);
|
|
645
1436
|
return formatResponse({
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
1437
|
+
results: page.results,
|
|
1438
|
+
metadata: {
|
|
1439
|
+
...total !== void 0 ? { total } : {},
|
|
1440
|
+
returned: page.results.length,
|
|
1441
|
+
skip,
|
|
1442
|
+
limit,
|
|
1443
|
+
hasMore: total === void 0 ? page.hasMore : page.hasMore || skip + page.results.length < total,
|
|
1444
|
+
...page.truncated ? { truncated: page.truncated } : {}
|
|
1445
|
+
}
|
|
649
1446
|
});
|
|
650
1447
|
} catch (error) {
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
return formatResponse({
|
|
654
|
-
error: "Bulk write error occurred",
|
|
655
|
-
writeErrors: bulkError.writeErrors,
|
|
656
|
-
insertedCount: bulkError.result?.nInserted || 0,
|
|
657
|
-
failedCount: bulkError.result?.nFailedInserts || 0
|
|
658
|
-
});
|
|
659
|
-
}
|
|
660
|
-
return handleError(error, "insert", collection.collectionName);
|
|
1448
|
+
signal?.throwIfAborted();
|
|
1449
|
+
return handleError(error, "query", collection.collectionName);
|
|
661
1450
|
}
|
|
662
1451
|
}
|
|
663
|
-
async function
|
|
1452
|
+
async function handleAggregate(collection, args, objectIdMode = "auto", isReadOnlyMode = false, allowCrossDb = false, allowServerJs = false, limits = {}, signal) {
|
|
664
1453
|
if (!collection) {
|
|
665
|
-
throw new Error("Collection is required for
|
|
1454
|
+
throw new Error("Collection is required for aggregate operation");
|
|
666
1455
|
}
|
|
667
|
-
const {
|
|
668
|
-
if (!Array.isArray(
|
|
669
|
-
throw new Error("
|
|
1456
|
+
const { pipeline, explain } = args;
|
|
1457
|
+
if (!Array.isArray(pipeline)) {
|
|
1458
|
+
throw new Error("Pipeline must be an array");
|
|
670
1459
|
}
|
|
671
|
-
if (
|
|
672
|
-
|
|
673
|
-
|
|
1460
|
+
if (isReadOnlyMode) {
|
|
1461
|
+
const forbidden = findAggOperator(
|
|
1462
|
+
pipeline,
|
|
1463
|
+
READONLY_FORBIDDEN_AGG_OPERATORS
|
|
674
1464
|
);
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
}
|
|
679
|
-
const processedIndexes = indexes.map((index) => {
|
|
680
|
-
if (index && typeof index === "object") {
|
|
681
|
-
return processObjectIdInFilter(
|
|
682
|
-
index,
|
|
683
|
-
objectIdMode
|
|
1465
|
+
if (forbidden) {
|
|
1466
|
+
throw new Error(
|
|
1467
|
+
`ReadonlyError: Aggregation operator '${forbidden}' is not allowed in read-only mode`
|
|
684
1468
|
);
|
|
685
1469
|
}
|
|
686
|
-
return index;
|
|
687
|
-
});
|
|
688
|
-
try {
|
|
689
|
-
const indexOptions = {
|
|
690
|
-
commitQuorum
|
|
691
|
-
};
|
|
692
|
-
signal?.throwIfAborted();
|
|
693
|
-
const createdIndexes = await collection.createIndexes(
|
|
694
|
-
processedIndexes,
|
|
695
|
-
indexOptions
|
|
696
|
-
);
|
|
697
|
-
return formatResponse({
|
|
698
|
-
acknowledged: true,
|
|
699
|
-
createdIndexes,
|
|
700
|
-
indexCount: createdIndexes.length
|
|
701
|
-
});
|
|
702
|
-
} catch (error) {
|
|
703
|
-
return handleError(error, "create indexes", collection.collectionName);
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
async function handleCount(collection, args, objectIdMode = "auto", allowServerJs = false, signal) {
|
|
707
|
-
if (!collection) {
|
|
708
|
-
throw new Error("Collection is required for count operation");
|
|
709
1470
|
}
|
|
710
|
-
const { query, limit, skip, hint, readConcern, maxTimeMS, collation } = args;
|
|
711
|
-
const countQuery = parseFilter(query, objectIdMode);
|
|
712
1471
|
if (!allowServerJs) {
|
|
713
|
-
assertNoServerJs(
|
|
1472
|
+
assertNoServerJs(pipeline);
|
|
714
1473
|
}
|
|
715
|
-
|
|
716
|
-
const
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
maxTimeMS: typeof maxTimeMS === "number" ? maxTimeMS : void 0,
|
|
722
|
-
collation: typeof collation === "object" && collation !== null ? collation : void 0
|
|
723
|
-
};
|
|
724
|
-
for (const key of Object.keys(options)) {
|
|
725
|
-
if (options[key] === void 0) {
|
|
726
|
-
delete options[key];
|
|
727
|
-
}
|
|
1474
|
+
if (!allowCrossDb) {
|
|
1475
|
+
const foreignDb = findCrossDbTarget(pipeline, collection.dbName);
|
|
1476
|
+
if (foreignDb) {
|
|
1477
|
+
throw new Error(
|
|
1478
|
+
`Cross-database access to '${foreignDb}' is not allowed (connected database is '${collection.dbName}'). Enable it with --allow-cross-db or MCP_MONGODB_ALLOW_CROSS_DB=true.`
|
|
1479
|
+
);
|
|
728
1480
|
}
|
|
729
|
-
signal?.throwIfAborted();
|
|
730
|
-
const count = await collection.countDocuments(countQuery, options);
|
|
731
|
-
return formatResponse({
|
|
732
|
-
count,
|
|
733
|
-
ok: 1
|
|
734
|
-
});
|
|
735
|
-
} catch (error) {
|
|
736
|
-
return handleError(error, "count documents", collection.collectionName);
|
|
737
1481
|
}
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
} else if (typeof input === "number") {
|
|
745
|
-
date = new Date(Math.abs(input) < 1e12 ? input * 1e3 : input);
|
|
746
|
-
} else if (typeof input === "string") {
|
|
747
|
-
const trimmed = input.trim();
|
|
748
|
-
if (/^-?\d+$/.test(trimmed)) {
|
|
749
|
-
const n = Number(trimmed);
|
|
750
|
-
date = new Date(Math.abs(n) < 1e12 ? n * 1e3 : n);
|
|
751
|
-
} else {
|
|
752
|
-
date = new Date(trimmed);
|
|
1482
|
+
const processedPipeline = pipeline.map((stage) => {
|
|
1483
|
+
if (typeof stage === "object" && stage !== null) {
|
|
1484
|
+
return processObjectIdInFilter(
|
|
1485
|
+
stage,
|
|
1486
|
+
objectIdMode
|
|
1487
|
+
);
|
|
753
1488
|
}
|
|
754
|
-
|
|
755
|
-
throw new Error(
|
|
756
|
-
"convertTime 'input' must be a Unix timestamp (number) or a date string"
|
|
757
|
-
);
|
|
758
|
-
}
|
|
759
|
-
if (Number.isNaN(date.getTime())) {
|
|
760
|
-
throw new Error(
|
|
761
|
-
`Could not interpret '${String(input)}' as a date or Unix timestamp`
|
|
762
|
-
);
|
|
763
|
-
}
|
|
764
|
-
const offsetMinutes = -date.getTimezoneOffset();
|
|
765
|
-
const sign = offsetMinutes >= 0 ? "+" : "-";
|
|
766
|
-
const absMinutes = Math.abs(offsetMinutes);
|
|
767
|
-
const offsetLabel = `${sign}${String(Math.floor(absMinutes / 60)).padStart(
|
|
768
|
-
2,
|
|
769
|
-
"0"
|
|
770
|
-
)}:${String(absMinutes % 60).padStart(2, "0")}`;
|
|
771
|
-
return formatResponse({
|
|
772
|
-
iso: date.toISOString(),
|
|
773
|
-
utc: date.toUTCString(),
|
|
774
|
-
unixSeconds: Math.floor(date.getTime() / 1e3),
|
|
775
|
-
unixMillis: date.getTime(),
|
|
776
|
-
serverLocal: date.toString(),
|
|
777
|
-
serverTimeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
778
|
-
serverUtcOffset: offsetLabel
|
|
1489
|
+
return stage;
|
|
779
1490
|
});
|
|
780
|
-
}
|
|
781
|
-
async function handleListCollections(db, args, objectIdMode = "auto", signal) {
|
|
782
|
-
const { nameOnly, filter } = args;
|
|
783
|
-
const skip = args.skip || 0;
|
|
784
|
-
const limit = args.limit || 20;
|
|
785
1491
|
try {
|
|
786
|
-
|
|
1492
|
+
if (explain) {
|
|
1493
|
+
signal?.throwIfAborted();
|
|
1494
|
+
const explainResult = await collection.aggregate(processedPipeline, {
|
|
1495
|
+
explain: {
|
|
1496
|
+
verbosity: explain
|
|
1497
|
+
},
|
|
1498
|
+
signal,
|
|
1499
|
+
timeoutMS: limits.timeoutMS
|
|
1500
|
+
}).toArray();
|
|
1501
|
+
return formatResponse(explainResult);
|
|
1502
|
+
}
|
|
787
1503
|
signal?.throwIfAborted();
|
|
788
|
-
const
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
returned:
|
|
797
|
-
|
|
798
|
-
limit,
|
|
799
|
-
hasMore: skip + paged.length < total
|
|
1504
|
+
const cursor = collection.aggregate(processedPipeline, {
|
|
1505
|
+
signal,
|
|
1506
|
+
timeoutMS: limits.timeoutMS
|
|
1507
|
+
});
|
|
1508
|
+
const { results, truncated } = limits.documents !== void 0 || limits.bytes !== void 0 ? await readResultPage(cursor, limits, Infinity, signal) : { results: await cursor.toArray(), truncated: void 0 };
|
|
1509
|
+
return formatResponse({
|
|
1510
|
+
results,
|
|
1511
|
+
metadata: {
|
|
1512
|
+
returned: results.length,
|
|
1513
|
+
...truncated ? { truncated } : {}
|
|
800
1514
|
}
|
|
801
1515
|
});
|
|
802
1516
|
} catch (error) {
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
// src/utils/pagination.ts
|
|
808
|
-
var DEFAULT_PAGE_SIZE = 20;
|
|
809
|
-
function encodeCursor(offset) {
|
|
810
|
-
const data = { offset };
|
|
811
|
-
return Buffer.from(JSON.stringify(data)).toString("base64");
|
|
812
|
-
}
|
|
813
|
-
function decodeCursor(cursor) {
|
|
814
|
-
if (!cursor) return 0;
|
|
815
|
-
try {
|
|
816
|
-
const data = JSON.parse(
|
|
817
|
-
Buffer.from(cursor, "base64").toString("utf-8")
|
|
818
|
-
);
|
|
819
|
-
return typeof data.offset === "number" && data.offset >= 0 ? data.offset : 0;
|
|
820
|
-
} catch {
|
|
821
|
-
return 0;
|
|
1517
|
+
signal?.throwIfAborted();
|
|
1518
|
+
return handleError(error, "aggregate", collection.collectionName);
|
|
822
1519
|
}
|
|
823
1520
|
}
|
|
824
|
-
function
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
const nextOffset = offset + pageSize;
|
|
828
|
-
const nextCursor = nextOffset < items.length ? encodeCursor(nextOffset) : void 0;
|
|
829
|
-
return { items: page, nextCursor };
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
// src/schemas/completion.ts
|
|
833
|
-
async function handleCompletionRequest({
|
|
834
|
-
request,
|
|
835
|
-
client,
|
|
836
|
-
db,
|
|
837
|
-
isReadOnlyMode,
|
|
838
|
-
signal
|
|
839
|
-
}) {
|
|
840
|
-
const { ref, argument } = request.params;
|
|
841
|
-
if (ref.type === "ref/prompt") {
|
|
842
|
-
return handlePromptCompletion(
|
|
843
|
-
client,
|
|
844
|
-
db,
|
|
845
|
-
isReadOnlyMode,
|
|
846
|
-
ref.name,
|
|
847
|
-
argument,
|
|
848
|
-
signal
|
|
849
|
-
);
|
|
850
|
-
}
|
|
851
|
-
if (ref.type === "ref/resource") {
|
|
852
|
-
return handleResourceCompletion(
|
|
853
|
-
client,
|
|
854
|
-
db,
|
|
855
|
-
isReadOnlyMode,
|
|
856
|
-
ref.uri,
|
|
857
|
-
argument,
|
|
858
|
-
signal
|
|
859
|
-
);
|
|
1521
|
+
async function handleUpdate(collection, args, objectIdMode = "auto", allowServerJs = false, signal) {
|
|
1522
|
+
if (!collection) {
|
|
1523
|
+
throw new Error("Collection is required for update operation");
|
|
860
1524
|
}
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
return emptyCompletionResult();
|
|
1525
|
+
const { filter, update, upsert, multi } = args;
|
|
1526
|
+
const queryFilter = parseFilter(filter, objectIdMode);
|
|
1527
|
+
if (!allowServerJs) {
|
|
1528
|
+
assertNoServerJs(queryFilter);
|
|
866
1529
|
}
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
signal
|
|
1530
|
+
let processedUpdate = update;
|
|
1531
|
+
if (update && typeof update === "object" && !Array.isArray(update)) {
|
|
1532
|
+
processedUpdate = processObjectIdInFilter(
|
|
1533
|
+
update,
|
|
1534
|
+
objectIdMode
|
|
873
1535
|
);
|
|
874
1536
|
}
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
async function handleResourceCompletion(client, db, isReadOnlyMode, promptName, argument, signal) {
|
|
878
|
-
if (!promptName) {
|
|
879
|
-
return emptyCompletionResult();
|
|
1537
|
+
if (!processedUpdate || typeof processedUpdate !== "object" || Array.isArray(processedUpdate)) {
|
|
1538
|
+
throw new Error("Update must be a valid MongoDB update document");
|
|
880
1539
|
}
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
1540
|
+
const validUpdateOperators = [
|
|
1541
|
+
"$set",
|
|
1542
|
+
"$unset",
|
|
1543
|
+
"$inc",
|
|
1544
|
+
"$push",
|
|
1545
|
+
"$pull",
|
|
1546
|
+
"$addToSet",
|
|
1547
|
+
"$pop",
|
|
1548
|
+
"$rename",
|
|
1549
|
+
"$mul"
|
|
1550
|
+
];
|
|
1551
|
+
const hasValidOperator = Object.keys(processedUpdate).some(
|
|
1552
|
+
(key) => validUpdateOperators.includes(key)
|
|
1553
|
+
);
|
|
1554
|
+
if (!hasValidOperator) {
|
|
1555
|
+
throw new Error(
|
|
1556
|
+
"Update must include at least one valid update operator ($set, $unset, etc.)"
|
|
887
1557
|
);
|
|
888
1558
|
}
|
|
889
|
-
return emptyCompletionResult();
|
|
890
|
-
}
|
|
891
|
-
async function completeCollectionNames(partialValue, db, isReadOnlyMode, signal) {
|
|
892
1559
|
try {
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
1560
|
+
const options = {
|
|
1561
|
+
upsert: !!upsert,
|
|
1562
|
+
multi: !!multi
|
|
1563
|
+
};
|
|
1564
|
+
const updateMethod = options.multi ? "updateMany" : "updateOne";
|
|
896
1565
|
signal?.throwIfAborted();
|
|
897
|
-
const
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
(name) => (
|
|
902
|
-
// Filter out system collections
|
|
903
|
-
!name.startsWith("system.") && // Match partial value
|
|
904
|
-
name.toLowerCase().includes(partialValue.toLowerCase())
|
|
905
|
-
)
|
|
906
|
-
).sort();
|
|
907
|
-
console.warn(`Found ${matchingCollections.length} matching collections`);
|
|
908
|
-
const PAGE_SIZE = 100;
|
|
909
|
-
const { items: limitedResults, nextCursor } = paginate(
|
|
910
|
-
matchingCollections,
|
|
911
|
-
void 0,
|
|
912
|
-
PAGE_SIZE
|
|
1566
|
+
const result = await collection[updateMethod](
|
|
1567
|
+
queryFilter,
|
|
1568
|
+
processedUpdate,
|
|
1569
|
+
options
|
|
913
1570
|
);
|
|
914
|
-
return {
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
};
|
|
1571
|
+
return formatResponse({
|
|
1572
|
+
matchedCount: result.matchedCount,
|
|
1573
|
+
modifiedCount: result.modifiedCount,
|
|
1574
|
+
upsertedCount: result.upsertedCount,
|
|
1575
|
+
upsertedId: result.upsertedId
|
|
1576
|
+
});
|
|
921
1577
|
} catch (error) {
|
|
922
|
-
|
|
923
|
-
return emptyCompletionResult();
|
|
1578
|
+
return handleError(error, "update", collection.collectionName);
|
|
924
1579
|
}
|
|
925
1580
|
}
|
|
926
|
-
function
|
|
927
|
-
|
|
928
|
-
completion: {
|
|
929
|
-
values: [],
|
|
930
|
-
total: 0,
|
|
931
|
-
hasMore: false
|
|
932
|
-
}
|
|
933
|
-
};
|
|
934
|
-
}
|
|
935
|
-
|
|
936
|
-
// src/schemas/ping.ts
|
|
937
|
-
async function handlePingRequest({
|
|
938
|
-
request,
|
|
939
|
-
client,
|
|
940
|
-
db,
|
|
941
|
-
isReadOnlyMode,
|
|
942
|
-
signal
|
|
943
|
-
}) {
|
|
1581
|
+
async function handleServerInfo(db, isReadOnlyMode, args, signal) {
|
|
1582
|
+
const { includeDebugInfo } = args;
|
|
944
1583
|
try {
|
|
945
|
-
if (!client) {
|
|
946
|
-
throw new Error("MongoDB connection is not available");
|
|
947
|
-
}
|
|
948
1584
|
signal?.throwIfAborted();
|
|
949
|
-
const
|
|
950
|
-
|
|
951
|
-
|
|
1585
|
+
const buildInfo = await db.command({ buildInfo: 1 });
|
|
1586
|
+
const serverInfo = {
|
|
1587
|
+
version: buildInfo.version,
|
|
1588
|
+
gitVersion: buildInfo.gitVersion,
|
|
1589
|
+
storageEngines: buildInfo.storageEngines,
|
|
1590
|
+
maxBsonObjectSize: buildInfo.maxBsonObjectSize,
|
|
1591
|
+
bits: buildInfo.bits,
|
|
1592
|
+
ok: buildInfo.ok,
|
|
1593
|
+
connectionInfo: {
|
|
1594
|
+
readOnlyMode: isReadOnlyMode
|
|
1595
|
+
}
|
|
1596
|
+
};
|
|
1597
|
+
if (includeDebugInfo) {
|
|
1598
|
+
signal?.throwIfAborted();
|
|
1599
|
+
const serverStatus = await db.command({ serverStatus: 1 });
|
|
1600
|
+
serverInfo.modules = buildInfo.modules;
|
|
1601
|
+
serverInfo.allocator = buildInfo.allocator;
|
|
1602
|
+
serverInfo.javascriptEngine = buildInfo.javascriptEngine;
|
|
1603
|
+
serverInfo.sysInfo = buildInfo.sysInfo;
|
|
1604
|
+
serverInfo.debug = buildInfo.debug;
|
|
1605
|
+
serverInfo.openssl = buildInfo.openssl;
|
|
1606
|
+
serverInfo.buildEnvironment = buildInfo.buildEnvironment;
|
|
1607
|
+
serverInfo.status = {
|
|
1608
|
+
host: serverStatus.host,
|
|
1609
|
+
version: serverStatus.version,
|
|
1610
|
+
process: serverStatus.process,
|
|
1611
|
+
pid: serverStatus.pid,
|
|
1612
|
+
uptime: serverStatus.uptime,
|
|
1613
|
+
uptimeMillis: serverStatus.uptimeMillis,
|
|
1614
|
+
uptimeEstimate: serverStatus.uptimeEstimate,
|
|
1615
|
+
localTime: serverStatus.localTime,
|
|
1616
|
+
connections: serverStatus.connections,
|
|
1617
|
+
network: serverStatus.network,
|
|
1618
|
+
memory: serverStatus.mem,
|
|
1619
|
+
storageEngine: serverStatus.storageEngine,
|
|
1620
|
+
security: serverStatus.security
|
|
1621
|
+
};
|
|
952
1622
|
}
|
|
953
|
-
return
|
|
1623
|
+
return formatResponse(serverInfo);
|
|
954
1624
|
} catch (error) {
|
|
955
|
-
|
|
956
|
-
throw new Error(`MongoDB ping failed: ${error.message}`);
|
|
957
|
-
}
|
|
958
|
-
throw new Error("MongoDB ping failed: Unknown error");
|
|
1625
|
+
return handleError(error, "get server information");
|
|
959
1626
|
}
|
|
960
1627
|
}
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
request,
|
|
965
|
-
client,
|
|
966
|
-
db,
|
|
967
|
-
isReadOnlyMode,
|
|
968
|
-
signal
|
|
969
|
-
}) {
|
|
970
|
-
return {
|
|
971
|
-
prompts: [
|
|
972
|
-
{
|
|
973
|
-
name: "analyze_collection",
|
|
974
|
-
description: "Analyze a MongoDB collection structure and contents",
|
|
975
|
-
arguments: [
|
|
976
|
-
{
|
|
977
|
-
name: "collection",
|
|
978
|
-
description: "Name of the collection to analyze",
|
|
979
|
-
required: true
|
|
980
|
-
}
|
|
981
|
-
]
|
|
982
|
-
}
|
|
983
|
-
]
|
|
984
|
-
};
|
|
985
|
-
}
|
|
986
|
-
async function handleGetPromptRequest({
|
|
987
|
-
request,
|
|
988
|
-
client,
|
|
989
|
-
db,
|
|
990
|
-
isReadOnlyMode,
|
|
991
|
-
signal,
|
|
992
|
-
sendProgress
|
|
993
|
-
}) {
|
|
994
|
-
const { name, arguments: args = {} } = request.params;
|
|
995
|
-
if (name !== "analyze_collection") {
|
|
996
|
-
throw new Error("Unknown prompt");
|
|
1628
|
+
async function handleInsert(collection, args, objectIdMode = "auto", signal) {
|
|
1629
|
+
if (!collection) {
|
|
1630
|
+
throw new Error("Collection is required for insert operation");
|
|
997
1631
|
}
|
|
998
|
-
const
|
|
999
|
-
if (!
|
|
1000
|
-
throw new Error("
|
|
1632
|
+
const { documents, ordered, writeConcern, bypassDocumentValidation } = args;
|
|
1633
|
+
if (!Array.isArray(documents)) {
|
|
1634
|
+
throw new Error("Documents must be an array");
|
|
1635
|
+
}
|
|
1636
|
+
if (documents.length === 0) {
|
|
1637
|
+
throw new Error("Documents array cannot be empty");
|
|
1638
|
+
}
|
|
1639
|
+
if (!documents.every(
|
|
1640
|
+
(doc) => doc && typeof doc === "object" && !Array.isArray(doc)
|
|
1641
|
+
)) {
|
|
1642
|
+
throw new Error("Each document must be a valid MongoDB document object");
|
|
1001
1643
|
}
|
|
1644
|
+
const processedDocuments = documents.map(
|
|
1645
|
+
(doc) => processObjectIdInFilter(doc, objectIdMode)
|
|
1646
|
+
);
|
|
1002
1647
|
try {
|
|
1003
|
-
const
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
signal?.throwIfAborted();
|
|
1009
|
-
const schemaSample = await collection.findOne({});
|
|
1010
|
-
await sendProgress?.(2, 3, "Getting stats");
|
|
1011
|
-
signal?.throwIfAborted();
|
|
1012
|
-
const stats = await collection.aggregate([{ $collStats: { count: {} } }]).toArray();
|
|
1013
|
-
await sendProgress?.(3, 3, "Fetching samples");
|
|
1014
|
-
signal?.throwIfAborted();
|
|
1015
|
-
const sampleDocs = await collection.find({}).limit(5).toArray();
|
|
1016
|
-
const documentCount = stats[0]?.count ?? "unknown";
|
|
1017
|
-
return {
|
|
1018
|
-
messages: [
|
|
1019
|
-
{
|
|
1020
|
-
role: "user",
|
|
1021
|
-
content: {
|
|
1022
|
-
type: "text",
|
|
1023
|
-
text: `Please analyze the following MongoDB collection:
|
|
1024
|
-
Collection: ${collectionName}
|
|
1025
|
-
|
|
1026
|
-
Schema:
|
|
1027
|
-
${JSON.stringify(schemaSample, null, 2)}
|
|
1028
|
-
|
|
1029
|
-
Stats:
|
|
1030
|
-
Document count: ${documentCount}
|
|
1031
|
-
|
|
1032
|
-
Sample documents:
|
|
1033
|
-
${JSON.stringify(sampleDocs, null, 2)}`
|
|
1034
|
-
}
|
|
1035
|
-
},
|
|
1036
|
-
{
|
|
1037
|
-
role: "user",
|
|
1038
|
-
content: {
|
|
1039
|
-
type: "text",
|
|
1040
|
-
text: "Provide insights about the collection's structure, data types, and basic statistics."
|
|
1041
|
-
}
|
|
1042
|
-
}
|
|
1043
|
-
]
|
|
1648
|
+
const options = {
|
|
1649
|
+
ordered: ordered !== false,
|
|
1650
|
+
// default to true if not specified
|
|
1651
|
+
writeConcern,
|
|
1652
|
+
bypassDocumentValidation
|
|
1044
1653
|
};
|
|
1654
|
+
signal?.throwIfAborted();
|
|
1655
|
+
const result = await collection.insertMany(
|
|
1656
|
+
processedDocuments,
|
|
1657
|
+
options
|
|
1658
|
+
);
|
|
1659
|
+
return formatResponse({
|
|
1660
|
+
acknowledged: result.acknowledged,
|
|
1661
|
+
insertedCount: result.insertedCount,
|
|
1662
|
+
insertedIds: result.insertedIds
|
|
1663
|
+
});
|
|
1045
1664
|
} catch (error) {
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
ProtocolErrorCode as ProtocolErrorCode2
|
|
1055
|
-
} from "@modelcontextprotocol/server";
|
|
1056
|
-
import { ObjectId as ObjectId2 } from "mongodb";
|
|
1057
|
-
function detectMongoType(value) {
|
|
1058
|
-
if (value === null) return "null";
|
|
1059
|
-
if (value === void 0) return "undefined";
|
|
1060
|
-
if (value instanceof ObjectId2) return "ObjectId";
|
|
1061
|
-
if (value instanceof Date) return "Date";
|
|
1062
|
-
if (Array.isArray(value)) {
|
|
1063
|
-
if (value.length === 0) return "Array";
|
|
1064
|
-
const elementTypes = new Set(value.map((item) => detectMongoType(item)));
|
|
1065
|
-
if (elementTypes.size === 1) {
|
|
1066
|
-
return `Array<${Array.from(elementTypes)[0]}>`;
|
|
1665
|
+
if (error instanceof Error && error.name === "BulkWriteError") {
|
|
1666
|
+
const bulkError = error;
|
|
1667
|
+
return formatResponse({
|
|
1668
|
+
error: "Bulk write error occurred",
|
|
1669
|
+
writeErrors: bulkError.writeErrors,
|
|
1670
|
+
insertedCount: bulkError.result?.nInserted || 0,
|
|
1671
|
+
failedCount: bulkError.result?.nFailedInserts || 0
|
|
1672
|
+
});
|
|
1067
1673
|
}
|
|
1068
|
-
return "
|
|
1069
|
-
}
|
|
1070
|
-
if (typeof value === "object") {
|
|
1071
|
-
return "Document";
|
|
1674
|
+
return handleError(error, "insert", collection.collectionName);
|
|
1072
1675
|
}
|
|
1073
|
-
return typeof value;
|
|
1074
1676
|
}
|
|
1075
|
-
function
|
|
1076
|
-
if (!
|
|
1077
|
-
|
|
1677
|
+
async function handleCreateIndex(collection, args, objectIdMode = "auto", signal) {
|
|
1678
|
+
if (!collection) {
|
|
1679
|
+
throw new Error("Collection is required for createIndex operation");
|
|
1078
1680
|
}
|
|
1079
|
-
const
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
if (!fieldMap.has(key)) {
|
|
1083
|
-
fieldMap.set(key, {
|
|
1084
|
-
name: key,
|
|
1085
|
-
types: /* @__PURE__ */ new Set([detectMongoType(value)]),
|
|
1086
|
-
nullable: false,
|
|
1087
|
-
// Store sample values for complex types
|
|
1088
|
-
samples: [value]
|
|
1089
|
-
});
|
|
1090
|
-
} else {
|
|
1091
|
-
const fieldInfo = fieldMap.get(key);
|
|
1092
|
-
fieldInfo.types.add(detectMongoType(value));
|
|
1093
|
-
if (fieldInfo.samples.length < 3 && !fieldInfo.samples.some(
|
|
1094
|
-
(sample) => JSON.stringify(sample) === JSON.stringify(value)
|
|
1095
|
-
)) {
|
|
1096
|
-
fieldInfo.samples.push(value);
|
|
1097
|
-
}
|
|
1098
|
-
}
|
|
1099
|
-
}
|
|
1681
|
+
const { indexes, commitQuorum, writeConcern } = args;
|
|
1682
|
+
if (!Array.isArray(indexes) || indexes.length === 0) {
|
|
1683
|
+
throw new Error("Indexes must be a non-empty array");
|
|
1100
1684
|
}
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
fieldInfo.nullable = true;
|
|
1106
|
-
}
|
|
1107
|
-
}
|
|
1685
|
+
if (writeConcern && (typeof writeConcern !== "object" || Array.isArray(writeConcern))) {
|
|
1686
|
+
throw new Error(
|
|
1687
|
+
"Write concern must be a valid MongoDB write concern object"
|
|
1688
|
+
);
|
|
1108
1689
|
}
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
const nestedDocs = documents.filter(
|
|
1112
|
-
(doc) => doc[key] && typeof doc[key] === "object" && !Array.isArray(doc[key])
|
|
1113
|
-
).map((doc) => doc[key]);
|
|
1114
|
-
if (nestedDocs.length > 0) {
|
|
1115
|
-
fieldInfo.nestedSchema = inferSchemaFromSamples(nestedDocs);
|
|
1116
|
-
}
|
|
1117
|
-
}
|
|
1690
|
+
if (commitQuorum !== void 0 && typeof commitQuorum !== "string" && typeof commitQuorum !== "number") {
|
|
1691
|
+
throw new Error("Commit quorum must be a string or number");
|
|
1118
1692
|
}
|
|
1119
|
-
const
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
documents.filter((doc) => fieldInfo.name in doc).length / documents.length * 100
|
|
1126
|
-
)}%`,
|
|
1127
|
-
examples: []
|
|
1128
|
-
};
|
|
1129
|
-
if (fieldInfo.nestedSchema) {
|
|
1130
|
-
result.nestedSchema = fieldInfo.nestedSchema;
|
|
1693
|
+
const processedIndexes = indexes.map((index) => {
|
|
1694
|
+
if (index && typeof index === "object") {
|
|
1695
|
+
return processObjectIdInFilter(
|
|
1696
|
+
index,
|
|
1697
|
+
objectIdMode
|
|
1698
|
+
);
|
|
1131
1699
|
}
|
|
1132
|
-
|
|
1133
|
-
if (sample instanceof ObjectId2) return sample.toString();
|
|
1134
|
-
if (sample instanceof Date) return sample.toISOString();
|
|
1135
|
-
if (typeof sample === "object") {
|
|
1136
|
-
return Array.isArray(sample) ? "[...]" : "{...}";
|
|
1137
|
-
}
|
|
1138
|
-
return sample;
|
|
1139
|
-
});
|
|
1140
|
-
result.examples = sampleValues;
|
|
1141
|
-
return result;
|
|
1700
|
+
return index;
|
|
1142
1701
|
});
|
|
1143
|
-
|
|
1702
|
+
try {
|
|
1703
|
+
const indexOptions = {
|
|
1704
|
+
commitQuorum
|
|
1705
|
+
};
|
|
1706
|
+
signal?.throwIfAborted();
|
|
1707
|
+
const createdIndexes = await collection.createIndexes(
|
|
1708
|
+
processedIndexes,
|
|
1709
|
+
indexOptions
|
|
1710
|
+
);
|
|
1711
|
+
return formatResponse({
|
|
1712
|
+
acknowledged: true,
|
|
1713
|
+
createdIndexes,
|
|
1714
|
+
indexCount: createdIndexes.length
|
|
1715
|
+
});
|
|
1716
|
+
} catch (error) {
|
|
1717
|
+
return handleError(error, "create indexes", collection.collectionName);
|
|
1718
|
+
}
|
|
1144
1719
|
}
|
|
1145
|
-
async function
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1720
|
+
async function handleCount(collection, args, objectIdMode = "auto", allowServerJs = false, signal) {
|
|
1721
|
+
if (!collection) {
|
|
1722
|
+
throw new Error("Collection is required for count operation");
|
|
1723
|
+
}
|
|
1724
|
+
const { query, limit, skip, hint, readConcern, maxTimeMS, collation } = args;
|
|
1725
|
+
const countQuery = parseFilter(query, objectIdMode);
|
|
1726
|
+
if (!allowServerJs) {
|
|
1727
|
+
assertNoServerJs(countQuery);
|
|
1728
|
+
}
|
|
1154
1729
|
try {
|
|
1155
|
-
const
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1730
|
+
const options = {
|
|
1731
|
+
limit: typeof limit === "number" ? limit : void 0,
|
|
1732
|
+
skip: typeof skip === "number" ? skip : void 0,
|
|
1733
|
+
hint: typeof hint === "object" && hint !== null ? hint : void 0,
|
|
1734
|
+
readConcern: typeof readConcern === "object" && readConcern !== null ? readConcern : void 0,
|
|
1735
|
+
maxTimeMS: typeof maxTimeMS === "number" ? maxTimeMS : void 0,
|
|
1736
|
+
collation: typeof collation === "object" && collation !== null ? collation : void 0
|
|
1737
|
+
};
|
|
1738
|
+
for (const key of Object.keys(options)) {
|
|
1739
|
+
if (options[key] === void 0) {
|
|
1740
|
+
delete options[key];
|
|
1741
|
+
}
|
|
1161
1742
|
}
|
|
1162
|
-
|
|
1743
|
+
signal?.throwIfAborted();
|
|
1744
|
+
const count = await collection.countDocuments(countQuery, options);
|
|
1745
|
+
return formatResponse({
|
|
1746
|
+
count,
|
|
1747
|
+
ok: 1
|
|
1748
|
+
});
|
|
1163
1749
|
} catch (error) {
|
|
1164
|
-
|
|
1165
|
-
throw new ProtocolError2(
|
|
1166
|
-
ProtocolErrorCode2.InvalidParams,
|
|
1167
|
-
`Invalid resource URI: ${request.params.uri}`
|
|
1168
|
-
);
|
|
1750
|
+
return handleError(error, "count documents", collection.collectionName);
|
|
1169
1751
|
}
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1752
|
+
}
|
|
1753
|
+
function handleConvertTime(args) {
|
|
1754
|
+
const { input } = args;
|
|
1755
|
+
let date;
|
|
1756
|
+
if (input === void 0 || input === null || input === "") {
|
|
1757
|
+
date = /* @__PURE__ */ new Date();
|
|
1758
|
+
} else if (typeof input === "number") {
|
|
1759
|
+
date = new Date(Math.abs(input) < 1e12 ? input * 1e3 : input);
|
|
1760
|
+
} else if (typeof input === "string") {
|
|
1761
|
+
const trimmed = input.trim();
|
|
1762
|
+
if (/^-?\d+$/.test(trimmed)) {
|
|
1763
|
+
const n = Number(trimmed);
|
|
1764
|
+
date = new Date(Math.abs(n) < 1e12 ? n * 1e3 : n);
|
|
1765
|
+
} else {
|
|
1766
|
+
date = new Date(trimmed);
|
|
1767
|
+
}
|
|
1768
|
+
} else {
|
|
1769
|
+
throw new Error(
|
|
1770
|
+
"convertTime 'input' must be a Unix timestamp (number) or a date string"
|
|
1174
1771
|
);
|
|
1175
1772
|
}
|
|
1176
|
-
if (
|
|
1177
|
-
throw new
|
|
1178
|
-
|
|
1179
|
-
"Access to system collections is not allowed"
|
|
1773
|
+
if (Number.isNaN(date.getTime())) {
|
|
1774
|
+
throw new Error(
|
|
1775
|
+
`Could not interpret '${String(input)}' as a date or Unix timestamp`
|
|
1180
1776
|
);
|
|
1181
1777
|
}
|
|
1778
|
+
const offsetMinutes = -date.getTimezoneOffset();
|
|
1779
|
+
const sign = offsetMinutes >= 0 ? "+" : "-";
|
|
1780
|
+
const absMinutes = Math.abs(offsetMinutes);
|
|
1781
|
+
const offsetLabel = `${sign}${String(Math.floor(absMinutes / 60)).padStart(
|
|
1782
|
+
2,
|
|
1783
|
+
"0"
|
|
1784
|
+
)}:${String(absMinutes % 60).padStart(2, "0")}`;
|
|
1785
|
+
return formatResponse({
|
|
1786
|
+
iso: date.toISOString(),
|
|
1787
|
+
utc: date.toUTCString(),
|
|
1788
|
+
unixSeconds: Math.floor(date.getTime() / 1e3),
|
|
1789
|
+
unixMillis: date.getTime(),
|
|
1790
|
+
serverLocal: date.toString(),
|
|
1791
|
+
serverTimeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
1792
|
+
serverUtcOffset: offsetLabel
|
|
1793
|
+
});
|
|
1794
|
+
}
|
|
1795
|
+
async function handleListCollections(db, args, objectIdMode = "auto", signal) {
|
|
1796
|
+
const { nameOnly, filter } = args;
|
|
1797
|
+
const skip = args.skip || 0;
|
|
1798
|
+
const limit = args.limit || 20;
|
|
1182
1799
|
try {
|
|
1183
|
-
const
|
|
1184
|
-
const sampleSize = 100;
|
|
1185
|
-
let sampleDocuments = [];
|
|
1186
|
-
await sendProgress?.(1, 4, "Sampling documents");
|
|
1187
|
-
try {
|
|
1188
|
-
signal?.throwIfAborted();
|
|
1189
|
-
sampleDocuments = await collection.aggregate([{ $sample: { size: sampleSize } }]).toArray();
|
|
1190
|
-
} catch (sampleError) {
|
|
1191
|
-
console.warn(
|
|
1192
|
-
`$sample aggregation failed for ${collectionName}, falling back to sequential scan: ${sampleError}`
|
|
1193
|
-
);
|
|
1194
|
-
signal?.throwIfAborted();
|
|
1195
|
-
sampleDocuments = await collection.find({}).limit(sampleSize).toArray();
|
|
1196
|
-
}
|
|
1197
|
-
await sendProgress?.(2, 4, "Getting indexes");
|
|
1800
|
+
const processedFilter = parseFilter(filter, objectIdMode);
|
|
1198
1801
|
signal?.throwIfAborted();
|
|
1199
|
-
const
|
|
1200
|
-
const
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
)
|
|
1212
|
-
)
|
|
1213
|
-
]);
|
|
1214
|
-
} catch (countError) {
|
|
1215
|
-
console.warn(
|
|
1216
|
-
`Count operation failed or timed out for ${collectionName}: ${countError}`
|
|
1217
|
-
);
|
|
1218
|
-
try {
|
|
1219
|
-
signal?.throwIfAborted();
|
|
1220
|
-
const stats = await db.command({ collStats: collectionName });
|
|
1221
|
-
documentCount = stats.count;
|
|
1222
|
-
} catch {
|
|
1223
|
-
documentCount = "unknown (count operation timed out)";
|
|
1802
|
+
const collections = await db.listCollections(processedFilter).toArray();
|
|
1803
|
+
const allResults = nameOnly ? collections.map((collection) => collection.name) : collections;
|
|
1804
|
+
const total = allResults.length;
|
|
1805
|
+
const paged = allResults.slice(skip, skip + limit);
|
|
1806
|
+
return formatResponse({
|
|
1807
|
+
results: paged,
|
|
1808
|
+
metadata: {
|
|
1809
|
+
total,
|
|
1810
|
+
returned: paged.length,
|
|
1811
|
+
skip,
|
|
1812
|
+
limit,
|
|
1813
|
+
hasMore: skip + paged.length < total
|
|
1224
1814
|
}
|
|
1225
|
-
}
|
|
1226
|
-
await sendProgress?.(4, 4, "Done");
|
|
1227
|
-
const schema = {
|
|
1228
|
-
type: "collection",
|
|
1229
|
-
name: collectionName,
|
|
1230
|
-
fields: inferredSchema.fields,
|
|
1231
|
-
indexes: indexes.map((idx) => ({
|
|
1232
|
-
name: idx.name,
|
|
1233
|
-
keys: idx.key
|
|
1234
|
-
})),
|
|
1235
|
-
documentCount,
|
|
1236
|
-
sampleSize: sampleDocuments.length,
|
|
1237
|
-
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
1238
|
-
};
|
|
1239
|
-
return {
|
|
1240
|
-
contents: [
|
|
1241
|
-
{
|
|
1242
|
-
uri: request.params.uri,
|
|
1243
|
-
mimeType: "application/json",
|
|
1244
|
-
text: JSON.stringify(schema, null, 2)
|
|
1245
|
-
}
|
|
1246
|
-
]
|
|
1247
|
-
};
|
|
1815
|
+
});
|
|
1248
1816
|
} catch (error) {
|
|
1249
|
-
|
|
1250
|
-
throw new Error(
|
|
1251
|
-
`Failed to read collection ${collectionName}: ${error.message}`
|
|
1252
|
-
);
|
|
1253
|
-
}
|
|
1254
|
-
throw new Error(
|
|
1255
|
-
`Failed to read collection ${collectionName}: Unknown error`
|
|
1256
|
-
);
|
|
1817
|
+
return handleError(error, "list collections");
|
|
1257
1818
|
}
|
|
1258
1819
|
}
|
|
1259
|
-
|
|
1820
|
+
|
|
1821
|
+
// src/schemas/completion.ts
|
|
1822
|
+
async function handleCompletionRequest({
|
|
1260
1823
|
request,
|
|
1261
1824
|
client,
|
|
1262
1825
|
db,
|
|
1263
1826
|
isReadOnlyMode,
|
|
1264
1827
|
signal
|
|
1265
1828
|
}) {
|
|
1829
|
+
const { ref, argument } = request.params;
|
|
1830
|
+
if (ref.type === "ref/prompt") {
|
|
1831
|
+
return handlePromptCompletion(
|
|
1832
|
+
client,
|
|
1833
|
+
db,
|
|
1834
|
+
isReadOnlyMode,
|
|
1835
|
+
ref.name,
|
|
1836
|
+
argument,
|
|
1837
|
+
signal
|
|
1838
|
+
);
|
|
1839
|
+
}
|
|
1840
|
+
if (ref.type === "ref/resource") {
|
|
1841
|
+
return handleResourceCompletion(
|
|
1842
|
+
client,
|
|
1843
|
+
db,
|
|
1844
|
+
isReadOnlyMode,
|
|
1845
|
+
ref.uri,
|
|
1846
|
+
argument,
|
|
1847
|
+
signal
|
|
1848
|
+
);
|
|
1849
|
+
}
|
|
1850
|
+
return emptyCompletionResult();
|
|
1851
|
+
}
|
|
1852
|
+
async function handlePromptCompletion(client, db, isReadOnlyMode, promptName, argument, signal) {
|
|
1853
|
+
if (!promptName) {
|
|
1854
|
+
return emptyCompletionResult();
|
|
1855
|
+
}
|
|
1856
|
+
if (argument.name === "collection") {
|
|
1857
|
+
return await completeCollectionNames(
|
|
1858
|
+
argument.value,
|
|
1859
|
+
db,
|
|
1860
|
+
isReadOnlyMode,
|
|
1861
|
+
signal
|
|
1862
|
+
);
|
|
1863
|
+
}
|
|
1864
|
+
return emptyCompletionResult();
|
|
1865
|
+
}
|
|
1866
|
+
async function handleResourceCompletion(client, db, isReadOnlyMode, promptName, argument, signal) {
|
|
1867
|
+
if (!promptName) {
|
|
1868
|
+
return emptyCompletionResult();
|
|
1869
|
+
}
|
|
1870
|
+
if (argument.name === "collection") {
|
|
1871
|
+
return await completeCollectionNames(
|
|
1872
|
+
argument.value,
|
|
1873
|
+
db,
|
|
1874
|
+
isReadOnlyMode,
|
|
1875
|
+
signal
|
|
1876
|
+
);
|
|
1877
|
+
}
|
|
1878
|
+
return emptyCompletionResult();
|
|
1879
|
+
}
|
|
1880
|
+
async function completeCollectionNames(partialValue, db, isReadOnlyMode, signal) {
|
|
1266
1881
|
try {
|
|
1882
|
+
console.warn(
|
|
1883
|
+
`Completing collection names with partial value: ${partialValue}`
|
|
1884
|
+
);
|
|
1267
1885
|
signal?.throwIfAborted();
|
|
1268
1886
|
const collections = await db.listCollections().toArray();
|
|
1269
|
-
const
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
name
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1887
|
+
const matchingCollections = collections.map(
|
|
1888
|
+
(c) => c.name
|
|
1889
|
+
).filter(
|
|
1890
|
+
(name) => (
|
|
1891
|
+
// Filter out system collections
|
|
1892
|
+
!name.startsWith("system.") && // Match partial value
|
|
1893
|
+
name.toLowerCase().includes(partialValue.toLowerCase())
|
|
1894
|
+
)
|
|
1895
|
+
).sort();
|
|
1896
|
+
console.warn(`Found ${matchingCollections.length} matching collections`);
|
|
1897
|
+
const PAGE_SIZE = 100;
|
|
1898
|
+
const { items: limitedResults, nextCursor } = paginate(
|
|
1899
|
+
matchingCollections,
|
|
1900
|
+
void 0,
|
|
1901
|
+
PAGE_SIZE
|
|
1902
|
+
);
|
|
1277
1903
|
return {
|
|
1278
|
-
|
|
1279
|
-
|
|
1904
|
+
completion: {
|
|
1905
|
+
values: limitedResults,
|
|
1906
|
+
total: matchingCollections.length,
|
|
1907
|
+
hasMore: !!nextCursor
|
|
1908
|
+
}
|
|
1280
1909
|
};
|
|
1281
1910
|
} catch (error) {
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
}
|
|
1285
|
-
throw new Error("Failed to list collections: Unknown error");
|
|
1911
|
+
console.error("Error completing collection names:", error);
|
|
1912
|
+
return emptyCompletionResult();
|
|
1286
1913
|
}
|
|
1287
1914
|
}
|
|
1915
|
+
function emptyCompletionResult() {
|
|
1916
|
+
return {
|
|
1917
|
+
completion: {
|
|
1918
|
+
values: [],
|
|
1919
|
+
total: 0,
|
|
1920
|
+
hasMore: false
|
|
1921
|
+
}
|
|
1922
|
+
};
|
|
1923
|
+
}
|
|
1288
1924
|
|
|
1289
|
-
// src/schemas/
|
|
1290
|
-
async function
|
|
1925
|
+
// src/schemas/ping.ts
|
|
1926
|
+
async function handlePingRequest({
|
|
1291
1927
|
request,
|
|
1292
1928
|
client,
|
|
1293
1929
|
db,
|
|
1294
1930
|
isReadOnlyMode,
|
|
1295
|
-
signal
|
|
1296
|
-
}) {
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
Example queries:
|
|
1315
|
-
1. Find documents where age > 21:
|
|
1316
|
-
{ "age": { "$gt": 21 } }
|
|
1317
|
-
|
|
1318
|
-
2. Find documents with specific status:
|
|
1319
|
-
{ "status": { "$in": ["active", "pending"] } }
|
|
1320
|
-
|
|
1321
|
-
3. Find documents with existing email:
|
|
1322
|
-
{ "email": { "$exists": true } }
|
|
1323
|
-
|
|
1324
|
-
4. Find documents with dates:
|
|
1325
|
-
// Using ISO date string format
|
|
1326
|
-
{ "createdAt": { "$gt": "2023-01-01T00:00:00Z" } }
|
|
1327
|
-
// Using ISODate syntax
|
|
1328
|
-
{ "createdAt": { "$gt": ISODate("2023-01-01T00:00:00Z") } }
|
|
1329
|
-
|
|
1330
|
-
Use these patterns to construct MongoDB queries.`
|
|
1331
|
-
}
|
|
1332
|
-
]
|
|
1333
|
-
};
|
|
1931
|
+
signal
|
|
1932
|
+
}) {
|
|
1933
|
+
try {
|
|
1934
|
+
if (!client) {
|
|
1935
|
+
throw new Error("MongoDB connection is not available");
|
|
1936
|
+
}
|
|
1937
|
+
signal?.throwIfAborted();
|
|
1938
|
+
const pong = await db.command({ ping: 1 });
|
|
1939
|
+
if (pong.ok !== 1) {
|
|
1940
|
+
throw new Error(`MongoDB ping failed: ${pong.errmsg}`);
|
|
1941
|
+
}
|
|
1942
|
+
return {};
|
|
1943
|
+
} catch (error) {
|
|
1944
|
+
if (error instanceof Error) {
|
|
1945
|
+
throw new Error(`MongoDB ping failed: ${error.message}`);
|
|
1946
|
+
}
|
|
1947
|
+
throw new Error("MongoDB ping failed: Unknown error");
|
|
1948
|
+
}
|
|
1334
1949
|
}
|
|
1335
1950
|
|
|
1336
|
-
// src/schemas/
|
|
1337
|
-
async function
|
|
1951
|
+
// src/schemas/prompts.ts
|
|
1952
|
+
async function handleListPromptsRequest({
|
|
1338
1953
|
request,
|
|
1339
1954
|
client,
|
|
1340
1955
|
db,
|
|
1341
1956
|
isReadOnlyMode,
|
|
1342
|
-
signal
|
|
1957
|
+
signal,
|
|
1958
|
+
connectionSelection = false
|
|
1343
1959
|
}) {
|
|
1344
|
-
return {
|
|
1345
|
-
|
|
1346
|
-
{
|
|
1347
|
-
name: "query",
|
|
1348
|
-
description: "Find documents in one collection using a filter, projection, or sort. Use aggregate for grouping or joins. Request an execution plan to inspect the query.",
|
|
1349
|
-
inputSchema: {
|
|
1350
|
-
type: "object",
|
|
1351
|
-
properties: {
|
|
1352
|
-
collection: {
|
|
1353
|
-
type: "string",
|
|
1354
|
-
description: "Name of the collection to query"
|
|
1355
|
-
},
|
|
1356
|
-
filter: {
|
|
1357
|
-
type: "object",
|
|
1358
|
-
description: "MongoDB query filter. Supports date strings in ISO format ('2025-01-01T00:00:00Z') and ISODate('2025-01-01T00:00:00Z') notation"
|
|
1359
|
-
},
|
|
1360
|
-
projection: {
|
|
1361
|
-
type: "object",
|
|
1362
|
-
description: "Fields to include/exclude"
|
|
1363
|
-
},
|
|
1364
|
-
limit: {
|
|
1365
|
-
type: "number",
|
|
1366
|
-
description: "Maximum number of documents to return",
|
|
1367
|
-
default: 10
|
|
1368
|
-
},
|
|
1369
|
-
skip: {
|
|
1370
|
-
type: "number",
|
|
1371
|
-
description: "Number of documents to skip before returning results",
|
|
1372
|
-
default: 0
|
|
1373
|
-
},
|
|
1374
|
-
sort: {
|
|
1375
|
-
type: "object",
|
|
1376
|
-
description: "Sort order as a field-to-direction map, e.g. { orderDate: -1 } for descending or { name: 1 } for ascending"
|
|
1377
|
-
},
|
|
1378
|
-
explain: {
|
|
1379
|
-
type: "string",
|
|
1380
|
-
description: "Optional: Get query execution information",
|
|
1381
|
-
enum: ["queryPlanner", "executionStats", "allPlansExecution"]
|
|
1382
|
-
},
|
|
1383
|
-
objectIdMode: {
|
|
1384
|
-
type: "string",
|
|
1385
|
-
description: "Control how 24-character hex strings are handled",
|
|
1386
|
-
enum: ["auto", "none", "force"],
|
|
1387
|
-
default: "auto"
|
|
1388
|
-
}
|
|
1389
|
-
},
|
|
1390
|
-
required: ["collection"]
|
|
1391
|
-
}
|
|
1392
|
-
},
|
|
1393
|
-
{
|
|
1394
|
-
name: "aggregate",
|
|
1395
|
-
description: "Run a MongoDB aggregation pipeline for grouping, joins, or multi-stage transformations. Request an execution plan to inspect the pipeline.",
|
|
1396
|
-
inputSchema: {
|
|
1397
|
-
type: "object",
|
|
1398
|
-
properties: {
|
|
1399
|
-
collection: {
|
|
1400
|
-
type: "string",
|
|
1401
|
-
description: "Name of the collection to aggregate"
|
|
1402
|
-
},
|
|
1403
|
-
pipeline: {
|
|
1404
|
-
type: "array",
|
|
1405
|
-
description: "Aggregation pipeline stages. Supports date strings in ISO format ('2025-01-01T00:00:00Z') and ISODate('2025-01-01T00:00:00Z') notation",
|
|
1406
|
-
items: {
|
|
1407
|
-
type: "object"
|
|
1408
|
-
}
|
|
1409
|
-
},
|
|
1410
|
-
explain: {
|
|
1411
|
-
type: "string",
|
|
1412
|
-
description: "Optional: Get aggregation execution information (queryPlanner, executionStats, or allPlansExecution)",
|
|
1413
|
-
enum: ["queryPlanner", "executionStats", "allPlansExecution"]
|
|
1414
|
-
},
|
|
1415
|
-
objectIdMode: {
|
|
1416
|
-
type: "string",
|
|
1417
|
-
description: "Control how 24-character hex strings are handled",
|
|
1418
|
-
enum: ["auto", "none", "force"],
|
|
1419
|
-
default: "auto"
|
|
1420
|
-
}
|
|
1421
|
-
},
|
|
1422
|
-
required: ["collection", "pipeline"]
|
|
1423
|
-
}
|
|
1424
|
-
},
|
|
1425
|
-
{
|
|
1426
|
-
name: "update",
|
|
1427
|
-
description: "Modify matching documents using operators such as $set, $unset, or $inc. Enable multi to update all matches or upsert to create a document when the filter matches none.",
|
|
1428
|
-
inputSchema: {
|
|
1429
|
-
type: "object",
|
|
1430
|
-
properties: {
|
|
1431
|
-
collection: {
|
|
1432
|
-
type: "string",
|
|
1433
|
-
description: "Name of the collection to update"
|
|
1434
|
-
},
|
|
1435
|
-
filter: {
|
|
1436
|
-
type: "object",
|
|
1437
|
-
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"
|
|
1438
|
-
},
|
|
1439
|
-
update: {
|
|
1440
|
-
type: "object",
|
|
1441
|
-
description: "Update operations to apply ($set, $unset, $inc, etc.)"
|
|
1442
|
-
},
|
|
1443
|
-
upsert: {
|
|
1444
|
-
type: "boolean",
|
|
1445
|
-
description: "Create a new document if no documents match the filter"
|
|
1446
|
-
},
|
|
1447
|
-
multi: {
|
|
1448
|
-
type: "boolean",
|
|
1449
|
-
description: "Update multiple documents that match the filter"
|
|
1450
|
-
},
|
|
1451
|
-
objectIdMode: {
|
|
1452
|
-
type: "string",
|
|
1453
|
-
description: "Control how 24-character hex strings are handled",
|
|
1454
|
-
enum: ["auto", "none", "force"],
|
|
1455
|
-
default: "auto"
|
|
1456
|
-
}
|
|
1457
|
-
},
|
|
1458
|
-
required: ["collection", "filter", "update"]
|
|
1459
|
-
}
|
|
1460
|
-
},
|
|
1461
|
-
{
|
|
1462
|
-
name: "serverInfo",
|
|
1463
|
-
description: "Inspect MongoDB server metadata such as version and storage engine. Enable includeDebugInfo for additional diagnostics.",
|
|
1464
|
-
inputSchema: {
|
|
1465
|
-
type: "object",
|
|
1466
|
-
properties: {
|
|
1467
|
-
includeDebugInfo: {
|
|
1468
|
-
type: "boolean",
|
|
1469
|
-
description: "Include additional debug information about the server"
|
|
1470
|
-
}
|
|
1471
|
-
}
|
|
1472
|
-
}
|
|
1473
|
-
},
|
|
1474
|
-
{
|
|
1475
|
-
name: "insert",
|
|
1476
|
-
description: "Insert new documents into a MongoDB collection. Use update to modify existing records.",
|
|
1477
|
-
inputSchema: {
|
|
1478
|
-
type: "object",
|
|
1479
|
-
properties: {
|
|
1480
|
-
collection: {
|
|
1481
|
-
type: "string",
|
|
1482
|
-
description: "Name of the collection to insert into"
|
|
1483
|
-
},
|
|
1484
|
-
documents: {
|
|
1485
|
-
type: "array",
|
|
1486
|
-
description: "Array of documents to insert",
|
|
1487
|
-
items: { type: "object" }
|
|
1488
|
-
},
|
|
1489
|
-
ordered: {
|
|
1490
|
-
type: "boolean",
|
|
1491
|
-
description: "If true, perform ordered insert. If false, insert unordered"
|
|
1492
|
-
},
|
|
1493
|
-
writeConcern: {
|
|
1494
|
-
type: "object",
|
|
1495
|
-
description: "Write concern for the insert operation"
|
|
1496
|
-
},
|
|
1497
|
-
bypassDocumentValidation: {
|
|
1498
|
-
type: "boolean",
|
|
1499
|
-
description: "Allow insert to bypass schema validation"
|
|
1500
|
-
},
|
|
1501
|
-
objectIdMode: {
|
|
1502
|
-
type: "string",
|
|
1503
|
-
description: "Control how 24-character hex strings are handled",
|
|
1504
|
-
enum: ["auto", "none", "force"],
|
|
1505
|
-
default: "auto"
|
|
1506
|
-
}
|
|
1507
|
-
},
|
|
1508
|
-
required: ["collection", "documents"]
|
|
1509
|
-
}
|
|
1510
|
-
},
|
|
1511
|
-
{
|
|
1512
|
-
name: "createIndex",
|
|
1513
|
-
description: "Create indexes to improve query performance, enforce uniqueness, or expire documents with a TTL index.",
|
|
1514
|
-
inputSchema: {
|
|
1515
|
-
type: "object",
|
|
1516
|
-
properties: {
|
|
1517
|
-
collection: {
|
|
1518
|
-
type: "string",
|
|
1519
|
-
description: "Name of the collection to create indexes on"
|
|
1520
|
-
},
|
|
1521
|
-
indexes: {
|
|
1522
|
-
type: "array",
|
|
1523
|
-
description: "Array of index specifications",
|
|
1524
|
-
items: {
|
|
1525
|
-
type: "object",
|
|
1526
|
-
properties: {
|
|
1527
|
-
key: {
|
|
1528
|
-
type: "object",
|
|
1529
|
-
description: "Index key pattern, e.g. { field: 1 }"
|
|
1530
|
-
},
|
|
1531
|
-
name: {
|
|
1532
|
-
type: "string",
|
|
1533
|
-
description: "Optional: Name of the index"
|
|
1534
|
-
},
|
|
1535
|
-
unique: {
|
|
1536
|
-
type: "boolean",
|
|
1537
|
-
description: "Optional: Creates a unique index"
|
|
1538
|
-
},
|
|
1539
|
-
sparse: {
|
|
1540
|
-
type: "boolean",
|
|
1541
|
-
description: "Optional: Creates a sparse index"
|
|
1542
|
-
},
|
|
1543
|
-
background: {
|
|
1544
|
-
type: "boolean",
|
|
1545
|
-
description: "Optional: Builds index in background"
|
|
1546
|
-
},
|
|
1547
|
-
expireAfterSeconds: {
|
|
1548
|
-
type: "number",
|
|
1549
|
-
description: "TTL in seconds for documents"
|
|
1550
|
-
},
|
|
1551
|
-
partialFilterExpression: {
|
|
1552
|
-
type: "object",
|
|
1553
|
-
description: "Filter expression for partial indexes"
|
|
1554
|
-
}
|
|
1555
|
-
},
|
|
1556
|
-
required: ["key"]
|
|
1557
|
-
}
|
|
1558
|
-
},
|
|
1559
|
-
writeConcern: {
|
|
1560
|
-
type: "object",
|
|
1561
|
-
description: "Write concern for index creation"
|
|
1562
|
-
},
|
|
1563
|
-
commitQuorum: {
|
|
1564
|
-
type: ["string", "number"],
|
|
1565
|
-
description: "Replica-set commit quorum as a member count or a mode such as majority"
|
|
1566
|
-
},
|
|
1567
|
-
objectIdMode: {
|
|
1568
|
-
type: "string",
|
|
1569
|
-
description: "Control how 24-character hex strings are handled",
|
|
1570
|
-
enum: ["auto", "none", "force"],
|
|
1571
|
-
default: "auto"
|
|
1572
|
-
}
|
|
1573
|
-
},
|
|
1574
|
-
required: ["collection", "indexes"]
|
|
1575
|
-
}
|
|
1576
|
-
},
|
|
1960
|
+
return {
|
|
1961
|
+
prompts: [
|
|
1577
1962
|
{
|
|
1578
|
-
name: "
|
|
1579
|
-
description: "
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
limit: { type: "integer", description: "Max documents to count" },
|
|
1586
|
-
skip: {
|
|
1587
|
-
type: "integer",
|
|
1588
|
-
description: "Docs to skip before counting"
|
|
1589
|
-
},
|
|
1590
|
-
hint: { type: "object", description: "Index hint" },
|
|
1591
|
-
readConcern: { type: "object", description: "Read concern option" },
|
|
1592
|
-
maxTimeMS: { type: "integer", description: "Max execution time" },
|
|
1593
|
-
collation: {
|
|
1594
|
-
type: "object",
|
|
1595
|
-
description: "Collation rules for comparison"
|
|
1596
|
-
},
|
|
1597
|
-
objectIdMode: {
|
|
1598
|
-
type: "string",
|
|
1599
|
-
description: "Control how 24-character hex strings are handled",
|
|
1600
|
-
enum: ["auto", "none", "force"],
|
|
1601
|
-
default: "auto"
|
|
1602
|
-
}
|
|
1963
|
+
name: "analyze_collection",
|
|
1964
|
+
description: "Analyze a MongoDB collection structure and contents",
|
|
1965
|
+
arguments: [
|
|
1966
|
+
{
|
|
1967
|
+
name: "collection",
|
|
1968
|
+
description: "Name of the collection to analyze",
|
|
1969
|
+
required: true
|
|
1603
1970
|
},
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
name: "listCollections",
|
|
1609
|
-
description: "List collections in the connected database. Filter by name or other collection properties and request names only for a compact result.",
|
|
1610
|
-
inputSchema: {
|
|
1611
|
-
type: "object",
|
|
1612
|
-
properties: {
|
|
1613
|
-
nameOnly: {
|
|
1614
|
-
type: "boolean",
|
|
1615
|
-
description: "If true, return only collection names"
|
|
1616
|
-
},
|
|
1617
|
-
filter: {
|
|
1618
|
-
type: "object",
|
|
1619
|
-
description: "Filter for collections"
|
|
1620
|
-
},
|
|
1621
|
-
skip: {
|
|
1622
|
-
type: "number",
|
|
1623
|
-
description: "Number of collections to skip before returning results",
|
|
1624
|
-
default: 0
|
|
1625
|
-
},
|
|
1626
|
-
limit: {
|
|
1627
|
-
type: "number",
|
|
1628
|
-
description: "Maximum number of collections to return",
|
|
1629
|
-
default: 20
|
|
1971
|
+
...connectionSelection ? [
|
|
1972
|
+
{
|
|
1973
|
+
name: "connectionId",
|
|
1974
|
+
description: "Configured MongoDB connection ID"
|
|
1630
1975
|
},
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
description: "
|
|
1634
|
-
enum: ["auto", "none", "force"],
|
|
1635
|
-
default: "auto"
|
|
1976
|
+
{
|
|
1977
|
+
name: "database",
|
|
1978
|
+
description: "Allowed MongoDB database name"
|
|
1636
1979
|
}
|
|
1980
|
+
] : []
|
|
1981
|
+
]
|
|
1982
|
+
}
|
|
1983
|
+
]
|
|
1984
|
+
};
|
|
1985
|
+
}
|
|
1986
|
+
async function handleGetPromptRequest({
|
|
1987
|
+
request,
|
|
1988
|
+
client,
|
|
1989
|
+
db,
|
|
1990
|
+
isReadOnlyMode,
|
|
1991
|
+
signal,
|
|
1992
|
+
sendProgress,
|
|
1993
|
+
connectionId,
|
|
1994
|
+
showTarget = false
|
|
1995
|
+
}) {
|
|
1996
|
+
const { name, arguments: args = {} } = request.params;
|
|
1997
|
+
if (name !== "analyze_collection") {
|
|
1998
|
+
throw new Error("Unknown prompt");
|
|
1999
|
+
}
|
|
2000
|
+
const collectionName = args.collection;
|
|
2001
|
+
if (!collectionName) {
|
|
2002
|
+
throw new Error("Collection name is required");
|
|
2003
|
+
}
|
|
2004
|
+
try {
|
|
2005
|
+
const collection = db.collection(collectionName);
|
|
2006
|
+
if (collection.collectionName.startsWith("system.")) {
|
|
2007
|
+
throw new Error("Access to system collections is not allowed");
|
|
2008
|
+
}
|
|
2009
|
+
await sendProgress?.(1, 3, "Getting schema");
|
|
2010
|
+
signal?.throwIfAborted();
|
|
2011
|
+
const schemaSample = await collection.findOne({});
|
|
2012
|
+
await sendProgress?.(2, 3, "Getting stats");
|
|
2013
|
+
signal?.throwIfAborted();
|
|
2014
|
+
const stats = await collection.aggregate([{ $collStats: { count: {} } }]).toArray();
|
|
2015
|
+
await sendProgress?.(3, 3, "Fetching samples");
|
|
2016
|
+
signal?.throwIfAborted();
|
|
2017
|
+
const sampleDocs = await collection.find({}).limit(5).toArray();
|
|
2018
|
+
const documentCount = stats[0]?.count ?? "unknown";
|
|
2019
|
+
return {
|
|
2020
|
+
messages: [
|
|
2021
|
+
{
|
|
2022
|
+
role: "user",
|
|
2023
|
+
content: {
|
|
2024
|
+
type: "text",
|
|
2025
|
+
text: `Please analyze the following MongoDB collection:
|
|
2026
|
+
Collection: ${collectionName}${showTarget ? `
|
|
2027
|
+
Connection: ${connectionId}
|
|
2028
|
+
Database: ${db.databaseName}` : ""}
|
|
2029
|
+
|
|
2030
|
+
Schema:
|
|
2031
|
+
${JSON.stringify(schemaSample, null, 2)}
|
|
2032
|
+
|
|
2033
|
+
Stats:
|
|
2034
|
+
Document count: ${documentCount}
|
|
2035
|
+
|
|
2036
|
+
Sample documents:
|
|
2037
|
+
${JSON.stringify(sampleDocs, null, 2)}`
|
|
1637
2038
|
}
|
|
1638
|
-
}
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
type: "object",
|
|
1645
|
-
properties: {
|
|
1646
|
-
input: {
|
|
1647
|
-
type: "string",
|
|
1648
|
-
description: "A Unix timestamp (seconds or milliseconds, as a number or numeric string) or a date string (ISO 8601, with or without a timezone offset). Omit to use the current server time."
|
|
1649
|
-
}
|
|
2039
|
+
},
|
|
2040
|
+
{
|
|
2041
|
+
role: "user",
|
|
2042
|
+
content: {
|
|
2043
|
+
type: "text",
|
|
2044
|
+
text: "Provide insights about the collection's structure, data types, and basic statistics."
|
|
1650
2045
|
}
|
|
1651
2046
|
}
|
|
2047
|
+
]
|
|
2048
|
+
};
|
|
2049
|
+
} catch (error) {
|
|
2050
|
+
const msg = error instanceof Error ? error.message : "Unknown error";
|
|
2051
|
+
throw new Error(`Failed to analyze collection ${collectionName}: ${msg}`);
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
// src/schemas/templates.ts
|
|
2056
|
+
async function handleListResourceTemplatesRequest({
|
|
2057
|
+
request,
|
|
2058
|
+
client,
|
|
2059
|
+
db,
|
|
2060
|
+
isReadOnlyMode,
|
|
2061
|
+
signal
|
|
2062
|
+
}) {
|
|
2063
|
+
return {
|
|
2064
|
+
resourceTemplates: [
|
|
2065
|
+
{
|
|
2066
|
+
name: "mongodb_query",
|
|
2067
|
+
description: "Template for constructing MongoDB queries",
|
|
2068
|
+
uriTemplate: "mongodb:///{collection}",
|
|
2069
|
+
text: `To query MongoDB collections, you can use these operators:
|
|
2070
|
+
|
|
2071
|
+
Filter operators:
|
|
2072
|
+
- $eq: Matches values equal to a specified value
|
|
2073
|
+
- $gt/$gte: Matches values greater than (or equal to) a specified value
|
|
2074
|
+
- $lt/$lte: Matches values less than (or equal to) a specified value
|
|
2075
|
+
- $in: Matches any of the values in an array
|
|
2076
|
+
- $nin: Matches none of the values in an array
|
|
2077
|
+
- $ne: Matches values not equal to a specified value
|
|
2078
|
+
- $exists: Matches documents that have the specified field
|
|
2079
|
+
|
|
2080
|
+
Example queries:
|
|
2081
|
+
1. Find documents where age > 21:
|
|
2082
|
+
{ "age": { "$gt": 21 } }
|
|
2083
|
+
|
|
2084
|
+
2. Find documents with specific status:
|
|
2085
|
+
{ "status": { "$in": ["active", "pending"] } }
|
|
2086
|
+
|
|
2087
|
+
3. Find documents with existing email:
|
|
2088
|
+
{ "email": { "$exists": true } }
|
|
2089
|
+
|
|
2090
|
+
4. Find documents with dates:
|
|
2091
|
+
// Using ISO date string format
|
|
2092
|
+
{ "createdAt": { "$gt": "2023-01-01T00:00:00Z" } }
|
|
2093
|
+
// Using ISODate syntax
|
|
2094
|
+
{ "createdAt": { "$gt": ISODate("2023-01-01T00:00:00Z") } }
|
|
2095
|
+
|
|
2096
|
+
Use these patterns to construct MongoDB queries.`
|
|
1652
2097
|
}
|
|
1653
2098
|
]
|
|
1654
2099
|
};
|
|
@@ -1662,12 +2107,13 @@ function createSendProgress(progressToken, notify) {
|
|
|
1662
2107
|
params: { progressToken, progress, total, message }
|
|
1663
2108
|
});
|
|
1664
2109
|
}
|
|
1665
|
-
function createServer(client, db, isReadOnlyMode = false, allowCrossDb = false, allowServerJs = false, options = {}) {
|
|
2110
|
+
function createServer(client, db, isReadOnlyMode = false, allowCrossDb = false, allowServerJs = false, options = {}, connections2, queryLimits) {
|
|
2111
|
+
const defaultTarget = { client, db, isReadOnlyMode };
|
|
1666
2112
|
const server = new Server(
|
|
1667
2113
|
{
|
|
1668
2114
|
name: "mongodb",
|
|
1669
2115
|
title: "MongoDB MCP Server",
|
|
1670
|
-
version: "3.0
|
|
2116
|
+
version: "3.2.0",
|
|
1671
2117
|
description: "MCP server for MongoDB: query, aggregate, and manage collections with read-only mode, progress notifications, and cancellation",
|
|
1672
2118
|
websiteUrl: "https://github.com/kiliczsh/mcp-mongo-server",
|
|
1673
2119
|
...options
|
|
@@ -1692,23 +2138,27 @@ function createServer(client, db, isReadOnlyMode = false, allowCrossDb = false,
|
|
|
1692
2138
|
signal: ctx.mcpReq.signal
|
|
1693
2139
|
})
|
|
1694
2140
|
);
|
|
1695
|
-
server.setRequestHandler(
|
|
1696
|
-
|
|
1697
|
-
|
|
2141
|
+
server.setRequestHandler("resources/list", (request, ctx) => {
|
|
2142
|
+
if (connections2?.named) {
|
|
2143
|
+
return listNamedResources(
|
|
2144
|
+
connections2,
|
|
2145
|
+
request.params?.cursor,
|
|
2146
|
+
ctx.mcpReq.signal
|
|
2147
|
+
);
|
|
2148
|
+
}
|
|
2149
|
+
return handleListResourcesRequest({
|
|
1698
2150
|
request,
|
|
1699
2151
|
client,
|
|
1700
2152
|
db,
|
|
1701
2153
|
isReadOnlyMode,
|
|
1702
2154
|
signal: ctx.mcpReq.signal
|
|
1703
|
-
})
|
|
1704
|
-
);
|
|
2155
|
+
});
|
|
2156
|
+
});
|
|
1705
2157
|
server.setRequestHandler(
|
|
1706
2158
|
"resources/read",
|
|
1707
|
-
(request, ctx) => handleReadResourceRequest({
|
|
2159
|
+
async (request, ctx) => handleReadResourceRequest({
|
|
1708
2160
|
request,
|
|
1709
|
-
|
|
1710
|
-
db,
|
|
1711
|
-
isReadOnlyMode,
|
|
2161
|
+
...connections2 ? await connections2.resolve(resourceSelectors(request.params.uri)) : defaultTarget,
|
|
1712
2162
|
signal: ctx.mcpReq.signal,
|
|
1713
2163
|
sendProgress: createSendProgress(
|
|
1714
2164
|
request.params._meta?.progressToken,
|
|
@@ -1723,6 +2173,9 @@ function createServer(client, db, isReadOnlyMode = false, allowCrossDb = false,
|
|
|
1723
2173
|
client,
|
|
1724
2174
|
db,
|
|
1725
2175
|
isReadOnlyMode,
|
|
2176
|
+
connectionSelection: !!connections2,
|
|
2177
|
+
hasWritableConnection: connections2?.list().some(({ readOnly }) => !readOnly),
|
|
2178
|
+
queryLimits,
|
|
1726
2179
|
signal: ctx.mcpReq.signal
|
|
1727
2180
|
})
|
|
1728
2181
|
);
|
|
@@ -1735,6 +2188,8 @@ function createServer(client, db, isReadOnlyMode = false, allowCrossDb = false,
|
|
|
1735
2188
|
isReadOnlyMode,
|
|
1736
2189
|
allowCrossDb,
|
|
1737
2190
|
allowServerJs,
|
|
2191
|
+
connections: connections2,
|
|
2192
|
+
queryLimits,
|
|
1738
2193
|
signal: ctx.mcpReq.signal
|
|
1739
2194
|
})
|
|
1740
2195
|
);
|
|
@@ -1745,16 +2200,16 @@ function createServer(client, db, isReadOnlyMode = false, allowCrossDb = false,
|
|
|
1745
2200
|
client,
|
|
1746
2201
|
db,
|
|
1747
2202
|
isReadOnlyMode,
|
|
2203
|
+
connectionSelection: !!connections2,
|
|
1748
2204
|
signal: ctx.mcpReq.signal
|
|
1749
2205
|
})
|
|
1750
2206
|
);
|
|
1751
2207
|
server.setRequestHandler(
|
|
1752
2208
|
"prompts/get",
|
|
1753
|
-
(request, ctx) => handleGetPromptRequest({
|
|
2209
|
+
async (request, ctx) => handleGetPromptRequest({
|
|
1754
2210
|
request,
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
isReadOnlyMode,
|
|
2211
|
+
...connections2 ? await connections2.resolve(request.params.arguments) : defaultTarget,
|
|
2212
|
+
showTarget: !!connections2?.named,
|
|
1758
2213
|
signal: ctx.mcpReq.signal,
|
|
1759
2214
|
sendProgress: createSendProgress(
|
|
1760
2215
|
request.params._meta?.progressToken,
|
|
@@ -1762,31 +2217,94 @@ function createServer(client, db, isReadOnlyMode = false, allowCrossDb = false,
|
|
|
1762
2217
|
)
|
|
1763
2218
|
})
|
|
1764
2219
|
);
|
|
1765
|
-
server.setRequestHandler(
|
|
1766
|
-
|
|
1767
|
-
(request, ctx) => handleListResourceTemplatesRequest({
|
|
2220
|
+
server.setRequestHandler("resources/templates/list", async (request, ctx) => {
|
|
2221
|
+
const result = await handleListResourceTemplatesRequest({
|
|
1768
2222
|
request,
|
|
1769
2223
|
client,
|
|
1770
2224
|
db,
|
|
1771
2225
|
isReadOnlyMode,
|
|
1772
2226
|
signal: ctx.mcpReq.signal
|
|
1773
|
-
})
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
2227
|
+
});
|
|
2228
|
+
if (connections2?.named) {
|
|
2229
|
+
result.resourceTemplates.push({
|
|
2230
|
+
...result.resourceTemplates[0],
|
|
2231
|
+
name: "mongodb_connection_collection",
|
|
2232
|
+
uriTemplate: "mongodb:///{collection}{?connectionId,database}"
|
|
2233
|
+
});
|
|
2234
|
+
}
|
|
2235
|
+
return result;
|
|
2236
|
+
});
|
|
2237
|
+
server.setRequestHandler("completion/complete", async (request, ctx) => {
|
|
2238
|
+
const { ref, argument, context } = request.params;
|
|
2239
|
+
const selectors = {
|
|
2240
|
+
...context?.arguments,
|
|
2241
|
+
...ref.type === "ref/resource" ? resourceSelectors(ref.uri) : {}
|
|
2242
|
+
};
|
|
2243
|
+
if (connections2 && (argument.name === "connectionId" || argument.name === "database")) {
|
|
2244
|
+
const candidates = argument.name === "connectionId" ? connections2.list().map(({ connectionId }) => connectionId) : await connections2.listDatabases(selectors.connectionId);
|
|
2245
|
+
const matches = candidates.filter(
|
|
2246
|
+
(value) => value.startsWith(argument.value)
|
|
2247
|
+
);
|
|
2248
|
+
return {
|
|
2249
|
+
completion: {
|
|
2250
|
+
values: matches.slice(0, 100),
|
|
2251
|
+
total: matches.length,
|
|
2252
|
+
hasMore: matches.length > 100
|
|
2253
|
+
}
|
|
2254
|
+
};
|
|
2255
|
+
}
|
|
2256
|
+
return handleCompletionRequest({
|
|
1778
2257
|
request,
|
|
1779
|
-
|
|
1780
|
-
db,
|
|
1781
|
-
isReadOnlyMode,
|
|
2258
|
+
...connections2 ? await connections2.resolve(selectors) : defaultTarget,
|
|
1782
2259
|
signal: ctx.mcpReq.signal
|
|
1783
|
-
})
|
|
1784
|
-
);
|
|
2260
|
+
});
|
|
2261
|
+
});
|
|
1785
2262
|
return server;
|
|
1786
2263
|
}
|
|
2264
|
+
async function listNamedResources(connections2, cursor, signal) {
|
|
2265
|
+
const prefix = `${connections2.cursorScope}.`;
|
|
2266
|
+
const offsetCursor = cursor?.slice(prefix.length);
|
|
2267
|
+
const offset = decodeCursor(offsetCursor);
|
|
2268
|
+
if (cursor !== void 0 && (!cursor.startsWith(prefix) || !Number.isSafeInteger(offset) || encodeCursor(offset) !== offsetCursor)) {
|
|
2269
|
+
throw new ProtocolError4(
|
|
2270
|
+
ProtocolErrorCode4.InvalidParams,
|
|
2271
|
+
"Invalid resource cursor for the current connection configuration"
|
|
2272
|
+
);
|
|
2273
|
+
}
|
|
2274
|
+
const resources = [];
|
|
2275
|
+
for (const { connectionId, defaultDatabase } of connections2.list()) {
|
|
2276
|
+
try {
|
|
2277
|
+
signal?.throwIfAborted();
|
|
2278
|
+
for (const database of await connections2.listDatabases(connectionId)) {
|
|
2279
|
+
const { db } = await connections2.resolve({ connectionId, database });
|
|
2280
|
+
const collections = await db.listCollections({}, { nameOnly: true }).toArray();
|
|
2281
|
+
for (const name of collections.map(({ name: name2 }) => name2).sort()) {
|
|
2282
|
+
if (name.startsWith("system.")) continue;
|
|
2283
|
+
const query = connectionId === "default" && database === defaultDatabase ? "" : `?${new URLSearchParams({ connectionId, database })}`;
|
|
2284
|
+
resources.push({
|
|
2285
|
+
uri: `mongodb:///${encodeURIComponent(name)}${query}`,
|
|
2286
|
+
name,
|
|
2287
|
+
mimeType: "application/json",
|
|
2288
|
+
description: `MongoDB collection: ${connectionId}/${database}/${name}`
|
|
2289
|
+
});
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
} catch (error) {
|
|
2293
|
+
if (signal?.aborted) throw error;
|
|
2294
|
+
throw new Error(
|
|
2295
|
+
`Failed to list resources for MongoDB connection '${connectionId}'`
|
|
2296
|
+
);
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
const { items, nextCursor } = paginate(resources, offsetCursor);
|
|
2300
|
+
return {
|
|
2301
|
+
resources: items,
|
|
2302
|
+
...nextCursor ? { nextCursor: prefix + nextCursor } : {}
|
|
2303
|
+
};
|
|
2304
|
+
}
|
|
1787
2305
|
|
|
1788
2306
|
// src/index.ts
|
|
1789
|
-
var
|
|
2307
|
+
var connections;
|
|
1790
2308
|
async function main() {
|
|
1791
2309
|
const args = process.argv.slice(2);
|
|
1792
2310
|
let connectionUrl = "";
|
|
@@ -1835,34 +2353,14 @@ async function main() {
|
|
|
1835
2353
|
if (!connectionUrl) {
|
|
1836
2354
|
connectionUrl = process.env.MCP_MONGODB_URI || "";
|
|
1837
2355
|
}
|
|
1838
|
-
if (!connectionUrl) {
|
|
1839
|
-
console.error(
|
|
1840
|
-
"Please provide a MongoDB connection URL via command-line argument or MCP_MONGODB_URI environment variable"
|
|
1841
|
-
);
|
|
1842
|
-
console.error(
|
|
1843
|
-
"Usage: command <mongodb-url> [--read-only|-r] [--transport stdio|http] [--port 3001]"
|
|
1844
|
-
);
|
|
1845
|
-
console.error(
|
|
1846
|
-
" or: MCP_MONGODB_URI=<mongodb-url> [MCP_MONGODB_READONLY=true] command"
|
|
1847
|
-
);
|
|
1848
|
-
process.exit(1);
|
|
1849
|
-
}
|
|
1850
|
-
if (!connectionUrl.startsWith("mongodb://") && !connectionUrl.startsWith("mongodb+srv://")) {
|
|
1851
|
-
console.error(
|
|
1852
|
-
"Invalid MongoDB connection URL. URL must start with 'mongodb://' or 'mongodb+srv://'"
|
|
1853
|
-
);
|
|
1854
|
-
process.exit(1);
|
|
1855
|
-
}
|
|
1856
2356
|
try {
|
|
1857
|
-
const
|
|
2357
|
+
const queryLimits = readQueryLimits(process.env);
|
|
2358
|
+
connections = new Connections(
|
|
1858
2359
|
connectionUrl,
|
|
2360
|
+
process.env.MCP_MONGODB_CONNECTIONS,
|
|
1859
2361
|
readOnlyMode
|
|
1860
2362
|
);
|
|
1861
|
-
|
|
1862
|
-
if (!isConnected || !client || !db) {
|
|
1863
|
-
console.error("Failed to connect to MongoDB");
|
|
1864
|
-
process.exit(1);
|
|
1865
|
-
}
|
|
2363
|
+
const { client, db, isReadOnlyMode } = await connections.resolve();
|
|
1866
2364
|
if (transportMode === "http") {
|
|
1867
2365
|
await startHttpServer(
|
|
1868
2366
|
client,
|
|
@@ -1870,6 +2368,8 @@ async function main() {
|
|
|
1870
2368
|
isReadOnlyMode,
|
|
1871
2369
|
allowCrossDb,
|
|
1872
2370
|
allowServerJs,
|
|
2371
|
+
connections,
|
|
2372
|
+
queryLimits,
|
|
1873
2373
|
port,
|
|
1874
2374
|
allowedOrigins,
|
|
1875
2375
|
jsonLimit,
|
|
@@ -1881,20 +2381,29 @@ async function main() {
|
|
|
1881
2381
|
db,
|
|
1882
2382
|
isReadOnlyMode,
|
|
1883
2383
|
allowCrossDb,
|
|
1884
|
-
allowServerJs
|
|
2384
|
+
allowServerJs,
|
|
2385
|
+
connections,
|
|
2386
|
+
queryLimits
|
|
1885
2387
|
);
|
|
1886
2388
|
}
|
|
1887
2389
|
} catch (error) {
|
|
1888
|
-
console.error("Failed to
|
|
1889
|
-
|
|
1890
|
-
await mongoClient.close();
|
|
1891
|
-
}
|
|
2390
|
+
console.error("Failed to start MongoDB MCP server:", error);
|
|
2391
|
+
await connections?.close();
|
|
1892
2392
|
process.exit(1);
|
|
1893
2393
|
}
|
|
1894
2394
|
}
|
|
1895
|
-
async function startStdioServer(client, db, isReadOnlyMode, allowCrossDb, allowServerJs) {
|
|
2395
|
+
async function startStdioServer(client, db, isReadOnlyMode, allowCrossDb, allowServerJs, connections2, queryLimits) {
|
|
1896
2396
|
serveStdio(
|
|
1897
|
-
() => createServer(
|
|
2397
|
+
() => createServer(
|
|
2398
|
+
client,
|
|
2399
|
+
db,
|
|
2400
|
+
isReadOnlyMode,
|
|
2401
|
+
allowCrossDb,
|
|
2402
|
+
allowServerJs,
|
|
2403
|
+
{},
|
|
2404
|
+
connections2,
|
|
2405
|
+
queryLimits
|
|
2406
|
+
)
|
|
1898
2407
|
);
|
|
1899
2408
|
console.warn("Server connected successfully via stdio");
|
|
1900
2409
|
}
|
|
@@ -1921,7 +2430,7 @@ function bearerTokenValid(authorization, expected) {
|
|
|
1921
2430
|
const wanted = Buffer.from(expected);
|
|
1922
2431
|
return provided.length === wanted.length && timingSafeEqual(provided, wanted);
|
|
1923
2432
|
}
|
|
1924
|
-
async function startHttpServer(client, db, isReadOnlyMode, allowCrossDb, allowServerJs, port, allowedOrigins, jsonLimit, authToken) {
|
|
2433
|
+
async function startHttpServer(client, db, isReadOnlyMode, allowCrossDb, allowServerJs, connections2, queryLimits, port, allowedOrigins, jsonLimit, authToken) {
|
|
1925
2434
|
const app = createMcpExpressApp({ host: "0.0.0.0", jsonLimit });
|
|
1926
2435
|
app.use((req, res, next) => {
|
|
1927
2436
|
const start = Date.now();
|
|
@@ -1972,7 +2481,16 @@ async function startHttpServer(client, db, isReadOnlyMode, allowCrossDb, allowSe
|
|
|
1972
2481
|
});
|
|
1973
2482
|
}
|
|
1974
2483
|
const mcpHandler = createMcpHandler(
|
|
1975
|
-
(_ctx) => createServer(
|
|
2484
|
+
(_ctx) => createServer(
|
|
2485
|
+
client,
|
|
2486
|
+
db,
|
|
2487
|
+
isReadOnlyMode,
|
|
2488
|
+
allowCrossDb,
|
|
2489
|
+
allowServerJs,
|
|
2490
|
+
{},
|
|
2491
|
+
connections2,
|
|
2492
|
+
queryLimits
|
|
2493
|
+
)
|
|
1976
2494
|
);
|
|
1977
2495
|
const nodeHandler = toNodeHandler(mcpHandler, {
|
|
1978
2496
|
onerror: (error) => console.error("Error handling MCP request:", error)
|
|
@@ -1999,15 +2517,11 @@ async function startHttpServer(client, db, isReadOnlyMode, allowCrossDb, allowSe
|
|
|
1999
2517
|
});
|
|
2000
2518
|
}
|
|
2001
2519
|
process.on("SIGINT", async () => {
|
|
2002
|
-
|
|
2003
|
-
await mongoClient.close();
|
|
2004
|
-
}
|
|
2520
|
+
await connections?.close();
|
|
2005
2521
|
process.exit(0);
|
|
2006
2522
|
});
|
|
2007
2523
|
process.on("SIGTERM", async () => {
|
|
2008
|
-
|
|
2009
|
-
await mongoClient.close();
|
|
2010
|
-
}
|
|
2524
|
+
await connections?.close();
|
|
2011
2525
|
process.exit(0);
|
|
2012
2526
|
});
|
|
2013
2527
|
main().catch((error) => {
|