mcp-mongo-server 1.3.0 → 2.1.0

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