This is page 3 of 3. Use http://codebase.md/furey/mongodb-lens?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .dockerhub
│ └── overview.md
├── .dockerignore
├── .github
│ ├── FUNDING.yml
│ └── workflows
│ ├── publish-docker.yml
│ └── publish-npm.yml
├── .gitignore
├── config-create.js
├── Dockerfile
├── LICENSE
├── mongodb-lens.js
├── mongodb-lens.test.js
├── package-lock.json
├── package.json
└── README.md
```
# Files
--------------------------------------------------------------------------------
/mongodb-lens.js:
--------------------------------------------------------------------------------
```javascript
1 | #!/usr/bin/env node
2 |
3 | import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js'
4 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
5 | import stripJsonComments from 'strip-json-comments'
6 | import { readFileSync, existsSync } from 'fs'
7 | import { dirname, join } from 'path'
8 | import { fileURLToPath } from 'url'
9 | import mongodb from 'mongodb'
10 | import { EJSON } from 'bson'
11 | import { z } from 'zod'
12 | import _ from 'lodash'
13 |
14 | const { MongoClient, ObjectId, GridFSBucket } = mongodb
15 |
16 | const __filename = fileURLToPath(import.meta.url)
17 | const __dirname = dirname(__filename)
18 |
19 | const start = async (mongoUri) => {
20 | log(`MongoDB Lens v${getPackageVersion()} starting…`, true)
21 |
22 | mongoUriCurrent = mongoUri
23 | mongoUriOriginal = mongoUri
24 |
25 | const connected = await connect(mongoUri)
26 | if (!connected) return log('Failed to connect to MongoDB database.', true) || false
27 |
28 | log('Starting watchdog…')
29 | startWatchdog()
30 |
31 | await setProfilerSlowMs()
32 |
33 | log('Initializing MCP server…')
34 | server = new McpServer({
35 | name: 'MongoDB Lens',
36 | version: getPackageVersion(),
37 | description: 'MongoDB MCP server for natural language database interaction',
38 | homepage: 'https://github.com/furey/mongodb-lens',
39 | license: 'MIT',
40 | vendor: {
41 | name: 'James Furey',
42 | url: 'https://about.me/jamesfurey'
43 | }
44 | }, {
45 | instructions
46 | })
47 |
48 | server.fallbackRequestHandler = async request => {
49 | log(`Received request for undefined method: ${request.method}`, true)
50 | const error = new Error(`Method '${request.method}' not found`)
51 | error.code = JSONRPC_ERROR_CODES.METHOD_NOT_FOUND
52 | throw error
53 | }
54 |
55 | log('Registering MCP resources…')
56 | registerResources(server)
57 |
58 | log('Registering MCP prompts…')
59 | registerPrompts(server)
60 |
61 | log('Registering MCP tools…')
62 | registerTools(server)
63 |
64 | log('Creating stdio transport…')
65 | transport = new StdioServerTransport()
66 |
67 | log('Connecting MCP server transport…')
68 | await server.connect(transport)
69 |
70 | log('MongoDB Lens server running.', true)
71 | return true
72 | }
73 |
74 | const isCacheEnabled = (cacheType) =>
75 | config.enabledCaches.includes(cacheType)
76 |
77 | const getCachedValue = (cacheType, key) => {
78 | if (!isCacheEnabled(cacheType)) return null
79 |
80 | const cache = memoryCache[cacheType]
81 | const cachedData = cache.get(key)
82 |
83 | if (cachedData && (Date.now() - cachedData.timestamp) < config.cacheTTL[cacheType]) {
84 | log(`Using cached ${cacheType} for '${key}'`)
85 | return cachedData.data
86 | }
87 |
88 | return null
89 | }
90 |
91 | const setCachedValue = (cacheType, key, data) => {
92 | if (!isCacheEnabled(cacheType)) return
93 |
94 | memoryCache[cacheType].set(key, {
95 | data,
96 | timestamp: Date.now()
97 | })
98 | }
99 |
100 | const invalidateRelatedCaches = (dbName, collectionName) => {
101 | if (collectionName) {
102 | const collKey = `${dbName}.${collectionName}`
103 | memoryCache.schemas.delete(collKey)
104 | memoryCache.fields.delete(collKey)
105 | memoryCache.indexes.delete(collKey)
106 | memoryCache.stats.delete(collKey)
107 |
108 | for (const key of memoryCache.schemas.keys()) {
109 | if (key.startsWith(collKey + '.')) memoryCache.schemas.delete(key)
110 | }
111 | }
112 |
113 | memoryCache.collections.delete(dbName)
114 | }
115 |
116 | const clearMemoryCache = () => {
117 | for (const cacheType of config.enabledCaches) {
118 | if (memoryCache[cacheType]) {
119 | log(`Clearing ${cacheType} cache`)
120 | memoryCache[cacheType].clear()
121 | }
122 | }
123 | }
124 |
125 | const connect = async (uri = 'mongodb://localhost:27017', validate = true) => {
126 | try {
127 | log(`Connecting to MongoDB at: ${obfuscateMongoUri(uri)}`)
128 |
129 | const connectionOptions = { ...config.connectionOptions }
130 |
131 | mongoClient = new MongoClient(uri, connectionOptions)
132 |
133 | let retryCount = 0
134 | const maxRetries = config.connection.maxRetries
135 |
136 | while (retryCount < maxRetries) {
137 | try {
138 | await mongoClient.connect()
139 | break
140 | } catch (connectionError) {
141 | retryCount++
142 | if (retryCount >= maxRetries) throw connectionError
143 | const delay = Math.min(config.connection.initialRetryDelayMs * Math.pow(2, retryCount), config.connection.maxRetryDelayMs)
144 | log(`Connection attempt ${retryCount} failed, retrying in ${delay/1000} seconds…`)
145 | await new Promise(resolve => setTimeout(resolve, delay))
146 | }
147 | }
148 |
149 | if (validate) {
150 | try {
151 | const adminDb = mongoClient.db('admin')
152 | const serverInfo = await adminDb.command({ buildInfo: 1 })
153 | log(`Connected to MongoDB server version: ${serverInfo.version}`)
154 | setCachedValue('serverStatus', 'server_info', serverInfo)
155 | } catch (infoError) {
156 | log(`Warning: Unable to get server info: ${infoError.message}`)
157 | }
158 | }
159 |
160 | currentDbName = extractDbNameFromConnectionString(uri)
161 | currentDb = mongoClient.db(currentDbName)
162 |
163 | if (validate) {
164 | try {
165 | await currentDb.stats()
166 | } catch (statsError) {
167 | log(`Warning: Unable to get database stats: ${statsError.message}`)
168 | }
169 | }
170 |
171 | mongoClient.on('error', err => {
172 | log(`MongoDB connection error: ${err.message}. Will attempt to reconnect.`, true)
173 | })
174 |
175 | mongoClient.on('close', () => {
176 | if (!isShuttingDown) {
177 | log('MongoDB connection closed. Will attempt to reconnect.', true)
178 | } else {
179 | log('MongoDB connection closed during shutdown.')
180 | }
181 | })
182 |
183 | log(`Connected to MongoDB successfully, using database: ${currentDbName}`)
184 | mongoUriCurrent = uri
185 | return true
186 | } catch (error) {
187 | log(`MongoDB connection error: ${error.message}`, true)
188 | return false
189 | }
190 | }
191 |
192 | const startWatchdog = () => {
193 | if (watchdog) clearInterval(watchdog)
194 |
195 | watchdog = setInterval(() => {
196 | const memoryUsage = process.memoryUsage()
197 | const heapUsedMB = Math.round(memoryUsage.heapUsed / 1024 / 1024)
198 | const heapTotalMB = Math.round(memoryUsage.heapTotal / 1024 / 1024)
199 |
200 | if (heapUsedMB > config.memory.warningThresholdMB) {
201 | log(`High memory usage: ${heapUsedMB}MB used of ${heapTotalMB}MB heap`, true)
202 |
203 | if (heapUsedMB > config.memory.criticalThresholdMB) {
204 | log('Critical memory pressure. Clearing caches…', true)
205 | clearMemoryCache()
206 | if (config.memory.enableGC && global.gc) {
207 | global.gc()
208 | } else if (config.memory.enableGC) {
209 | log('Garbage collection requested but not available. Run Node.js with --expose-gc flag.', true)
210 | }
211 | }
212 | }
213 |
214 | const isClientConnected = mongoClient && mongoClient.topology && mongoClient.topology.isConnected()
215 | if (!isClientConnected && !isChangingConnection) {
216 | log('Detected MongoDB disconnection. Attempting reconnect…', true)
217 | reconnect()
218 | }
219 | }, config.watchdogIntervalMs)
220 | }
221 |
222 | const reconnect = async () => {
223 | if (connectionRetries > config.connection.reconnectionRetries) {
224 | log('Maximum reconnection attempts reached. Giving up.', true)
225 | return false
226 | }
227 |
228 | connectionRetries++
229 | log(`Reconnection attempt ${connectionRetries}…`, true)
230 |
231 | try {
232 | await mongoClient.connect()
233 | log('Reconnected to MongoDB successfully', true)
234 | connectionRetries = 0
235 | return true
236 | } catch (error) {
237 | log(`Reconnection failed: ${error.message}`, true)
238 | return false
239 | }
240 | }
241 |
242 | const extractDbNameFromConnectionString = (uri) => {
243 | const pathParts = uri.split('/').filter(part => part)
244 | const lastPart = pathParts[pathParts.length - 1]?.split('?')[0]
245 | currentDbName = (lastPart && !lastPart.includes(':')) ? lastPart : config.defaultDbName
246 | return currentDbName
247 | }
248 |
249 | const resolveMongoUri = (uriOrAlias) => {
250 | if (uriOrAlias.includes('://') || uriOrAlias.includes('@')) return uriOrAlias
251 |
252 | const uri = mongoUriMap.get(uriOrAlias.toLowerCase())
253 | if (uri) {
254 | mongoUriAlias = uriOrAlias
255 | log(`Resolved alias "${uriOrAlias}" to URI: ${uri}`)
256 | return uri
257 | }
258 |
259 | throw new Error(`"${uriOrAlias}" is not a valid MongoDB URI or connection alias.`)
260 | }
261 |
262 | const obfuscateMongoUri = (uri) => {
263 | if (!uri || typeof uri !== 'string') return uri
264 | try {
265 | if (uri.includes('@') && uri.includes('://')) {
266 | const parts = uri.split('@')
267 | const authPart = parts[0]
268 | const restPart = parts.slice(1).join('@')
269 | const authIndex = authPart.lastIndexOf('://')
270 | if (authIndex !== -1) {
271 | const protocol = authPart.substring(0, authIndex + 3)
272 | const credentials = authPart.substring(authIndex + 3)
273 | if (credentials.includes(':')) {
274 | const [username, ...passwordParts] = credentials.split(':')
275 | return `${protocol}${username}:********@${restPart}`
276 | }
277 | }
278 | }
279 | return uri
280 | } catch (error) {
281 | log(`Error obfuscating URI: ${error.message}`, true)
282 | return uri
283 | }
284 | }
285 |
286 | const changeConnection = async (uri, validate = true) => {
287 | isChangingConnection = true
288 | try {
289 | if (mongoClient) await mongoClient.close()
290 | clearMemoryCache()
291 | const resolvedUri = resolveMongoUri(uri)
292 | const connected = await connect(resolvedUri, validate)
293 | isChangingConnection = false
294 | if (!connected) throw new Error(`Failed to connect to MongoDB at URI: ${obfuscateMongoUri(resolvedUri)}`)
295 | return true
296 | } catch (error) {
297 | isChangingConnection = false
298 | throw error
299 | }
300 | }
301 |
302 | const isDisabled = (type, name) => {
303 | if (config.enabled && config.enabled[type] !== undefined) {
304 | if (name === 'all') return config.enabled[type] === false
305 | if (Array.isArray(config.enabled[type]) && config.enabled[type].includes(name)) return false
306 | }
307 |
308 | if (config.disabled && config.disabled[type] !== undefined) {
309 | if (name === 'all') return config.disabled[type] === true
310 | if (Array.isArray(config.disabled[type]) && config.disabled[type].includes(name)) return true
311 | }
312 |
313 | if (config.enabled && Array.isArray(config.enabled[type]))
314 | return !config.enabled[type].includes(name)
315 |
316 | return false
317 | }
318 |
319 | const registerResources = (server) => {
320 | if (isDisabled('resources', 'all')) {
321 | log('All MCP resources disabled via configuration', true)
322 | return
323 | }
324 |
325 | if (!isDisabled('resources', 'databases')) {
326 | server.resource(
327 | 'databases',
328 | 'mongodb://databases',
329 | { description: 'List of all accessible MongoDB databases' },
330 | async () => {
331 | log('Resource: Retrieving list of databases…')
332 | const dbs = await listDatabases()
333 | log(`Resource: Found ${dbs.length} databases.`)
334 | return {
335 | contents: [{
336 | uri: 'mongodb://databases',
337 | text: formatDatabasesList(dbs)
338 | }]
339 | }
340 | }
341 | )
342 | }
343 |
344 | if (!isDisabled('resources', 'database-users')) {
345 | server.resource(
346 | 'database-users',
347 | 'mongodb://database/users',
348 | { description: 'MongoDB database users and roles' },
349 | async () => {
350 | log('Resource: Retrieving database users…')
351 | const users = await getDatabaseUsers()
352 | log(`Resource: Retrieved user information.`)
353 | return {
354 | contents: [{
355 | uri: 'mongodb://database/users',
356 | text: formatDatabaseUsers(users)
357 | }]
358 | }
359 | }
360 | )
361 | }
362 |
363 | if (!isDisabled('resources', 'database-triggers')) {
364 | server.resource(
365 | 'database-triggers',
366 | 'mongodb://database/triggers',
367 | { description: 'Database change streams and event triggers configuration' },
368 | async () => {
369 | log('Resource: Retrieving database triggers and event configuration…')
370 | const triggers = await getDatabaseTriggers()
371 | return {
372 | contents: [{
373 | uri: 'mongodb://database/triggers',
374 | text: formatTriggerConfiguration(triggers)
375 | }]
376 | }
377 | }
378 | )
379 | }
380 |
381 | if (!isDisabled('resources', 'stored-functions')) {
382 | server.resource(
383 | 'stored-functions',
384 | 'mongodb://database/functions',
385 | { description: 'MongoDB stored JavaScript functions' },
386 | async () => {
387 | log('Resource: Retrieving stored JavaScript functions…')
388 | const functions = await getStoredFunctions()
389 | log(`Resource: Retrieved stored functions.`)
390 | return {
391 | contents: [{
392 | uri: 'mongodb://database/functions',
393 | text: formatStoredFunctions(functions)
394 | }]
395 | }
396 | }
397 | )
398 | }
399 |
400 | if (!isDisabled('resources', 'collections')) {
401 | server.resource(
402 | 'collections',
403 | 'mongodb://collections',
404 | { description: 'List of collections in the current database' },
405 | async () => {
406 | log(`Resource: Retrieving collections from database '${currentDbName}'…`)
407 | const collections = await listCollections()
408 | log(`Resource: Found ${collections.length} collections.`)
409 | return {
410 | contents: [{
411 | uri: 'mongodb://collections',
412 | text: formatCollectionsList(collections)
413 | }]
414 | }
415 | }
416 | )
417 | }
418 |
419 | if (!isDisabled('resources', 'collection-indexes')) {
420 | server.resource(
421 | 'collection-indexes',
422 | new ResourceTemplate('mongodb://collection/{name}/indexes', {
423 | list: async () => {
424 | try {
425 | log('Resource: Listing collection indexes resources…')
426 | const collections = await listCollections()
427 | log(`Resource: Preparing index resources for ${collections.length} collections.`)
428 | return {
429 | resources: collections.map(coll => ({
430 | uri: `mongodb://collection/${coll.name}/indexes`,
431 | name: `${coll.name} Indexes`,
432 | description: `Indexes for ${coll.name} collection`
433 | }))
434 | }
435 | } catch (error) {
436 | log(`Error listing collections for indexes: ${error.message}`, true)
437 | return { resources: [] }
438 | }
439 | },
440 | complete: {
441 | name: async (value) => {
442 | try {
443 | log(`Resource: Autocompleting collection name for indexes with prefix '${value}'…`)
444 | const collections = await listCollections()
445 | const matches = collections
446 | .map(coll => coll.name)
447 | .filter(name => name.toLowerCase().includes(value.toLowerCase()))
448 | log(`Resource: Found ${matches.length} matching collections for indexes.`)
449 | return matches
450 | } catch (error) {
451 | log(`Error completing collection names: ${error.message}`, true)
452 | return []
453 | }
454 | }
455 | }
456 | }),
457 | { description: 'Index information for a MongoDB collection' },
458 | async (uri, { name }) => {
459 | log(`Resource: Retrieving indexes for collection '${name}'…`)
460 | const indexes = await getCollectionIndexes(name)
461 | log(`Resource: Retrieved ${indexes.length} indexes for collection '${name}'.`)
462 | return {
463 | contents: [{
464 | uri: uri.href,
465 | text: formatIndexes(indexes)
466 | }]
467 | }
468 | }
469 | )
470 | }
471 |
472 | if (!isDisabled('resources', 'collection-schema')) {
473 | server.resource(
474 | 'collection-schema',
475 | new ResourceTemplate('mongodb://collection/{name}/schema', {
476 | list: async () => {
477 | try {
478 | log('Resource: Listing collection schemas…')
479 | const collections = await listCollections()
480 | log(`Resource: Preparing schema resources for ${collections.length} collections.`)
481 | return {
482 | resources: collections.map(coll => ({
483 | uri: `mongodb://collection/${coll.name}/schema`,
484 | name: `${coll.name} Schema`,
485 | description: `Schema for ${coll.name} collection`
486 | }))
487 | }
488 | } catch (error) {
489 | log(`Error listing collection schemas: ${error.message}`, true)
490 | return { resources: [] }
491 | }
492 | },
493 | complete: {
494 | name: async (value) => {
495 | try {
496 | log(`Resource: Autocompleting collection name with prefix '${value}'…`)
497 | const collections = await listCollections()
498 | const matches = collections
499 | .map(coll => coll.name)
500 | .filter(name => name.toLowerCase().includes(value.toLowerCase()))
501 | log(`Resource: Found ${matches.length} matching collections.`)
502 | return matches
503 | } catch (error) {
504 | log(`Error completing collection names: ${error.message}`, true)
505 | return []
506 | }
507 | },
508 | field: async (value, params) => {
509 | try {
510 | if (!params.name) return []
511 |
512 | log(`Resource: Autocompleting field name with prefix '${value}' for collection '${params.name}'…`)
513 | const fields = await getCollectionFields(params.name)
514 | const matches = fields.filter(field =>
515 | field.toLowerCase().includes(value.toLowerCase())
516 | )
517 | log(`Resource: Found ${matches.length} matching fields for collection '${params.name}'.`)
518 | return matches
519 | } catch (error) {
520 | log(`Error completing field names: ${error.message}`, true)
521 | return []
522 | }
523 | }
524 | }
525 | }),
526 | { description: 'Schema information for a MongoDB collection' },
527 | async (uri, { name }) => {
528 | log(`Resource: Inferring schema for collection '${name}'…`)
529 | const schema = await inferSchema(name)
530 | log(`Resource: Schema inference complete for '${name}', identified ${Object.keys(schema.fields).length} fields.`)
531 | return {
532 | contents: [{
533 | uri: uri.href,
534 | text: formatSchema(schema)
535 | }]
536 | }
537 | }
538 | )
539 | }
540 |
541 | if (!isDisabled('resources', 'collection-stats')) {
542 | server.resource(
543 | 'collection-stats',
544 | new ResourceTemplate('mongodb://collection/{name}/stats', {
545 | list: async () => {
546 | try {
547 | log('Resource: Listing collection stats resources…')
548 | const collections = await listCollections()
549 | log(`Resource: Preparing stats resources for ${collections.length} collections.`)
550 | return {
551 | resources: collections.map(coll => ({
552 | uri: `mongodb://collection/${coll.name}/stats`,
553 | name: `${coll.name} Stats`,
554 | description: `Statistics for ${coll.name} collection`
555 | }))
556 | }
557 | } catch (error) {
558 | log(`Error listing collections for stats: ${error.message}`, true)
559 | return { resources: [] }
560 | }
561 | },
562 | complete: {
563 | name: async (value) => {
564 | try {
565 | log(`Resource: Autocompleting collection name for stats with prefix '${value}'…`)
566 | const collections = await listCollections()
567 | const matches = collections
568 | .map(coll => coll.name)
569 | .filter(name => name.toLowerCase().includes(value.toLowerCase()))
570 | log(`Resource: Found ${matches.length} matching collections for stats.`)
571 | return matches
572 | } catch (error) {
573 | log(`Error completing collection names: ${error.message}`, true)
574 | return []
575 | }
576 | }
577 | }
578 | }),
579 | { description: 'Performance statistics for a MongoDB collection' },
580 | async (uri, { name }) => {
581 | log(`Resource: Retrieving stats for collection '${name}'…`)
582 | const stats = await getCollectionStats(name)
583 | log(`Resource: Retrieved stats for collection '${name}'.`)
584 | return {
585 | contents: [{
586 | uri: uri.href,
587 | text: formatStats(stats)
588 | }]
589 | }
590 | }
591 | )
592 | }
593 |
594 | if (!isDisabled('resources', 'collection-validation')) {
595 | server.resource(
596 | 'collection-validation',
597 | new ResourceTemplate('mongodb://collection/{name}/validation', {
598 | list: async () => {
599 | try {
600 | log('Resource: Listing collection validation resources…')
601 | const collections = await listCollections()
602 | log(`Resource: Preparing validation resources for ${collections.length} collections.`)
603 | return {
604 | resources: collections.map(coll => ({
605 | uri: `mongodb://collection/${coll.name}/validation`,
606 | name: `${coll.name} Validation`,
607 | description: `Validation rules for ${coll.name} collection`
608 | }))
609 | }
610 | } catch (error) {
611 | log(`Error listing collections for validation: ${error.message}`, true)
612 | return { resources: [] }
613 | }
614 | },
615 | complete: {
616 | name: async (value) => {
617 | try {
618 | log(`Resource: Autocompleting collection name for validation with prefix '${value}'…`)
619 | const collections = await listCollections()
620 | const matches = collections
621 | .map(coll => coll.name)
622 | .filter(name => name.toLowerCase().includes(value.toLowerCase()))
623 | return matches
624 | } catch (error) {
625 | log(`Error completing collection names: ${error.message}`, true)
626 | return []
627 | }
628 | }
629 | }
630 | }),
631 | { description: 'Validation rules for a MongoDB collection' },
632 | async (uri, { name }) => {
633 | log(`Resource: Retrieving validation rules for collection '${name}'…`)
634 | const validation = await getCollectionValidation(name)
635 | log(`Resource: Retrieved validation rules for collection '${name}'.`)
636 | return {
637 | contents: [{
638 | uri: uri.href,
639 | text: formatValidationRules(validation)
640 | }]
641 | }
642 | }
643 | )
644 | }
645 |
646 | if (!isDisabled('resources', 'server-status')) {
647 | server.resource(
648 | 'server-status',
649 | 'mongodb://server/status',
650 | { description: 'MongoDB server status information' },
651 | async () => {
652 | log('Resource: Retrieving server status…')
653 | const status = await getServerStatus()
654 | log('Resource: Retrieved server status information.')
655 | return {
656 | contents: [{
657 | uri: 'mongodb://server/status',
658 | text: formatServerStatus(status)
659 | }]
660 | }
661 | }
662 | )
663 | }
664 |
665 | if (!isDisabled('resources', 'replica-status')) {
666 | server.resource(
667 | 'replica-status',
668 | 'mongodb://server/replica',
669 | { description: 'MongoDB replica set status and configuration' },
670 | async () => {
671 | log('Resource: Retrieving replica set status…')
672 | const status = await getReplicaSetStatus()
673 | log('Resource: Retrieved replica set status information.')
674 | return {
675 | contents: [{
676 | uri: 'mongodb://server/replica',
677 | text: formatReplicaSetStatus(status)
678 | }]
679 | }
680 | }
681 | )
682 | }
683 |
684 | if (!isDisabled('resources', 'performance-metrics')) {
685 | server.resource(
686 | 'performance-metrics',
687 | 'mongodb://server/metrics',
688 | { description: 'Real-time MongoDB performance metrics and profiling data' },
689 | async () => {
690 | log('Resource: Retrieving performance metrics…')
691 | const metrics = await getPerformanceMetrics()
692 | return {
693 | contents: [{
694 | uri: 'mongodb://server/metrics',
695 | text: formatPerformanceMetrics(metrics)
696 | }]
697 | }
698 | }
699 | )
700 | }
701 |
702 | const totalRegisteredResources =
703 | Object.keys(server._registeredResources).length +
704 | Object.keys(server._registeredResourceTemplates).length
705 | log(`Total MCP resources: ${totalRegisteredResources}`)
706 | }
707 |
708 | const registerPrompts = (server) => {
709 | if (isDisabled('prompts', 'all')) {
710 | log('All MCP prompts disabled via configuration', true)
711 | return
712 | }
713 |
714 | if (!isDisabled('prompts', 'query-builder')) {
715 | server.prompt(
716 | 'query-builder',
717 | 'Help construct MongoDB query filters',
718 | {
719 | collection: z.string().min(1).describe('Collection name to query'),
720 | condition: z.string().describe('Describe the condition in natural language (e.g. "users older than 30")')
721 | },
722 | async ({ collection, condition }) => {
723 | log(`Prompt: Initializing [query-builder] for collection '${collection}' with condition: "${condition}".`)
724 | const fields = await getCollectionFields(collection)
725 | const fieldInfo = fields.length > 0 ? `\nAvailable fields: ${fields.join(', ')}` : ''
726 | return {
727 | description: `MongoDB Query Builder for ${collection}`,
728 | messages: [
729 | {
730 | role: 'user',
731 | content: {
732 | type: 'text',
733 | text: `Please help me create a MongoDB query for the '${collection}' collection based on this condition: "${condition}".
734 |
735 | I need both the filter object and a complete example showing how to use it with the findDocuments tool.
736 |
737 | Guidelines:
738 | 1. Create a valid MongoDB query filter as a JSON object
739 | 2. Show me how special MongoDB operators work if needed (like $gt, $in, etc.)
740 | 3. Provide a complete example of using this with the findDocuments tool
741 | 4. Suggest any relevant projections or sort options
742 | ${fieldInfo}
743 |
744 | Remember: I'm working with the ${currentDbName} database and the ${collection} collection.`
745 | }
746 | }
747 | ]
748 | }
749 | }
750 | )
751 | }
752 |
753 | if (!isDisabled('prompts', 'aggregation-builder')) {
754 | server.prompt(
755 | 'aggregation-builder',
756 | 'Help construct MongoDB aggregation pipelines',
757 | {
758 | collection: z.string().min(1).describe('Collection name for aggregation'),
759 | goal: z.string().describe('What you want to calculate or analyze')
760 | },
761 | ({ collection, goal }) => {
762 | log(`Prompt: Initializing [aggregation-builder] for collection '${collection}' with goal: "${goal}".`)
763 | return {
764 | description: `MongoDB Aggregation Pipeline Builder for ${collection}`,
765 | messages: [
766 | {
767 | role: 'user',
768 | content: {
769 | type: 'text',
770 | text: `I need to create a MongoDB aggregation pipeline for the '${collection}' collection to ${goal}.
771 |
772 | Please help me create:
773 | 1. A complete aggregation pipeline as a JSON array
774 | 2. An explanation of each stage in the pipeline
775 | 3. How to execute this with the aggregateData tool
776 |
777 | Remember: I'm working with the ${currentDbName} database and the ${collection} collection.`
778 | }
779 | }
780 | ]
781 | }
782 | }
783 | )
784 | }
785 |
786 | if (!isDisabled('prompts', 'schema-analysis')) {
787 | server.prompt(
788 | 'schema-analysis',
789 | 'Analyze collection schema and recommend improvements',
790 | {
791 | collection: z.string().min(1).describe('Collection name to analyze')
792 | },
793 | async ({ collection }) => {
794 | log(`Prompt: Initializing [schema-analysis] for collection '${collection}'…`)
795 | const schema = await inferSchema(collection)
796 | log(`Prompt: Retrieved schema for '${collection}' with ${Object.keys(schema.fields).length} fields.`)
797 | return {
798 | description: `MongoDB Schema Analysis for ${collection}`,
799 | messages: [
800 | {
801 | role: 'user',
802 | content: {
803 | type: 'text',
804 | text: `Please analyze the schema of the '${collection}' collection and provide recommendations:
805 |
806 | Here's the current schema:
807 | ${formatSchema(schema)}
808 |
809 | Could you help with:
810 | 1. Identifying any schema design issues or inconsistencies
811 | 2. Suggesting schema improvements for better performance
812 | 3. Recommending appropriate indexes based on the data structure
813 | 4. Best practices for this type of data model
814 | 5. Any potential MongoDB-specific optimizations`
815 | }
816 | }
817 | ]
818 | }
819 | }
820 | )
821 | }
822 |
823 | if (!isDisabled('prompts', 'index-recommendation')) {
824 | server.prompt(
825 | 'index-recommendation',
826 | 'Get index recommendations for query patterns',
827 | {
828 | collection: z.string().min(1).describe('Collection name'),
829 | queryPattern: z.string().describe('Common query pattern or operation')
830 | },
831 | ({ collection, queryPattern }) => {
832 | log(`Prompt: Initializing [index-recommendation] for collection '${collection}' with query pattern: "${queryPattern}".`)
833 | return {
834 | description: `MongoDB Index Recommendations for ${collection}`,
835 | messages: [
836 | {
837 | role: 'user',
838 | content: {
839 | type: 'text',
840 | text: `I need index recommendations for the '${collection}' collection to optimize this query pattern: "${queryPattern}".
841 |
842 | Please provide:
843 | 1. Recommended index(es) with proper key specification
844 | 2. Explanation of why this index would help
845 | 3. The exact command to create this index using the createIndex tool
846 | 4. How to verify the index is being used
847 | 5. Any potential trade-offs or considerations for this index
848 |
849 | Remember: I'm working with the ${currentDbName} database and the ${collection} collection.`
850 | }
851 | }
852 | ]
853 | }
854 | }
855 | )
856 | }
857 |
858 | if (!isDisabled('prompts', 'mongo-shell')) {
859 | server.prompt(
860 | 'mongo-shell',
861 | 'Generate MongoDB shell commands',
862 | {
863 | operation: z.string().describe('Operation you want to perform'),
864 | details: z.string().optional().describe('Additional details about the operation')
865 | },
866 | ({ operation, details }) => {
867 | log(`Prompt: Initializing [mongo-shell] for operation: "${operation}" with${details ? ' details: "' + details + '"' : 'out details'}.`)
868 | return {
869 | description: 'MongoDB Shell Command Generator',
870 | messages: [
871 | {
872 | role: 'user',
873 | content: {
874 | type: 'text',
875 | text: `Please generate MongoDB shell commands to ${operation}${details ? ` with these details: ${details}` : ''}.
876 |
877 | I need:
878 | 1. The exact MongoDB shell command(s)
879 | 2. Explanation of each part of the command
880 | 3. How this translates to using MongoDB Lens tools
881 | 4. Any important considerations or variations
882 |
883 | Current database: ${currentDbName}`
884 | }
885 | }
886 | ]
887 | }
888 | }
889 | )
890 | }
891 |
892 | if (!isDisabled('prompts', 'data-modeling')) {
893 | server.prompt(
894 | 'data-modeling',
895 | 'Get MongoDB data modeling advice for specific use cases',
896 | {
897 | useCase: z.string().describe('Describe your application or data use case'),
898 | requirements: z.string().describe('Key requirements (performance, access patterns, etc.)'),
899 | existingData: z.string().optional().describe('Optional: describe any existing data structure')
900 | },
901 | ({ useCase, requirements, existingData }) => {
902 | log(`Prompt: Initializing [data-modeling] for use case: "${useCase}".`)
903 | return {
904 | description: 'MongoDB Data Modeling Guide',
905 | messages: [
906 | {
907 | role: 'user',
908 | content: {
909 | type: 'text',
910 | text: `I need help designing a MongoDB data model for this use case: "${useCase}".
911 |
912 | Key requirements:
913 | ${requirements}
914 |
915 | ${existingData ? `Existing data structure:\n${existingData}\n\n` : ''}
916 | Please provide:
917 | 1. Recommended data model with collection structures
918 | 2. Sample document structures in JSON format
919 | 3. Explanation of design decisions and trade-offs
920 | 4. Appropriate indexing strategy
921 | 5. Any MongoDB-specific features or patterns I should consider
922 | 6. How this model addresses the stated requirements`
923 | }
924 | }
925 | ]
926 | }
927 | }
928 | )
929 | }
930 |
931 | if (!isDisabled('prompts', 'query-optimizer')) {
932 | server.prompt(
933 | 'query-optimizer',
934 | 'Get optimization advice for slow queries',
935 | {
936 | collection: z.string().min(1).describe('Collection name'),
937 | query: z.string().describe('The slow query (as a JSON filter)'),
938 | performance: z.string().optional().describe('Optional: current performance metrics')
939 | },
940 | async ({ collection, query, performance }) => {
941 | log(`Prompt: Initializing [query-optimizer] for collection '${collection}' with query: ${query}.`)
942 | const stats = await getCollectionStats(collection)
943 | const indexes = await getCollectionIndexes(collection)
944 | return {
945 | description: 'MongoDB Query Optimization Advisor',
946 | messages: [
947 | {
948 | role: 'user',
949 | content: {
950 | type: 'text',
951 | text: `I have a slow MongoDB query on the '${collection}' collection and need help optimizing it.
952 |
953 | Query filter: ${query}
954 |
955 | ${performance ? `Current performance: ${performance}\n\n` : ''}
956 | Collection stats:
957 | ${formatStats(stats)}
958 |
959 | Current indexes:
960 | ${formatIndexes(indexes)}
961 |
962 | Please provide:
963 | 1. Analysis of why this query might be slow
964 | 2. Recommend index changes (additions or modifications)
965 | 3. Suggest query structure improvements
966 | 4. Explain how to verify performance improvements
967 | 5. Other optimization techniques I should consider`
968 | }
969 | }
970 | ]
971 | }
972 | }
973 | )
974 | }
975 |
976 | if (!isDisabled('prompts', 'security-audit')) {
977 | server.prompt(
978 | 'security-audit',
979 | 'Get MongoDB security recommendations',
980 | {},
981 | async () => {
982 | log('Prompt: Initializing [security-audit].')
983 | const serverStatus = await getServerStatus()
984 | const users = await getDatabaseUsers()
985 | return {
986 | description: 'MongoDB Security Audit',
987 | messages: [
988 | {
989 | role: 'user',
990 | content: {
991 | type: 'text',
992 | text: `Please help me perform a security audit on my MongoDB deployment.
993 |
994 | Server information:
995 | ${formatServerStatus(serverStatus)}
996 |
997 | User information:
998 | ${formatDatabaseUsers(users)}
999 |
1000 | Please provide:
1001 | 1. Potential security vulnerabilities in my current setup
1002 | 2. Recommendations for improving security
1003 | 3. Authentication and authorization best practices
1004 | 4. Network security considerations
1005 | 5. Data encryption options
1006 | 6. Audit logging recommendations
1007 | 7. Backup security considerations`
1008 | }
1009 | }
1010 | ]
1011 | }
1012 | }
1013 | )
1014 | }
1015 |
1016 | if (!isDisabled('prompts', 'backup-strategy')) {
1017 | server.prompt(
1018 | 'backup-strategy',
1019 | 'Get advice on MongoDB backup and recovery approaches',
1020 | {
1021 | databaseSize: z.string().optional().describe('Optional: database size information'),
1022 | uptime: z.string().optional().describe('Optional: uptime requirements (e.g. "99.9%")'),
1023 | rpo: z.string().optional().describe('Optional: recovery point objective'),
1024 | rto: z.string().optional().describe('Optional: recovery time objective')
1025 | },
1026 | ({ databaseSize, uptime, rpo, rto }) => {
1027 | log('Prompt: Initializing [backup-strategy].')
1028 | return {
1029 | description: 'MongoDB Backup & Recovery Strategy',
1030 | messages: [
1031 | {
1032 | role: 'user',
1033 | content: {
1034 | type: 'text',
1035 | text: `I need recommendations for a MongoDB backup and recovery strategy.
1036 |
1037 | ${databaseSize ? `Database size: ${databaseSize}\n` : ''}${uptime ? `Uptime requirement: ${uptime}\n` : ''}${rpo ? `Recovery Point Objective (RPO): ${rpo}\n` : ''}${rto ? `Recovery Time Objective (RTO): ${rto}\n` : ''}
1038 | Current database: ${currentDbName}
1039 |
1040 | Please provide:
1041 | 1. Recommended backup methods for my scenario
1042 | 2. Backup frequency and retention recommendations
1043 | 3. Storage considerations and best practices
1044 | 4. Restoration procedures and testing strategy
1045 | 5. Monitoring and validation approaches
1046 | 6. High availability considerations
1047 | 7. Tools and commands for implementing the strategy`
1048 | }
1049 | }
1050 | ]
1051 | }
1052 | }
1053 | )
1054 | }
1055 |
1056 | if (!isDisabled('prompts', 'migration-guide')) {
1057 | server.prompt(
1058 | 'migration-guide',
1059 | 'Generate MongoDB migration steps between versions',
1060 | {
1061 | sourceVersion: z.string().describe('Source MongoDB version'),
1062 | targetVersion: z.string().describe('Target MongoDB version'),
1063 | features: z.string().optional().describe('Optional: specific features you use')
1064 | },
1065 | ({ sourceVersion, targetVersion, features }) => {
1066 | log(`Prompt: Initializing [migration-guide] from ${sourceVersion} to ${targetVersion}.`)
1067 | return {
1068 | description: 'MongoDB Version Migration Guide',
1069 | messages: [
1070 | {
1071 | role: 'user',
1072 | content: {
1073 | type: 'text',
1074 | text: `I need to migrate my MongoDB deployment from version ${sourceVersion} to ${targetVersion}.
1075 |
1076 | ${features ? `Key features I'm using: ${features}\n\n` : ''}
1077 | Please provide:
1078 | 1. Step-by-step migration plan
1079 | 2. Pre-migration checks and preparations
1080 | 3. Breaking changes and deprecated features to be aware of
1081 | 4. New features or improvements I can leverage
1082 | 5. Common pitfalls and how to avoid them
1083 | 6. Performance considerations
1084 | 7. Rollback strategy in case of issues`
1085 | }
1086 | }
1087 | ]
1088 | }
1089 | }
1090 | )
1091 | }
1092 |
1093 | if (!isDisabled('prompts', 'sql-to-mongodb')) {
1094 | server.prompt(
1095 | 'sql-to-mongodb',
1096 | 'Convert SQL queries to MongoDB aggregation pipelines',
1097 | {
1098 | sqlQuery: z.string().min(1).describe('SQL query to convert'),
1099 | targetCollection: z.string().optional().describe('Target MongoDB collection name')
1100 | },
1101 | ({ sqlQuery, targetCollection }) => {
1102 | log(`Prompt: Initializing [sql-to-mongodb] for query: "${sqlQuery}".`)
1103 | return {
1104 | description: 'SQL to MongoDB Query Translator',
1105 | messages: [
1106 | {
1107 | role: 'user',
1108 | content: {
1109 | type: 'text',
1110 | text: `Please convert this SQL query to MongoDB syntax:\n\n\`\`\`sql\n${sqlQuery}\n\`\`\`\n\n${targetCollection ? `Target collection: ${targetCollection}` : ''}\n\nI need:
1111 | 1. The equivalent MongoDB query/aggregation pipeline with proper MongoDB operators
1112 | 2. Explanation of how each part of the SQL query maps to MongoDB
1113 | 3. How to execute this using MongoDB Lens tools
1114 | 4. Any important considerations or MongoDB-specific optimizations
1115 |
1116 | Please provide both the find() query format (if applicable) and the aggregation pipeline format.`
1117 | }
1118 | }
1119 | ]
1120 | }
1121 | }
1122 | )
1123 | }
1124 |
1125 | if (!isDisabled('prompts', 'database-health-check')) {
1126 | server.prompt(
1127 | 'database-health-check',
1128 | 'Comprehensive database health assessment',
1129 | {
1130 | includePerformance: z.string().default('true').describe('Include performance metrics'),
1131 | includeSchema: z.string().default('true').describe('Include schema analysis'),
1132 | includeSecurity: z.string().default('true').describe('Include security assessment')
1133 | },
1134 | async ({ includePerformance, includeSchema, includeSecurity }) => {
1135 | const includePerformanceBool = includePerformance.toLowerCase() === 'true'
1136 | const includeSchemaBool = includeSchema.toLowerCase() === 'true'
1137 | const includeSecurityBool = includeSecurity.toLowerCase() === 'true'
1138 |
1139 | log('Prompt: Initializing [database-health-check].')
1140 |
1141 | const dbStats = await getDatabaseStats()
1142 | const collections = await listCollections()
1143 |
1144 | let serverStatus = null
1145 | let indexes = {}
1146 | let schemaAnalysis = {}
1147 |
1148 | if (includePerformanceBool) {
1149 | serverStatus = await getServerStatus()
1150 | const collectionsToAnalyze = collections.slice(0, 5)
1151 | for (const coll of collectionsToAnalyze) {
1152 | indexes[coll.name] = await getCollectionIndexes(coll.name)
1153 | }
1154 | }
1155 |
1156 | if (includeSchemaBool) {
1157 | const collectionsToAnalyze = collections.slice(0, 3)
1158 | for (const coll of collectionsToAnalyze) {
1159 | try {
1160 | schemaAnalysis[coll.name] = await inferSchema(coll.name, 10)
1161 | } catch (schemaError) {
1162 | log(`Prompt: Error analyzing schema for collection '${coll.name}': ${schemaError.message}`)
1163 | schemaAnalysis[coll.name] = {
1164 | collectionName: coll.name,
1165 | error: schemaError.message,
1166 | isEmpty: schemaError.message.includes('empty'),
1167 | fields: {}
1168 | }
1169 | }
1170 | }
1171 | }
1172 |
1173 | let securityInfo = null
1174 | if (includeSecurityBool) {
1175 | try {
1176 | const users = await getDatabaseUsers()
1177 | securityInfo = {
1178 | users,
1179 | auth: serverStatus ? serverStatus.security : null
1180 | }
1181 | } catch (secError) {
1182 | log(`Prompt: Error getting security info: ${secError.message}`)
1183 | securityInfo = { error: secError.message }
1184 | }
1185 | }
1186 |
1187 | return {
1188 | description: `MongoDB Health Check: ${currentDbName}`,
1189 | messages: [
1190 | {
1191 | role: 'user',
1192 | content: {
1193 | type: 'text',
1194 | text: `Please perform a comprehensive health check on my MongoDB database "${currentDbName}" and provide recommendations for improvements.
1195 |
1196 | Database Statistics:
1197 | ${JSON.stringify(dbStats, null, 2)}
1198 |
1199 | Collections (${collections.length}):
1200 | ${collections.map(c => `- ${c.name}`).join('\n')}
1201 |
1202 | ${includePerformanceBool ? `\nPerformance Metrics:
1203 | ${JSON.stringify(serverStatus ? {
1204 | connections: serverStatus.connections,
1205 | opcounters: serverStatus.opcounters,
1206 | mem: serverStatus.mem
1207 | } : {}, null, 2)}
1208 |
1209 | Indexes:
1210 | ${Object.entries(indexes).map(([coll, idxs]) =>
1211 | `- ${coll}: ${idxs.length} indexes`
1212 | ).join('\n')}` : ''}
1213 |
1214 | ${includeSchemaBool ? `\nSchema Samples:
1215 | ${Object.keys(schemaAnalysis).join(', ')}` : ''}
1216 |
1217 | ${includeSecurityBool ? `\nSecurity Information:
1218 | - Users: ${securityInfo?.users?.users ? securityInfo.users.users.length : 'N/A'}
1219 | - Authentication: ${securityInfo?.auth?.authentication ?
1220 | JSON.stringify(securityInfo.auth.authentication.mechanisms || securityInfo.auth.authentication) : 'N/A'}` : ''}
1221 |
1222 | Please provide:
1223 | 1. Overall health assessment
1224 | 2. Urgent issues that need addressing
1225 | 3. Performance optimization recommendations
1226 | 4. Schema design suggestions and improvements
1227 | 5. Security best practices and concerns
1228 | 6. Monitoring and maintenance recommendations
1229 | 7. Specific MongoDB Lens tools to use for implementing your recommendations`
1230 | }
1231 | }
1232 | ]
1233 | }
1234 | }
1235 | )
1236 | }
1237 |
1238 | if (!isDisabled('prompts', 'multi-tenant-design')) {
1239 | server.prompt(
1240 | 'multi-tenant-design',
1241 | 'Design MongoDB multi-tenant database architecture',
1242 | {
1243 | tenantIsolation: z.enum(['database', 'collection', 'field']).describe('Level of tenant isolation required'),
1244 | estimatedTenants: z.string().describe('Estimated number of tenants'),
1245 | sharedFeatures: z.string().describe('Features/data that will be shared across tenants'),
1246 | tenantSpecificFeatures: z.string().describe('Features/data unique to each tenant'),
1247 | scalingPriorities: z.string().optional().describe('Primary scaling concerns (e.g., read-heavy, write-heavy)')
1248 | },
1249 | ({ tenantIsolation, estimatedTenants, sharedFeatures, tenantSpecificFeatures, scalingPriorities }) => {
1250 | const estimatedTenantsNum = parseInt(estimatedTenants, 10) || 1
1251 | log(`Prompt: Initializing [multi-tenant-design] with ${tenantIsolation} isolation level for ${estimatedTenantsNum} tenants.`)
1252 | return {
1253 | description: 'MongoDB Multi-Tenant Architecture Design',
1254 | messages: [
1255 | {
1256 | role: 'user',
1257 | content: {
1258 | type: 'text',
1259 | text: `I need to design a multi-tenant MongoDB architecture with the following requirements:
1260 |
1261 | - Tenant isolation level: ${tenantIsolation}
1262 | - Estimated number of tenants: ${estimatedTenants}
1263 | - Shared features/data: ${sharedFeatures}
1264 | - Tenant-specific features/data: ${tenantSpecificFeatures}
1265 | ${scalingPriorities ? `- Scaling priorities: ${scalingPriorities}` : ''}
1266 |
1267 | Please provide:
1268 | 1. Recommended multi-tenant architecture for MongoDB
1269 | 2. Data model with collection structures and relationships
1270 | 3. Schema examples (in JSON) for each collection
1271 | 4. Indexing strategy for optimal tenant isolation and performance
1272 | 5. Security considerations and access control patterns
1273 | 6. Scaling approach as tenant count and data volume grow
1274 | 7. Query patterns to efficiently retrieve tenant-specific data
1275 | 8. Specific MongoDB features to leverage for multi-tenancy
1276 | 9. Potential challenges and mitigation strategies
1277 |
1278 | For context, I'm using MongoDB version ${mongoClient.topology?.lastIsMaster?.version || 'recent'} and want to ensure my architecture follows best practices.`
1279 | }
1280 | }
1281 | ]
1282 | }
1283 | }
1284 | )
1285 | }
1286 |
1287 | if (!isDisabled('prompts', 'schema-versioning')) {
1288 | server.prompt(
1289 | 'schema-versioning',
1290 | 'Manage schema evolution in MongoDB applications',
1291 | {
1292 | collection: z.string().min(1).describe('Collection name to version'),
1293 | currentSchema: z.string().describe('Current schema structure (brief description)'),
1294 | plannedChanges: z.string().describe('Planned schema changes'),
1295 | migrationConstraints: z.string().optional().describe('Migration constraints (e.g., zero downtime)')
1296 | },
1297 | async ({ collection, currentSchema, plannedChanges, migrationConstraints }) => {
1298 | log(`Prompt: Initializing [schema-versioning] for collection '${collection}'…`)
1299 | const schema = await inferSchema(collection)
1300 | return {
1301 | description: 'MongoDB Schema Versioning Strategy',
1302 | messages: [
1303 | {
1304 | role: 'user',
1305 | content: {
1306 | type: 'text',
1307 | text: `I need to implement schema versioning/evolution for the '${collection}' collection in MongoDB. Please help me design a strategy.
1308 |
1309 | Current Schema Information:
1310 | ${formatSchema(schema)}
1311 |
1312 | Current Schema Description:
1313 | ${currentSchema}
1314 |
1315 | Planned Schema Changes:
1316 | ${plannedChanges}
1317 |
1318 | ${migrationConstraints ? `Migration Constraints: ${migrationConstraints}` : ''}
1319 |
1320 | Please provide:
1321 | 1. Recommended approach to schema versioning in MongoDB
1322 | 2. Step-by-step migration plan for these specific changes
1323 | 3. Code examples showing how to handle both old and new schema versions
1324 | 4. Schema validation rules to enforce the new schema
1325 | 5. Performance considerations during migration
1326 | 6. Rollback strategy if needed
1327 | 7. Testing approach to validate the migration
1328 | 8. MongoDB Lens tools and commands to use for the migration process
1329 |
1330 | I want to ensure backward compatibility during this transition.`
1331 | }
1332 | }
1333 | ]
1334 | }
1335 | }
1336 | )
1337 | }
1338 |
1339 | const totalRegisteredPrompts = Object.keys(server._registeredPrompts).length
1340 | log(`Total MCP prompts: ${totalRegisteredPrompts}`)
1341 | }
1342 |
1343 | const registerTools = (server) => {
1344 | if (isDisabled('tools', 'all')) {
1345 | log('All MCP tools disabled via configuration', true)
1346 | return
1347 | }
1348 |
1349 | if (!isDisabled('tools', 'connect-mongodb')) {
1350 | server.tool(
1351 | 'connect-mongodb',
1352 | 'Connect to a different MongoDB URI or alias',
1353 | {
1354 | uri: z.string().min(1).describe('MongoDB connection URI or alias to connect to'),
1355 | validateConnection: createBooleanSchema('Whether to validate the connection', 'true')
1356 | },
1357 | async ({ uri, validateConnection }) => {
1358 | return withErrorHandling(async () => {
1359 | try {
1360 | const processedUri = ensureValidMongoUri(uri)
1361 |
1362 | if (!processedUri.includes('://') && !processedUri.includes('@') && !mongoUriMap.has(processedUri.toLowerCase())) {
1363 | if (await isDatabaseName(processedUri)) {
1364 | return {
1365 | content: [{
1366 | type: 'text',
1367 | text: `It seems you're trying to switch to database "${processedUri}" rather than connect to a new MongoDB instance. Please use the "use-database" tool instead.`
1368 | }]
1369 | }
1370 | }
1371 | }
1372 |
1373 | const resolvedUri = resolveMongoUri(processedUri)
1374 | await changeConnection(resolvedUri, validateConnection === 'true')
1375 | return {
1376 | content: [{
1377 | type: 'text',
1378 | text: `Successfully connected to MongoDB${mongoUriAlias ? ` (${mongoUriAlias})` : ''} at URI: ${obfuscateMongoUri(resolvedUri)}`
1379 | }]
1380 | }
1381 | } catch (error) {
1382 | if (mongoUriCurrent && mongoUriCurrent !== uri) {
1383 | try {
1384 | await changeConnection(mongoUriCurrent, true)
1385 | } catch (reconnectError) {
1386 | log(`Failed to reconnect to current URI: ${reconnectError.message}`, true)
1387 | }
1388 | }
1389 | throw error
1390 | }
1391 | }, 'Error connecting to new MongoDB URI')
1392 | }
1393 | )
1394 | }
1395 |
1396 | if (!isDisabled('tools', 'connect-original')) {
1397 | server.tool(
1398 | 'connect-original',
1399 | 'Connect back to the original MongoDB URI used at startup',
1400 | {
1401 | validateConnection: createBooleanSchema('Whether to validate the connection', 'true')
1402 | },
1403 | async ({ validateConnection }) => {
1404 | return withErrorHandling(async () => {
1405 | if (!mongoUriOriginal) throw new Error('Original MongoDB URI not available')
1406 | await changeConnection(mongoUriOriginal, validateConnection === 'true')
1407 | return {
1408 | content: [{
1409 | type: 'text',
1410 | text: `Successfully connected back to original MongoDB URI: ${obfuscateMongoUri(mongoUriOriginal)}`
1411 | }]
1412 | }
1413 | }, 'Error connecting to original MongoDB URI')
1414 | }
1415 | )
1416 | }
1417 |
1418 | if (!isDisabled('tools', 'add-connection-alias')) {
1419 | server.tool(
1420 | 'add-connection-alias',
1421 | 'Add a new MongoDB connection alias',
1422 | {
1423 | alias: z.string().min(1).describe('Alias name for the connection'),
1424 | uri: z.string().min(1).describe('MongoDB connection URI')
1425 | },
1426 | async ({ alias, uri }) => {
1427 | return withErrorHandling(async () => {
1428 | log(`Tool: Adding connection alias '${alias}'…`)
1429 |
1430 | uri = ensureValidMongoUri(uri)
1431 | if (!uri.includes('://') && !uri.includes('@')) {
1432 | throw new Error(`Invalid MongoDB URI: ${uri}. URI must include protocol (mongodb://) or authentication (@)`)
1433 | }
1434 |
1435 | const normalizedAlias = alias.toLowerCase()
1436 |
1437 | if (mongoUriMap.has(normalizedAlias)) {
1438 | return {
1439 | content: [{
1440 | type: 'text',
1441 | text: `Connection alias '${alias}' already exists. Use 'connect-mongodb' with the URI directly or choose a different alias.`
1442 | }]
1443 | }
1444 | }
1445 |
1446 | mongoUriMap.set(normalizedAlias, uri)
1447 | log(`Tool: Added connection alias '${alias}' for URI: ${obfuscateMongoUri(uri)}`)
1448 |
1449 | return {
1450 | content: [{
1451 | type: 'text',
1452 | text: `Successfully added connection alias '${alias}' for MongoDB URI: ${obfuscateMongoUri(uri)}\n\nYou can now connect using: "Connect to ${alias}"`
1453 | }]
1454 | }
1455 | }, `Error adding connection alias '${alias}'`)
1456 | }
1457 | )
1458 | }
1459 |
1460 | if (!isDisabled('tools', 'list-connections')) {
1461 | server.tool(
1462 | 'list-connections',
1463 | 'List all configured MongoDB connection aliases',
1464 | async () => {
1465 | return withErrorHandling(async () => {
1466 | log('Tool: Listing configured MongoDB connections')
1467 |
1468 | if (mongoUriMap.size === 0) {
1469 | return {
1470 | content: [{
1471 | type: 'text',
1472 | text: 'No connection aliases configured.'
1473 | }]
1474 | }
1475 | }
1476 |
1477 | let result = 'Configured MongoDB connections:\n'
1478 | mongoUriMap.forEach((uri, alias) => {
1479 | const obfuscatedUri = obfuscateMongoUri(uri)
1480 | result += `- ${alias}: ${obfuscatedUri}${alias === mongoUriAlias ? ' (current)' : ''}\n`
1481 | })
1482 |
1483 | return {
1484 | content: [{
1485 | type: 'text',
1486 | text: result
1487 | }]
1488 | }
1489 | }, 'Error listing MongoDB connections')
1490 | }
1491 | )
1492 | }
1493 |
1494 | if (!isDisabled('tools', 'list-databases')) {
1495 | server.tool(
1496 | 'list-databases',
1497 | 'List all accessible MongoDB databases',
1498 | async () => {
1499 | return withErrorHandling(async () => {
1500 | log('Tool: Listing databases…')
1501 | const dbs = await listDatabases()
1502 | log(`Tool: Found ${dbs.length} databases.`)
1503 | return {
1504 | content: [{
1505 | type: 'text',
1506 | text: formatDatabasesList(dbs)
1507 | }]
1508 | }
1509 | }, 'Error listing databases')
1510 | }
1511 | )
1512 | }
1513 |
1514 | if (!isDisabled('tools', 'current-database')) {
1515 | server.tool(
1516 | 'current-database',
1517 | 'Get the name of the current database',
1518 | async () => {
1519 | return withErrorHandling(async () => {
1520 | log('Tool: Getting current database name…')
1521 | return {
1522 | content: [{
1523 | type: 'text',
1524 | text: `Current database: ${currentDbName}`
1525 | }]
1526 | }
1527 | }, 'Error getting current database')
1528 | }
1529 | )
1530 | }
1531 |
1532 | if (!isDisabled('tools', 'create-database')) {
1533 | server.tool(
1534 | 'create-database',
1535 | 'Create a new MongoDB database with option to switch',
1536 | {
1537 | name: z.string().min(1).describe('Database name to create'),
1538 | switch: createBooleanSchema('Whether to switch to the new database after creation', 'false'),
1539 | validateName: createBooleanSchema('Whether to validate database name', 'true')
1540 | },
1541 | async ({ name, switch: shouldSwitch, validateName }) => {
1542 | return withErrorHandling(async () => {
1543 | log(`Tool: Creating database '${name}'${shouldSwitch === 'true' ? ' and switching to it' : ''}…`)
1544 | const db = await createDatabase(name, validateName)
1545 |
1546 | if (shouldSwitch === 'true') {
1547 | currentDbName = name
1548 | currentDb = db
1549 | log(`Tool: Switched to database '${name}'.`)
1550 | return {
1551 | content: [{
1552 | type: 'text',
1553 | text: `Database '${name}' created successfully and connected.`
1554 | }]
1555 | }
1556 | }
1557 |
1558 | log(`Tool: Database '${name}' created successfully. Current database is still '${currentDbName}'.`)
1559 | return {
1560 | content: [{
1561 | type: 'text',
1562 | text: `Database '${name}' created successfully. Current database is still '${currentDbName}'.`
1563 | }]
1564 | }
1565 | }, `Error creating database '${name}'${shouldSwitch === 'true' ? ' and switching to it' : ''}`)
1566 | }
1567 | )
1568 | }
1569 |
1570 | if (!isDisabled('tools', 'use-database')) {
1571 | server.tool(
1572 | 'use-database',
1573 | 'Switch to a specific database',
1574 | {
1575 | database: z.string().min(1).describe('Database name to use')
1576 | },
1577 | async ({ database }) => {
1578 | return withErrorHandling(async () => {
1579 | log(`Tool: Switching to database '${database}'…`)
1580 | await switchDatabase(database)
1581 | log(`Tool: Successfully switched to database '${database}'.`)
1582 | return {
1583 | content: [{
1584 | type: 'text',
1585 | text: `Switched to database: ${database}`
1586 | }]
1587 | }
1588 | }, `Error switching to database '${database}'`)
1589 | }
1590 | )
1591 | }
1592 |
1593 | if (!isDisabled('tools', 'drop-database')) {
1594 | server.tool(
1595 | 'drop-database',
1596 | 'Drop a database (requires confirmation)',
1597 | {
1598 | name: z.string().min(1).describe('Database name to drop'),
1599 | token: z.string().optional().describe('Confirmation token from previous request')
1600 | },
1601 | async ({ name, token }) => {
1602 | return withErrorHandling(async () => {
1603 | log(`Tool: Processing drop database request for '${name}'…`)
1604 |
1605 | if (config.disableDestructiveOperationTokens) {
1606 | const result = await dropDatabase(name)
1607 | return {
1608 | content: [{
1609 | type: 'text',
1610 | text: result.message
1611 | }]
1612 | }
1613 | }
1614 |
1615 | if (token) {
1616 | if (!validateDropDatabaseToken(name, token)) {
1617 | throw new Error(`Invalid or expired confirmation token for dropping '${name}'. Please try again without a token to generate a new confirmation code.`)
1618 | }
1619 | const result = await dropDatabase(name)
1620 | return {
1621 | content: [{
1622 | type: 'text',
1623 | text: result.message
1624 | }]
1625 | }
1626 | }
1627 |
1628 | const dbs = await listDatabases()
1629 | const dbExists = dbs.some(db => db.name === name)
1630 | if (!dbExists) throw new Error(`Database '${name}' does not exist`)
1631 |
1632 | const newToken = storeDropDatabaseToken(name)
1633 | return {
1634 | content: [{
1635 | type: 'text',
1636 | text: `⚠️ DESTRUCTIVE OPERATION WARNING ⚠️\n\nYou've requested to drop the database '${name}'.\n\nThis operation is irreversible and will permanently delete all collections and data in this database.\n\nTo confirm, you must type the 4-digit confirmation code EXACTLY as shown below:\n\nConfirmation code: ${newToken}\n\nThis code will expire in 5 minutes for security purposes.\n\n${importantNoticeToAI}`
1637 | }]
1638 | }
1639 | }, `Error processing database drop for '${name}'`)
1640 | }
1641 | )
1642 | }
1643 |
1644 | if (!isDisabled('tools', 'create-user')) {
1645 | server.tool(
1646 | 'create-user',
1647 | 'Create a new database user',
1648 | {
1649 | username: z.string().min(1).describe('Username'),
1650 | password: z.string().min(1).describe('Password'),
1651 | roles: z.string().describe('Roles as JSON array, e.g. [{"role": "readWrite", "db": "mydb"}]')
1652 | },
1653 | async ({ username, password, roles }) => {
1654 | return withErrorHandling(async () => {
1655 | log(`Tool: Creating user '${username}'…`)
1656 | const parsedRoles = JSON.parse(roles)
1657 | await createUser(username, password, parsedRoles)
1658 | log(`Tool: User created successfully.`)
1659 | return {
1660 | content: [{
1661 | type: 'text',
1662 | text: `User '${username}' created with roles: ${JSON.stringify(parsedRoles)}`
1663 | }]
1664 | }
1665 | }, `Error creating user '${username}'`)
1666 | }
1667 | )
1668 | }
1669 |
1670 | if (!isDisabled('tools', 'drop-user')) {
1671 | server.tool(
1672 | 'drop-user',
1673 | 'Drop an existing database user',
1674 | {
1675 | username: z.string().min(1).describe('Username to drop'),
1676 | token: z.string().optional().describe('Confirmation token from previous request')
1677 | },
1678 | async ({ username, token }) => {
1679 | return withErrorHandling(async () => {
1680 | log(`Tool: Processing drop user request for '${username}'…`)
1681 |
1682 | if (config.disableDestructiveOperationTokens) {
1683 | await dropUser(username)
1684 | return {
1685 | content: [{
1686 | type: 'text',
1687 | text: `User '${username}' dropped successfully.`
1688 | }]
1689 | }
1690 | }
1691 |
1692 | if (token) {
1693 | if (!validateDropUserToken(username, token)) {
1694 | throw new Error(`Invalid or expired confirmation token. Please try again without a token to generate a new confirmation code.`)
1695 | }
1696 | await dropUser(username)
1697 | return {
1698 | content: [{
1699 | type: 'text',
1700 | text: `User '${username}' dropped successfully.`
1701 | }]
1702 | }
1703 | }
1704 |
1705 | const newToken = storeDropUserToken(username)
1706 | return {
1707 | content: [{
1708 | type: 'text',
1709 | text: `⚠️ SECURITY OPERATION WARNING ⚠️\n\nYou've requested to drop the user '${username}'.\n\nThis operation will remove all access permissions for this user and is irreversible.\n\nTo confirm, type the 4-digit confirmation code EXACTLY as shown below:\n\nConfirmation code: ${newToken}\n\nThis code will expire in 5 minutes for security purposes.\n\n${importantNoticeToAI}`
1710 | }]
1711 | }
1712 | }, `Error processing user drop for '${username}'`)
1713 | }
1714 | )
1715 | }
1716 |
1717 | if (!isDisabled('tools', 'list-collections')) {
1718 | server.tool(
1719 | 'list-collections',
1720 | 'List collections in the current database',
1721 | async () => {
1722 | return withErrorHandling(async () => {
1723 | log(`Tool: Listing collections in database '${currentDbName}'…`)
1724 | const collections = await listCollections()
1725 | log(`Tool: Found ${collections.length} collections in database '${currentDbName}'.`)
1726 | return {
1727 | content: [{
1728 | type: 'text',
1729 | text: formatCollectionsList(collections)
1730 | }]
1731 | }
1732 | }, 'Error listing collections')
1733 | }
1734 | )
1735 | }
1736 |
1737 | if (!isDisabled('tools', 'create-collection')) {
1738 | server.tool(
1739 | 'create-collection',
1740 | 'Create a new collection with options',
1741 | {
1742 | name: z.string().min(1).describe('Collection name'),
1743 | options: z.string().default('{}').describe('Collection options as JSON string (capped, size, etc.)')
1744 | },
1745 | async ({ name, options }) => {
1746 | return withErrorHandling(async () => {
1747 | log(`Tool: Creating collection '${name}'…`)
1748 | log(`Tool: Using options: ${options}`)
1749 | const parsedOptions = options ? JSON.parse(options) : {}
1750 | const result = await createCollection(name, parsedOptions)
1751 | log(`Tool: Collection created successfully.`)
1752 | return {
1753 | content: [{
1754 | type: 'text',
1755 | text: `Collection '${name}' created successfully.`
1756 | }]
1757 | }
1758 | }, `Error creating collection '${name}'`)
1759 | }
1760 | )
1761 | }
1762 |
1763 | if (!isDisabled('tools', 'drop-collection')) {
1764 | server.tool(
1765 | 'drop-collection',
1766 | 'Drop a collection (requires confirmation)',
1767 | {
1768 | name: z.string().min(1).describe('Collection name to drop'),
1769 | token: z.string().optional().describe('Confirmation token from previous request')
1770 | },
1771 | async ({ name, token }) => {
1772 | return withErrorHandling(async () => {
1773 | log(`Tool: Processing drop collection request for '${name}'…`)
1774 |
1775 | if (config.disableDestructiveOperationTokens) {
1776 | await dropCollection(name)
1777 | return {
1778 | content: [{
1779 | type: 'text',
1780 | text: `Collection '${name}' has been permanently deleted.`
1781 | }]
1782 | }
1783 | }
1784 |
1785 | if (token) {
1786 | if (!validateDropCollectionToken(name, token)) {
1787 | throw new Error(`Invalid or expired confirmation token for dropping '${name}'. Please try again without a token to generate a new confirmation code.`)
1788 | }
1789 | await dropCollection(name)
1790 | return {
1791 | content: [{
1792 | type: 'text',
1793 | text: `Collection '${name}' has been permanently deleted.`
1794 | }]
1795 | }
1796 | }
1797 |
1798 | await throwIfCollectionNotExists(name)
1799 | const newToken = storeDropCollectionToken(name)
1800 | return {
1801 | content: [{
1802 | type: 'text',
1803 | text: `⚠️ DESTRUCTIVE OPERATION WARNING ⚠️\n\nYou've requested to drop the collection '${name}'.\n\nThis operation is irreversible and will permanently delete all data in this collection.\n\nTo confirm, you must type the 4-digit confirmation code EXACTLY as shown below:\n\nConfirmation code: ${newToken}\n\nThis code will expire in 5 minutes for security purposes.\n\n${importantNoticeToAI}`
1804 | }]
1805 | }
1806 | }, `Error processing collection drop for '${name}'`)
1807 | }
1808 | )
1809 | }
1810 |
1811 | if (!isDisabled('tools', 'rename-collection')) {
1812 | server.tool(
1813 | 'rename-collection',
1814 | 'Rename an existing collection',
1815 | {
1816 | oldName: z.string().min(1).describe('Current collection name'),
1817 | newName: z.string().min(1).describe('New collection name'),
1818 | dropTarget: createBooleanSchema('Whether to drop target collection if it exists', 'false'),
1819 | token: z.string().optional().describe('Confirmation token from previous request')
1820 | },
1821 | async ({ oldName, newName, dropTarget, token }) => {
1822 | return withErrorHandling(async () => {
1823 | log(`Tool: Processing rename collection from '${oldName}' to '${newName}'…`)
1824 | await throwIfCollectionNotExists(oldName)
1825 |
1826 | const collections = await listCollections()
1827 | const targetExists = collections.some(c => c.name === newName)
1828 |
1829 | if (!targetExists || dropTarget !== 'true' || config.disableDestructiveOperationTokens) {
1830 | const result = await renameCollection(oldName, newName, dropTarget === 'true')
1831 | return {
1832 | content: [{
1833 | type: 'text',
1834 | text: `Collection '${oldName}' renamed to '${newName}' successfully.`
1835 | }]
1836 | }
1837 | }
1838 |
1839 | if (token) {
1840 | if (!validateRenameCollectionToken(oldName, newName, dropTarget, token)) {
1841 | throw new Error(`Invalid or expired confirmation token. Please try again without a token to generate a new confirmation code.`)
1842 | }
1843 | const result = await renameCollection(oldName, newName, true)
1844 | return {
1845 | content: [{
1846 | type: 'text',
1847 | text: `Collection '${oldName}' renamed to '${newName}' successfully.`
1848 | }]
1849 | }
1850 | }
1851 |
1852 | const newToken = storeRenameCollectionToken(oldName, newName, dropTarget)
1853 | return {
1854 | content: [{
1855 | type: 'text',
1856 | text: `⚠️ DESTRUCTIVE OPERATION WARNING ⚠️\n\nYou've requested to rename collection '${oldName}' to '${newName}' and drop the existing target collection.\n\nDropping a collection is irreversible. To confirm, type the 4-digit confirmation code EXACTLY as shown below:\n\nConfirmation code: ${newToken}\n\nThis code will expire in 5 minutes for security purposes.\n\n${importantNoticeToAI}`
1857 | }]
1858 | }
1859 | }, `Error processing rename for collection '${oldName}'`)
1860 | }
1861 | )
1862 | }
1863 |
1864 | if (!isDisabled('tools', 'validate-collection')) {
1865 | server.tool(
1866 | 'validate-collection',
1867 | 'Run validation on a collection to check for inconsistencies',
1868 | {
1869 | collection: z.string().min(1).describe('Collection name'),
1870 | full: createBooleanSchema('Perform full validation (slower but more thorough)', 'false')
1871 | },
1872 | async ({ collection, full }) => {
1873 | return withErrorHandling(async () => {
1874 | log(`Tool: Validating collection '${collection}'…`)
1875 | log(`Tool: Full validation: ${full}`)
1876 | const results = await validateCollection(collection, full)
1877 | log(`Tool: Validation complete.`)
1878 | return {
1879 | content: [{
1880 | type: 'text',
1881 | text: formatValidationResults(results)
1882 | }]
1883 | }
1884 | }, `Error validating collection '${collection}'`)
1885 | }
1886 | )
1887 | }
1888 |
1889 | if (!isDisabled('tools', 'distinct-values')) {
1890 | server.tool(
1891 | 'distinct-values',
1892 | 'Get unique values for a field',
1893 | {
1894 | collection: z.string().min(1).describe('Collection name'),
1895 | field: z.string().min(1).describe('Field name to get distinct values for'),
1896 | filter: z.string().default('{}').describe('Optional filter as JSON string')
1897 | },
1898 | async ({ collection, field, filter }) => {
1899 | return withErrorHandling(async () => {
1900 | log(`Tool: Getting distinct values for field '${field}' in collection '${collection}'…`)
1901 | log(`Tool: Using filter: ${filter}`)
1902 | const parsedFilter = filter ? parseJsonString(filter) : {}
1903 | const values = await getDistinctValues(collection, field, parsedFilter)
1904 | log(`Tool: Found ${values.length} distinct values.`)
1905 | return {
1906 | content: [{
1907 | type: 'text',
1908 | text: formatDistinctValues(field, values)
1909 | }]
1910 | }
1911 | }, `Error getting distinct values for field '${field}' in collection '${collection}'`)
1912 | }
1913 | )
1914 | }
1915 |
1916 | if (!isDisabled('tools', 'find-documents')) {
1917 | server.tool(
1918 | 'find-documents',
1919 | 'Run queries with filters and projections',
1920 | {
1921 | collection: z.string().min(1).describe('Collection name'),
1922 | filter: z.string().default('{}').describe('MongoDB query filter (JSON string)'),
1923 | projection: z.string().optional().describe('Fields to include/exclude (JSON string)'),
1924 | limit: z.number().int().min(1).default(10).describe('Maximum number of documents to return'),
1925 | skip: z.number().int().min(0).default(0).describe('Number of documents to skip'),
1926 | sort: z.string().optional().describe('Sort specification (JSON string)')
1927 | },
1928 | async ({ collection, filter, projection, limit, skip, sort }) => {
1929 | return withErrorHandling(async () => {
1930 | log(`Tool: Finding documents in collection '${collection}'…`)
1931 | log(`Tool: Using filter: ${filter}`)
1932 | if (projection) log(`Tool: Using projection: ${projection}`)
1933 | if (sort) log(`Tool: Using sort: ${sort}`)
1934 | log(`Tool: Using limit: ${limit}, skip: ${skip}`)
1935 | const parsedFilter = filter ? parseJsonString(filter) : {}
1936 | const parsedProjection = projection ? parseJsonString(projection) : null
1937 | const parsedSort = sort ? parseJsonString(sort) : null
1938 | const documents = await findDocuments(collection, parsedFilter, parsedProjection, limit, skip, parsedSort)
1939 | log(`Tool: Found ${documents.length} documents in collection '${collection}'.`)
1940 | return {
1941 | content: [{
1942 | type: 'text',
1943 | text: formatDocuments(documents, limit)
1944 | }]
1945 | }
1946 | }, `Error finding documents in collection '${collection}'`)
1947 | }
1948 | )
1949 | }
1950 |
1951 | if (!isDisabled('tools', 'count-documents')) {
1952 | server.tool(
1953 | 'count-documents',
1954 | 'Count documents with optional filter',
1955 | {
1956 | collection: z.string().min(1).describe('Collection name'),
1957 | filter: z.string().default('{}').describe('MongoDB query filter (JSON string)')
1958 | },
1959 | async ({ collection, filter }) => {
1960 | return withErrorHandling(async () => {
1961 | log(`Tool: Counting documents in collection '${collection}'…`)
1962 | log(`Tool: Using filter: ${filter}`)
1963 | const parsedFilter = filter ? parseJsonString(filter) : {}
1964 | const count = await countDocuments(collection, parsedFilter)
1965 | log(`Tool: Count result: ${count} documents.`)
1966 | return {
1967 | content: [{
1968 | type: 'text',
1969 | text: `Count: ${count} document(s)`
1970 | }]
1971 | }
1972 | }, `Error counting documents in collection '${collection}'`)
1973 | }
1974 | )
1975 | }
1976 |
1977 | if (!isDisabled('tools', 'insert-document')) {
1978 | server.tool(
1979 | 'insert-document',
1980 | 'Insert one or multiple documents into a collection',
1981 | {
1982 | collection: z.string().min(1).describe('Collection name'),
1983 | document: z.string().describe('Document as JSON string or array of documents'),
1984 | options: z.string().optional().describe('Options as JSON string (including "ordered" for multiple documents)')
1985 | },
1986 | async ({ collection, document, options }) => {
1987 | return withErrorHandling(async () => {
1988 | log(`Tool: Inserting document(s) into collection '${collection}'…`)
1989 | if (!document) throw new Error('Document is required for insert operation')
1990 |
1991 | const parsedDocument = parseJsonString(document)
1992 | const parsedOptions = options ? parseJsonString(options) : {}
1993 | const result = await insertDocument(collection, parsedDocument, parsedOptions)
1994 |
1995 | const cacheKey = `${currentDbName}.${collection}`
1996 | memoryCache.schemas.delete(cacheKey)
1997 | memoryCache.fields.delete(cacheKey)
1998 | memoryCache.stats.delete(cacheKey)
1999 |
2000 | if (Array.isArray(parsedDocument)) {
2001 | log(`Tool: Successfully inserted ${result.insertedCount} documents.`)
2002 | return {
2003 | content: [{
2004 | type: 'text',
2005 | text: `Successfully inserted ${result.insertedCount} documents.\n\nInserted IDs: ${
2006 | Object.values(result.insertedIds || {})
2007 | .map(id => id.toString())
2008 | .join(', ')
2009 | }`
2010 | }]
2011 | }
2012 | }
2013 |
2014 | log(`Tool: Document inserted successfully.`)
2015 | return {
2016 | content: [{
2017 | type: 'text',
2018 | text: formatInsertResult(result)
2019 | }]
2020 | }
2021 | }, `Error inserting document(s) into collection '${collection}'`)
2022 | }
2023 | )
2024 | }
2025 |
2026 | if (!isDisabled('tools', 'update-document')) {
2027 | server.tool(
2028 | 'update-document',
2029 | 'Update specific documents in a collection',
2030 | {
2031 | collection: z.string().min(1).describe('Collection name'),
2032 | filter: z.string().describe('Filter as JSON string'),
2033 | update: z.string().describe('Update operations as JSON string'),
2034 | options: z.string().optional().describe('Options as JSON string')
2035 | },
2036 | async ({ collection, filter, update, options }) => {
2037 | return withErrorHandling(async () => {
2038 | log(`Tool: Updating documents in collection '${collection}'…`)
2039 |
2040 | if (!filter) throw new Error('Filter is required for update operation')
2041 | if (!update) throw new Error('Update is required for update operation')
2042 |
2043 | const parsedFilter = parseJsonString(filter)
2044 | const parsedUpdate = parseJsonString(update)
2045 | const parsedOptions = options ? parseJsonString(options) : {}
2046 | const result = await updateDocument(collection, parsedFilter, parsedUpdate, parsedOptions)
2047 |
2048 | const cacheKey = `${currentDbName}.${collection}`
2049 | memoryCache.schemas.delete(cacheKey)
2050 | memoryCache.fields.delete(cacheKey)
2051 | memoryCache.stats.delete(cacheKey)
2052 |
2053 | log(`Tool: Document(s) updated successfully.`)
2054 | return {
2055 | content: [{
2056 | type: 'text',
2057 | text: formatUpdateResult(result)
2058 | }]
2059 | }
2060 | }, `Error updating documents in collection '${collection}'`)
2061 | }
2062 | )
2063 | }
2064 |
2065 | if (!isDisabled('tools', 'delete-document')) {
2066 | server.tool(
2067 | 'delete-document',
2068 | 'Delete document(s) (requires confirmation)',
2069 | {
2070 | collection: z.string().min(1).describe('Collection name'),
2071 | filter: z.string().min(1).describe('Filter as JSON string'),
2072 | many: createBooleanSchema('Delete multiple documents if true', 'false'),
2073 | token: z.string().optional().describe('Confirmation token from previous request')
2074 | },
2075 | async ({ collection, filter, many, token }) => {
2076 | return withErrorHandling(async () => {
2077 | log(`Tool: Processing delete document request for collection '${collection}'…`)
2078 | const parsedFilter = parseJsonString(filter)
2079 |
2080 | if (config.disableDestructiveOperationTokens) {
2081 | const options = { many: many === 'true' }
2082 | const result = await deleteDocument(collection, parsedFilter, options)
2083 |
2084 | const cacheKey = `${currentDbName}.${collection}`
2085 | memoryCache.schemas.delete(cacheKey)
2086 | memoryCache.fields.delete(cacheKey)
2087 | memoryCache.stats.delete(cacheKey)
2088 |
2089 | return {
2090 | content: [{
2091 | type: 'text',
2092 | text: `Successfully deleted ${result.deletedCount} document(s) from collection '${collection}'.`
2093 | }]
2094 | }
2095 | }
2096 |
2097 | if (token) {
2098 | if (!validateDeleteDocumentToken(collection, parsedFilter, token)) {
2099 | throw new Error(`Invalid or expired confirmation token. Please try again without a token to generate a new confirmation code.`)
2100 | }
2101 | const options = { many: many === 'true' }
2102 | const result = await deleteDocument(collection, parsedFilter, options)
2103 | return {
2104 | content: [{
2105 | type: 'text',
2106 | text: `Successfully deleted ${result.deletedCount} document(s) from collection '${collection}'.`
2107 | }]
2108 | }
2109 | }
2110 |
2111 | await throwIfCollectionNotExists(collection)
2112 | const count = await countDocuments(collection, parsedFilter)
2113 |
2114 | const newToken = storeDeleteDocumentToken(collection, parsedFilter)
2115 | return {
2116 | content: [{
2117 | type: 'text',
2118 | text: `⚠️ DESTRUCTIVE OPERATION WARNING ⚠️\n\nYou've requested to delete ${many === 'true' ? 'all' : 'one'} document(s) from collection '${collection}' matching:\n${filter}\n\nThis matches approximately ${count} document(s).\n\nThis operation is irreversible. To confirm, type the 4-digit confirmation code EXACTLY as shown below:\n\nConfirmation code: ${newToken}\n\nThis code will expire in 5 minutes for security purposes.\n\n${importantNoticeToAI}`
2119 | }]
2120 | }
2121 | }, `Error processing document delete for collection '${collection}'`)
2122 | }
2123 | )
2124 | }
2125 |
2126 | if (!isDisabled('tools', 'aggregate-data')) {
2127 | server.tool(
2128 | 'aggregate-data',
2129 | 'Run aggregation pipelines',
2130 | {
2131 | collection: z.string().min(1).describe('Collection name'),
2132 | pipeline: z.string().describe('Aggregation pipeline as JSON string array'),
2133 | limit: z.number().int().min(1).default(1000).describe('Maximum number of results to return')
2134 | },
2135 | async ({ collection, pipeline, limit }) => {
2136 | return withErrorHandling(async () => {
2137 | log(`Tool: Running aggregation on collection '${collection}'…`)
2138 | log(`Tool: Using pipeline: ${pipeline}`)
2139 | log(`Tool: Limit: ${limit}`)
2140 | const parsedPipeline = parseJsonString(pipeline)
2141 | const processedPipeline = processAggregationPipeline(parsedPipeline)
2142 | const results = await aggregateData(collection, processedPipeline)
2143 | log(`Tool: Aggregation returned ${results.length} results.`)
2144 | return {
2145 | content: [{
2146 | type: 'text',
2147 | text: formatDocuments(results, 100)
2148 | }]
2149 | }
2150 | }, `Error running aggregation on collection '${collection}'`)
2151 | }
2152 | )
2153 | }
2154 |
2155 | if (!isDisabled('tools', 'create-index')) {
2156 | server.tool(
2157 | 'create-index',
2158 | 'Create new index on collection',
2159 | {
2160 | collection: z.string().min(1).describe('Collection name'),
2161 | keys: z.string().describe('Index keys as JSON object'),
2162 | options: z.string().optional().describe('Index options as JSON object')
2163 | },
2164 | async ({ collection, keys, options }) => {
2165 | return withErrorHandling(async () => {
2166 | log(`Tool: Creating index on collection '${collection}'…`)
2167 | log(`Tool: Index keys: ${keys}`)
2168 | if (options) log(`Tool: Index options: ${options}`)
2169 | const parsedKeys = parseJsonString(keys)
2170 | const parsedOptions = options ? parseJsonString(options) : {}
2171 | const result = await createIndex(collection, parsedKeys, parsedOptions)
2172 | log(`Tool: Index created successfully: ${result}`)
2173 | return {
2174 | content: [{
2175 | type: 'text',
2176 | text: `Index created: ${result}`
2177 | }]
2178 | }
2179 | }, `Error creating index on collection '${collection}'`)
2180 | }
2181 | )
2182 | }
2183 |
2184 | if (!isDisabled('tools', 'drop-index')) {
2185 | server.tool(
2186 | 'drop-index',
2187 | 'Drop an existing index from a collection',
2188 | {
2189 | collection: z.string().min(1).describe('Collection name'),
2190 | indexName: z.string().min(1).describe('Name of the index to drop'),
2191 | token: z.string().optional().describe('Confirmation token from previous request')
2192 | },
2193 | async ({ collection, indexName, token }) => {
2194 | return withErrorHandling(async () => {
2195 | log(`Tool: Processing drop index request for '${indexName}' on collection '${collection}'…`)
2196 |
2197 | if (config.disableDestructiveOperationTokens) {
2198 | await dropIndex(collection, indexName)
2199 | return {
2200 | content: [{
2201 | type: 'text',
2202 | text: `Index '${indexName}' dropped from collection '${collection}' successfully.`
2203 | }]
2204 | }
2205 | }
2206 |
2207 | if (token) {
2208 | if (!validateDropIndexToken(collection, indexName, token)) {
2209 | throw new Error(`Invalid or expired confirmation token. Please try again without a token to generate a new confirmation code.`)
2210 | }
2211 | await dropIndex(collection, indexName)
2212 | return {
2213 | content: [{
2214 | type: 'text',
2215 | text: `Index '${indexName}' dropped from collection '${collection}' successfully.`
2216 | }]
2217 | }
2218 | }
2219 |
2220 | await throwIfCollectionNotExists(collection)
2221 | const indexes = await getCollectionIndexes(collection)
2222 | const indexExists = indexes.some(idx => idx.name === indexName)
2223 |
2224 | if (!indexExists) {
2225 | throw new Error(`Index '${indexName}' does not exist on collection '${collection}'`)
2226 | }
2227 |
2228 | const newToken = storeDropIndexToken(collection, indexName)
2229 | return {
2230 | content: [{
2231 | type: 'text',
2232 | text: `⚠️ PERFORMANCE IMPACT WARNING ⚠️\n\nYou've requested to drop the index '${indexName}' from collection '${collection}'.\n\nDropping this index may impact query performance. To confirm, type the 4-digit confirmation code EXACTLY as shown below:\n\nConfirmation code: ${newToken}\n\nThis code will expire in 5 minutes for security purposes.\n\n${importantNoticeToAI}`
2233 | }]
2234 | }
2235 | }, `Error processing index drop for '${indexName}' on collection '${collection}'`)
2236 | }
2237 | )
2238 | }
2239 |
2240 | if (!isDisabled('tools', 'get-stats')) {
2241 | server.tool(
2242 | 'get-stats',
2243 | 'Get database or collection statistics',
2244 | {
2245 | target: z.enum(['database', 'collection']).describe('Target type'),
2246 | name: z.string().optional().describe('Collection name (for collection stats)')
2247 | },
2248 | async ({ target, name }) => {
2249 | return withErrorHandling(async () => {
2250 | let stats
2251 | if (target === 'database') {
2252 | log(`Tool: Getting statistics for database '${currentDbName}'…`)
2253 | stats = await getDatabaseStats()
2254 | log(`Tool: Retrieved database statistics.`)
2255 | } else if (target === 'collection') {
2256 | if (!name) throw new Error('Collection name is required for collection stats')
2257 | log(`Tool: Getting statistics for collection '${name}'…`)
2258 | stats = await getCollectionStats(name)
2259 | log(`Tool: Retrieved collection statistics.`)
2260 | }
2261 | return {
2262 | content: [{
2263 | type: 'text',
2264 | text: formatStats(stats)
2265 | }]
2266 | }
2267 | }, `Error getting ${target} statistics${name ? ` for '${name}'` : ''}`)
2268 | }
2269 | )
2270 | }
2271 |
2272 | if (!isDisabled('tools', 'analyze-schema')) {
2273 | server.tool(
2274 | 'analyze-schema',
2275 | 'Automatically infer schema from collection',
2276 | {
2277 | collection: z.string().min(1).describe('Collection name'),
2278 | sampleSize: z.number().int().min(1).default(100).describe('Number of documents to sample')
2279 | },
2280 | async ({ collection, sampleSize }) => {
2281 | return withErrorHandling(async () => {
2282 | log(`Tool: Analyzing schema for collection '${collection}' with sample size ${sampleSize}…`)
2283 | const schema = await inferSchema(collection, sampleSize)
2284 | log(`Tool: Schema analysis complete for '${collection}', found ${Object.keys(schema.fields).length} fields.`)
2285 | return {
2286 | content: [{
2287 | type: 'text',
2288 | text: formatSchema(schema)
2289 | }]
2290 | }
2291 | }, `Error inferring schema for collection '${collection}'`)
2292 | }
2293 | )
2294 | }
2295 |
2296 | if (!isDisabled('tools', 'generate-schema-validator')) {
2297 | server.tool(
2298 | 'generate-schema-validator',
2299 | 'Generate a JSON Schema validator for a collection',
2300 | {
2301 | collection: z.string().min(1).describe('Collection name'),
2302 | strictness: z.enum(['strict', 'moderate', 'relaxed']).default('moderate').describe('Validation strictness level')
2303 | },
2304 | async ({ collection, strictness }) => {
2305 | return withErrorHandling(async () => {
2306 | log(`Tool: Generating schema validator for '${collection}' with ${strictness} strictness`)
2307 | const schema = await inferSchema(collection, 200)
2308 | const validator = generateJsonSchemaValidator(schema, strictness)
2309 | const result = `# MongoDB JSON Schema Validator for '${collection}'
2310 |
2311 | ## Schema Validator
2312 | \`\`\`json
2313 | ${JSON.stringify(validator, null, 2)}
2314 | \`\`\`
2315 |
2316 | ## How to Apply This Validator
2317 |
2318 | ### MongoDB Shell Command
2319 | \`\`\`javascript
2320 | db.runCommand({
2321 | collMod: "${collection}",
2322 | validator: ${JSON.stringify(validator, null, 2)},
2323 | validationLevel: "${strictness === 'relaxed' ? 'moderate' : strictness}",
2324 | validationAction: "${strictness === 'strict' ? 'error' : 'warn'}"
2325 | })
2326 | \`\`\`
2327 |
2328 | ### Using update-document Tool in MongoDB Lens
2329 | \`\`\`
2330 | update-document {
2331 | "collection": "system.command",
2332 | "filter": { "collMod": "${collection}" },
2333 | "update": {
2334 | "$set": {
2335 | "collMod": "${collection}",
2336 | "validator": ${JSON.stringify(validator)},
2337 | "validationLevel": "${strictness === 'relaxed' ? 'moderate' : strictness}",
2338 | "validationAction": "${strictness === 'strict' ? 'error' : 'warn'}"
2339 | }
2340 | },
2341 | "options": { "upsert": true }
2342 | }
2343 | \`\`\`
2344 |
2345 | This schema validator was generated based on ${schema.sampleSize} sample documents with ${Object.keys(schema.fields).length} fields.
2346 | `
2347 | return {
2348 | content: [{
2349 | type: 'text',
2350 | text: result
2351 | }]
2352 | }
2353 | }, `Error generating schema validator for '${collection}'`)
2354 | }
2355 | )
2356 | }
2357 |
2358 | if (!isDisabled('tools', 'compare-schemas')) {
2359 | server.tool(
2360 | 'compare-schemas',
2361 | 'Compare schemas between two collections',
2362 | {
2363 | sourceCollection: z.string().min(1).describe('Source collection name'),
2364 | targetCollection: z.string().min(1).describe('Target collection name'),
2365 | sampleSize: z.number().int().min(1).default(100).describe('Number of documents to sample')
2366 | },
2367 | async ({ sourceCollection, targetCollection, sampleSize }) => {
2368 | return withErrorHandling(async () => {
2369 | log(`Tool: Comparing schemas between '${sourceCollection}' and '${targetCollection}'…`)
2370 | const sourceSchema = await inferSchema(sourceCollection, sampleSize)
2371 | const targetSchema = await inferSchema(targetCollection, sampleSize)
2372 | const comparison = compareSchemas(sourceSchema, targetSchema)
2373 | return {
2374 | content: [{
2375 | type: 'text',
2376 | text: formatSchemaComparison(comparison, sourceCollection, targetCollection)
2377 | }]
2378 | }
2379 | }, `Error comparing schemas between '${sourceCollection}' and '${targetCollection}'`)
2380 | }
2381 | )
2382 | }
2383 |
2384 | if (!isDisabled('tools', 'explain-query')) {
2385 | server.tool(
2386 | 'explain-query',
2387 | 'Analyze query performance',
2388 | {
2389 | collection: z.string().min(1).describe('Collection name'),
2390 | filter: z.string().describe('MongoDB query filter (JSON string)'),
2391 | verbosity: z.enum(['queryPlanner', 'executionStats', 'allPlansExecution']).default('executionStats').describe('Explain verbosity level')
2392 | },
2393 | async ({ collection, filter, verbosity }) => {
2394 | return withErrorHandling(async () => {
2395 | log(`Tool: Explaining query on collection '${collection}'…`)
2396 | log(`Tool: Filter: ${filter}`)
2397 | log(`Tool: Verbosity level: ${verbosity}`)
2398 |
2399 | const parsedFilter = parseJsonString(filter)
2400 | const explanation = await explainQuery(collection, parsedFilter, verbosity)
2401 | log(`Tool: Query explanation generated.`)
2402 |
2403 | const fields = await getCollectionFields(collection)
2404 | const fieldInfo = fields.length > 0 ?
2405 | `\n\nAvailable fields for projection: ${fields.join(', ')}` : ''
2406 |
2407 | const suggestedProjection = getSuggestedProjection(parsedFilter, fields)
2408 | const projectionInfo = suggestedProjection ?
2409 | `\n\nSuggested projection: ${suggestedProjection}` : ''
2410 |
2411 | return {
2412 | content: [{
2413 | type: 'text',
2414 | text: formatExplanation(explanation) + fieldInfo + projectionInfo
2415 | }]
2416 | }
2417 | }, `Error explaining query for collection '${collection}'`)
2418 | }
2419 | )
2420 | }
2421 |
2422 | if (!isDisabled('tools', 'analyze-query-patterns')) {
2423 | server.tool(
2424 | 'analyze-query-patterns',
2425 | 'Analyze query patterns and suggest optimizations',
2426 | {
2427 | collection: z.string().min(1).describe('Collection name to analyze'),
2428 | duration: z.number().int().min(1).max(60).default(config.tools.queryAnalysis.defaultDurationSeconds).describe('Duration to analyze in seconds')
2429 | },
2430 | async ({ collection, duration }) => {
2431 | return withErrorHandling(async () => {
2432 | log(`Tool: Analyzing query patterns for collection '${collection}'…`)
2433 |
2434 | await throwIfCollectionNotExists(collection)
2435 | const indexes = await getCollectionIndexes(collection)
2436 | const schema = await inferSchema(collection)
2437 |
2438 | await setProfilerSlowMs()
2439 |
2440 | let queryStats = []
2441 | try {
2442 | const adminDb = mongoClient.db('admin')
2443 | const profilerStatus = await currentDb.command({ profile: -1 })
2444 |
2445 | let prevProfileLevel = profilerStatus.was
2446 | let prevSlowMs = profilerStatus.slowms
2447 |
2448 | await currentDb.command({ profile: 2, slowms: 0 })
2449 |
2450 | log(`Tool: Monitoring queries for ${duration} seconds…`)
2451 |
2452 | await new Promise(resolve => setTimeout(resolve, duration * 1000))
2453 |
2454 | queryStats = await currentDb.collection('system.profile')
2455 | .find({ ns: `${currentDbName}.${collection}`, op: 'query' })
2456 | .sort({ ts: -1 })
2457 | .limit(100)
2458 | .toArray()
2459 |
2460 | await currentDb.command({ profile: prevProfileLevel, slowms: prevSlowMs })
2461 |
2462 | } catch (profileError) {
2463 | log(`Tool: Unable to use profiler: ${profileError.message}`)
2464 | }
2465 |
2466 | const analysis = analyzeQueryPatterns(collection, schema, indexes, queryStats)
2467 |
2468 | return {
2469 | content: [{
2470 | type: 'text',
2471 | text: formatQueryAnalysis(analysis)
2472 | }]
2473 | }
2474 | }, `Error analyzing query patterns for '${collection}'`)
2475 | }
2476 | )
2477 | }
2478 |
2479 | if (!isDisabled('tools', 'bulk-operations')) {
2480 | server.tool(
2481 | 'bulk-operations',
2482 | 'Perform bulk inserts, updates, or deletes',
2483 | {
2484 | collection: z.string().min(1).describe('Collection name'),
2485 | operations: z.string().describe('Array of operations as JSON string'),
2486 | ordered: createBooleanSchema('Whether operations should be performed in order', 'true'),
2487 | token: z.string().optional().describe('Confirmation token from previous request')
2488 | },
2489 | async ({ collection, operations, ordered, token }) => {
2490 | return withErrorHandling(async () => {
2491 | log(`Tool: Processing bulk operations on collection '${collection}'…`)
2492 | const parsedOperations = parseJsonString(operations)
2493 |
2494 | const deleteOps = parsedOperations.filter(op =>
2495 | op.deleteOne || op.deleteMany
2496 | )
2497 |
2498 | if (deleteOps.length === 0 || config.disableDestructiveOperationTokens) {
2499 | const result = await bulkOperations(collection, parsedOperations, ordered === 'true')
2500 |
2501 | const cacheKey = `${currentDbName}.${collection}`
2502 | memoryCache.schemas.delete(cacheKey)
2503 | memoryCache.fields.delete(cacheKey)
2504 | memoryCache.stats.delete(cacheKey)
2505 |
2506 | return {
2507 | content: [{
2508 | type: 'text',
2509 | text: formatBulkResult(result)
2510 | }]
2511 | }
2512 | }
2513 |
2514 | if (token) {
2515 | if (!validateBulkOperationsToken(collection, parsedOperations, token)) {
2516 | throw new Error(`Invalid or expired confirmation token. Please try again without a token to generate a new confirmation code.`)
2517 | }
2518 | const result = await bulkOperations(collection, parsedOperations, ordered === 'true')
2519 | return {
2520 | content: [{
2521 | type: 'text',
2522 | text: formatBulkResult(result)
2523 | }]
2524 | }
2525 | }
2526 |
2527 | await throwIfCollectionNotExists(collection)
2528 | const newToken = storeBulkOperationsToken(collection, parsedOperations)
2529 | return {
2530 | content: [{
2531 | type: 'text',
2532 | text: `⚠️ DESTRUCTIVE OPERATION WARNING ⚠️\n\nYou've requested to perform bulk operations on collection '${collection}' including ${deleteOps.length} delete operation(s).\n\nDelete operations are irreversible. To confirm, type the 4-digit confirmation code EXACTLY as shown below:\n\nConfirmation code: ${newToken}\n\nThis code will expire in 5 minutes for security purposes.\n\n${importantNoticeToAI}`
2533 | }]
2534 | }
2535 | }, `Error processing bulk operations for collection '${collection}'`)
2536 | }
2537 | )
2538 | }
2539 |
2540 | if (!isDisabled('tools', 'create-timeseries')) {
2541 | server.tool(
2542 | 'create-timeseries',
2543 | 'Create a time series collection for temporal data',
2544 | {
2545 | name: z.string().min(1).describe('Collection name'),
2546 | timeField: z.string().min(1).describe('Field that contains the time value'),
2547 | metaField: z.string().optional().describe('Field that contains metadata for grouping'),
2548 | granularity: z.enum(['seconds', 'minutes', 'hours']).default('seconds').describe('Time series granularity'),
2549 | expireAfterSeconds: z.number().int().optional().describe('Optional TTL in seconds')
2550 | },
2551 | async ({ name, timeField, metaField, granularity, expireAfterSeconds }) => {
2552 | return withErrorHandling(async () => {
2553 | log(`Tool: Creating time series collection '${name}'…`)
2554 |
2555 | const adminDb = mongoClient.db('admin')
2556 | const serverInfo = await adminDb.command({ buildInfo: 1 })
2557 | const versionParts = serverInfo.version.split('.').map(Number)
2558 | if (versionParts[0] < 5) {
2559 | return { content: [{ type: 'text', text: `Time series collections require MongoDB 5.0+` }] }
2560 | }
2561 |
2562 | const options = {
2563 | timeseries: {
2564 | timeField,
2565 | granularity
2566 | }
2567 | }
2568 |
2569 | if (metaField) options.timeseries.metaField = metaField
2570 | if (expireAfterSeconds) options.expireAfterSeconds = expireAfterSeconds
2571 |
2572 | const result = await createCollection(name, options)
2573 |
2574 | memoryCache.collections.delete(currentDbName)
2575 |
2576 | return {
2577 | content: [{
2578 | type: 'text',
2579 | text: `Time series collection '${name}' created successfully.`
2580 | }]
2581 | }
2582 | }, `Error creating time series collection '${name}'`)
2583 | }
2584 | )
2585 | }
2586 |
2587 | if (!isDisabled('tools', 'collation-query')) {
2588 | server.tool(
2589 | 'collation-query',
2590 | 'Find documents with language-specific collation rules',
2591 | {
2592 | collection: z.string().min(1).describe('Collection name'),
2593 | filter: z.string().default('{}').describe('Query filter as JSON string'),
2594 | locale: z.string().min(2).describe('Locale code (e.g., "en", "fr", "de")'),
2595 | strength: z.number().int().min(1).max(5).default(3).describe('Collation strength (1-5)'),
2596 | caseLevel: createBooleanSchema('Consider case in first-level differences', 'false'),
2597 | sort: z.string().optional().describe('Sort specification as JSON string')
2598 | },
2599 | async ({ collection, filter, locale, strength, caseLevel, sort }) => {
2600 | return withErrorHandling(async () => {
2601 | log(`Tool: Running collation query on collection '${collection}' with locale '${locale}'`)
2602 |
2603 | const parsedFilter = parseJsonString(filter)
2604 | const parsedSort = sort ? parseJsonString(sort) : null
2605 |
2606 | const collationOptions = {
2607 | locale,
2608 | strength,
2609 | caseLevel
2610 | }
2611 |
2612 | const coll = currentDb.collection(collection)
2613 | let query = coll.find(parsedFilter).collation(collationOptions)
2614 |
2615 | if (parsedSort) query = query.sort(parsedSort)
2616 |
2617 | const results = await query.toArray()
2618 |
2619 | return {
2620 | content: [{
2621 | type: 'text',
2622 | text: formatCollationResults(results, locale, strength, caseLevel)
2623 | }]
2624 | }
2625 | }, `Error running collation query on collection '${collection}' with locale '${locale}'`)
2626 | }
2627 | )
2628 | }
2629 |
2630 | if (!isDisabled('tools', 'text-search')) {
2631 | server.tool(
2632 | 'text-search',
2633 | 'Perform full-text search across text-indexed fields',
2634 | {
2635 | collection: z.string().min(1).describe('Collection name'),
2636 | searchText: z.string().min(1).describe('Text to search for'),
2637 | language: z.string().optional().describe('Optional language for text search'),
2638 | caseSensitive: createBooleanSchema('Case sensitive search', 'false'),
2639 | diacriticSensitive: createBooleanSchema('Diacritic sensitive search', 'false'),
2640 | limit: z.number().int().min(1).default(10).describe('Maximum results to return')
2641 | },
2642 | async ({ collection, searchText, language, caseSensitive, diacriticSensitive, limit }) => {
2643 | return withErrorHandling(async () => {
2644 | log(`Tool: Performing text search in collection '${collection}' for: "${searchText}"`)
2645 |
2646 | try {
2647 | const coll = currentDb.collection(collection)
2648 | const indexes = await coll.listIndexes().toArray()
2649 | const hasTextIndex = indexes.some(idx => Object.values(idx.key).includes('text'))
2650 |
2651 | if (!hasTextIndex) {
2652 | return {
2653 | content: [{
2654 | type: 'text',
2655 | text: `No text index found on collection '${collection}'.\n\nText search requires a text index. Create one with:\n\ncreate-index {\n "collection": "${collection}",\n "keys": "{\\"fieldName\\": \\"text\\"}"\n}`
2656 | }]
2657 | }
2658 | }
2659 | } catch (indexError) {
2660 | log(`Warning: Unable to check for text indexes: ${indexError.message}`, true)
2661 | }
2662 |
2663 | const textQuery = { $search: searchText }
2664 | if (language) textQuery.$language = language
2665 | if (caseSensitive === 'true') textQuery.$caseSensitive = true
2666 | if (diacriticSensitive === 'true') textQuery.$diacriticSensitive = true
2667 |
2668 | const query = { $text: textQuery }
2669 | const projection = { score: { $meta: 'textScore' } }
2670 | const sort = { score: { $meta: 'textScore' } }
2671 |
2672 | const results = await findDocuments(collection, query, projection, limit, 0, sort)
2673 |
2674 | return {
2675 | content: [{
2676 | type: 'text',
2677 | text: formatTextSearchResults(results, searchText)
2678 | }]
2679 | }
2680 | }, `Error performing text search in collection '${collection}' for: "${searchText}"`)
2681 | }
2682 | )
2683 | }
2684 |
2685 | if (!isDisabled('tools', 'geo-query')) {
2686 | server.tool(
2687 | 'geo-query',
2688 | 'Run geospatial queries with various operators',
2689 | {
2690 | collection: z.string().min(1).describe('Collection name'),
2691 | operator: z.enum(['near', 'geoWithin', 'geoIntersects']).describe('Geospatial operator type'),
2692 | field: z.string().min(1).describe('Geospatial field name'),
2693 | geometry: z.string().describe('GeoJSON geometry as JSON string'),
2694 | maxDistance: z.number().optional().describe('Maximum distance in meters (for near queries)'),
2695 | limit: z.number().int().min(1).default(10).describe('Maximum number of documents to return')
2696 | },
2697 | async ({ collection, operator, field, geometry, maxDistance, limit }) => {
2698 | return withErrorHandling(async () => {
2699 | log(`Tool: Running geospatial query on collection '${collection}'…`)
2700 |
2701 | let indexMessage = ''
2702 | try {
2703 | const coll = currentDb.collection(collection)
2704 | const indexes = await coll.listIndexes().toArray()
2705 |
2706 | const hasGeoIndex = indexes.some(idx => {
2707 | if (!idx.key[field]) return false
2708 | const indexType = idx.key[field]
2709 | return indexType === '2dsphere' || indexType === '2d'
2710 | })
2711 |
2712 | if (!hasGeoIndex) {
2713 | log(`Warning: No geospatial index found for field '${field}' in collection '${collection}'`, true)
2714 | indexMessage = "\n\nNote: This query would be more efficient with a geospatial index. " +
2715 | `Consider creating a 2dsphere index with: create-index {"collection": "${collection}", "keys": "{\\"${field}\\": \\"2dsphere\\"}"}`
2716 | }
2717 | } catch (indexError) {
2718 | log(`Warning: Unable to check for geospatial indexes: ${indexError.message}`, true)
2719 | }
2720 |
2721 | const geoJson = parseJsonString(geometry)
2722 | let query = {}
2723 |
2724 | if (operator === 'near') {
2725 | query[field] = { $near: { $geometry: geoJson } }
2726 | if (maxDistance) query[field].$near.$maxDistance = maxDistance
2727 | } else if (operator === 'geoWithin') {
2728 | query[field] = { $geoWithin: { $geometry: geoJson } }
2729 | } else if (operator === 'geoIntersects') {
2730 | query[field] = { $geoIntersects: { $geometry: geoJson } }
2731 | }
2732 |
2733 | const results = await findDocuments(collection, query, null, limit, 0)
2734 | const resultText = formatDocuments(results, limit) + indexMessage
2735 |
2736 | return {
2737 | content: [{
2738 | type: 'text',
2739 | text: resultText
2740 | }]
2741 | }
2742 | }, `Error running geospatial query on collection '${collection}'`)
2743 | }
2744 | )
2745 | }
2746 |
2747 | if (!isDisabled('tools', 'transaction')) {
2748 | server.tool(
2749 | 'transaction',
2750 | 'Execute multiple operations in a single transaction',
2751 | {
2752 | operations: z.string().describe('JSON array of operations with collection, operation type, and parameters')
2753 | },
2754 | async ({ operations }) => {
2755 | return withErrorHandling(async () => {
2756 | log('Tool: Executing operations in a transaction…')
2757 |
2758 | try {
2759 | const session = mongoClient.startSession()
2760 | await session.endSession()
2761 | } catch (error) {
2762 | if (error.message.includes('not supported') ||
2763 | error.message.includes('requires replica set') ||
2764 | error.codeName === 'NotAReplicaSet') {
2765 | return {
2766 | content: [{
2767 | type: 'text',
2768 | text: `Transactions are not supported on your MongoDB deployment.\n\nTransactions require MongoDB to be running as a replica set or sharded cluster. You appear to be running a standalone server.\n\nAlternative: You can set up a single-node replica set for development purposes by following these steps:\n\n1. Stop your MongoDB server\n2. Start it with the --replSet option: \`mongod --replSet rs0\`\n3. Connect to it and initialize the replica set: \`rs.initiate()\`\n\nThen try the transaction tool again.`
2769 | }]
2770 | }
2771 | }
2772 | throw error
2773 | }
2774 |
2775 | const parsedOps = parseJsonString(operations)
2776 | const session = mongoClient.startSession()
2777 | let results = []
2778 |
2779 | try {
2780 | session.startTransaction({
2781 | readConcern: { level: config.tools.transaction.readConcern },
2782 | writeConcern: config.tools.transaction.writeConcern
2783 | })
2784 |
2785 | for (let i = 0; i < parsedOps.length; i++) {
2786 | const op = parsedOps[i]
2787 | log(`Tool: Transaction step ${i+1}: ${op.operation} on ${op.collection}`)
2788 |
2789 | let result
2790 | const collection = currentDb.collection(op.collection)
2791 |
2792 | if (op.operation === 'insert') {
2793 | result = await collection.insertOne(op.document, { session })
2794 | } else if (op.operation === 'update') {
2795 | result = await collection.updateOne(op.filter, op.update, { session })
2796 | } else if (op.operation === 'delete') {
2797 | result = await collection.deleteOne(op.filter, { session })
2798 | } else if (op.operation === 'find') {
2799 | result = await collection.findOne(op.filter, { session })
2800 | } else {
2801 | throw new Error(`Unsupported operation: ${op.operation}`)
2802 | }
2803 |
2804 | results.push({ step: i+1, operation: op.operation, result })
2805 | }
2806 |
2807 | await session.commitTransaction()
2808 |
2809 | clearMemoryCache()
2810 |
2811 | log('Tool: Transaction committed successfully')
2812 | } catch (error) {
2813 | await session.abortTransaction()
2814 |
2815 | clearMemoryCache()
2816 |
2817 | log(`Tool: Transaction aborted due to error: ${error.message}`)
2818 | throw error
2819 | } finally {
2820 | await session.endSession()
2821 | }
2822 |
2823 | return {
2824 | content: [{
2825 | type: 'text',
2826 | text: formatTransactionResults(results)
2827 | }]
2828 | }
2829 | }, 'Error executing transaction')
2830 | }
2831 | )
2832 | }
2833 |
2834 | if (!isDisabled('tools', 'watch-changes')) {
2835 | server.tool(
2836 | 'watch-changes',
2837 | 'Watch for changes in a collection using change streams',
2838 | {
2839 | collection: z.string().min(1).describe('Collection name'),
2840 | operations: z.array(z.enum(['insert', 'update', 'delete', 'replace'])).default(['insert', 'update', 'delete']).describe('Operations to watch'),
2841 | duration: z.number().int().min(1).max(60).default(config.tools.watchChanges.defaultDurationSeconds).describe('Duration to watch in seconds'),
2842 | fullDocument: createBooleanSchema('Include full document in update events', 'false')
2843 | },
2844 | async ({ collection, operations, duration, fullDocument }) => {
2845 | return withErrorHandling(async () => {
2846 | log(`Tool: Watching collection '${collection}' for changes…`)
2847 |
2848 | let maxDuration = config.tools.watchChanges.maxDurationSeconds
2849 | let actualDuration = duration
2850 |
2851 | if (actualDuration > maxDuration) {
2852 | log(`Requested duration ${actualDuration}s exceeds maximum ${maxDuration}s, using maximum`, true)
2853 | actualDuration = maxDuration
2854 | }
2855 |
2856 | try {
2857 | const adminDb = mongoClient.db('admin')
2858 | await adminDb.command({ replSetGetStatus: 1 })
2859 | } catch (err) {
2860 | if (err.codeName === 'NotYetInitialized' ||
2861 | err.codeName === 'NoReplicationEnabled' ||
2862 | err.message.includes('not running with --replSet') ||
2863 | err.code === 76 || err.code === 40573) {
2864 | return {
2865 | content: [{
2866 | type: 'text',
2867 | text: `Change streams are not supported on your MongoDB deployment.\n\nChange streams require MongoDB to be running as a replica set or sharded cluster. You appear to be running a standalone server.\n\nAlternative: You can set up a single-node replica set for development purposes by following these steps:\n\n1. Stop your MongoDB server\n2. Start it with the --replSet option: \`mongod --replSet rs0\`\n3. Connect to it and initialize the replica set: \`rs.initiate()\`\n\nThen try the watch-changes tool again.`
2868 | }]
2869 | }
2870 | }
2871 | }
2872 |
2873 | const pipeline = [
2874 | { $match: { 'operationType': { $in: operations } } }
2875 | ]
2876 |
2877 | const options = {}
2878 | if (fullDocument) options.fullDocument = 'updateLookup'
2879 |
2880 | const coll = currentDb.collection(collection)
2881 | const changeStream = coll.watch(pipeline, options)
2882 |
2883 | const changes = []
2884 | const timeout = setTimeout(() => {
2885 | changeStream.close()
2886 | }, actualDuration * 1000)
2887 |
2888 | changeStream.on('change', change => {
2889 | changes.push(change)
2890 | })
2891 |
2892 | return new Promise(resolve => {
2893 | changeStream.on('close', () => {
2894 | clearTimeout(timeout)
2895 | resolve({
2896 | content: [{
2897 | type: 'text',
2898 | text: formatChangeStreamResults(changes, actualDuration)
2899 | }]
2900 | })
2901 | })
2902 | })
2903 | }, `Error watching for changes in collection '${collection}'`)
2904 | }
2905 | )
2906 | }
2907 |
2908 | if (!isDisabled('tools', 'gridfs-operation')) {
2909 | server.tool(
2910 | 'gridfs-operation',
2911 | 'Manage large files with GridFS',
2912 | {
2913 | operation: z.enum(['list', 'info', 'delete']).describe('GridFS operation type'),
2914 | bucket: z.string().default('fs').describe('GridFS bucket name'),
2915 | filename: z.string().optional().describe('Filename for info/delete operations'),
2916 | limit: z.number().int().min(1).default(20).describe('Maximum files to list')
2917 | },
2918 | async ({ operation, bucket, filename, limit }) => {
2919 | return withErrorHandling(async () => {
2920 | log(`Tool: Performing GridFS ${operation} operation on bucket '${bucket}'`)
2921 |
2922 | const gridFsBucket = new mongodb.GridFSBucket(currentDb, { bucketName: bucket })
2923 | let result
2924 |
2925 | if (operation === 'list') {
2926 | const files = await currentDb.collection(`${bucket}.files`).find({}).limit(limit).toArray()
2927 | result = formatGridFSList(files)
2928 | } else if (operation === 'info') {
2929 | if (!filename) throw new Error('Filename is required for info operation')
2930 | const file = await currentDb.collection(`${bucket}.files`).findOne({ filename })
2931 | if (!file) throw new Error(`File '${filename}' not found`)
2932 | result = formatGridFSInfo(file)
2933 | } else if (operation === 'delete') {
2934 | if (!filename) throw new Error('Filename is required for delete operation')
2935 | await gridFsBucket.delete(await getFileId(bucket, filename))
2936 | memoryCache.collections.delete(currentDbName)
2937 | result = `File '${filename}' deleted successfully from bucket '${bucket}'`
2938 | }
2939 |
2940 | return {
2941 | content: [{
2942 | type: 'text',
2943 | text: result
2944 | }]
2945 | }
2946 | }, `Error performing GridFS ${operation} operation${filename ? ` on file '${filename}'` : ''}`)
2947 | }
2948 | )
2949 | }
2950 |
2951 | if (!isDisabled('tools', 'clear-cache')) {
2952 | server.tool(
2953 | 'clear-cache',
2954 | 'Clear memory caches to ensure fresh data',
2955 | {
2956 | target: z.enum(['all', 'collections', 'schemas', 'indexes', 'stats', 'fields', 'serverStatus']).default('all').describe('Cache type to clear (default: all)')
2957 | },
2958 | async ({ target }) => {
2959 | return withErrorHandling(async () => {
2960 | log(`Tool: Clearing cache: ${target}`)
2961 |
2962 | if (target === 'all') {
2963 | clearMemoryCache()
2964 | return {
2965 | content: [{
2966 | type: 'text',
2967 | text: 'All enabled caches have been cleared. Next data requests will fetch fresh data from MongoDB.'
2968 | }]
2969 | }
2970 | }
2971 |
2972 | if (Object.keys(memoryCache).includes(target)) {
2973 | if (isCacheEnabled(target)) {
2974 | memoryCache[target].clear()
2975 | return {
2976 | content: [{
2977 | type: 'text',
2978 | text: `The ${target} cache has been cleared. Next ${target} requests will fetch fresh data from MongoDB.`
2979 | }]
2980 | }
2981 | }
2982 |
2983 | return {
2984 | content: [{
2985 | type: 'text',
2986 | text: `The ${target} cache is currently disabled. No action taken.`
2987 | }]
2988 | }
2989 | }
2990 |
2991 | return {
2992 | content: [{
2993 | type: 'text',
2994 | text: `Invalid cache target: ${target}. Valid targets are: all, ${config.enabledCaches.join(', ')}`
2995 | }],
2996 | isError: true
2997 | }
2998 | }, `Error clearing cache target: ${target}`)
2999 | }
3000 | )
3001 | }
3002 |
3003 | if (!isDisabled('tools', 'shard-status')) {
3004 | server.tool(
3005 | 'shard-status',
3006 | 'Get sharding status for database or collections',
3007 | {
3008 | target: z.enum(['database', 'collection']).default('database').describe('Target type'),
3009 | collection: z.string().optional().describe('Collection name (if target is collection)')
3010 | },
3011 | async ({ target, collection }) => {
3012 | return withErrorHandling(async () => {
3013 | log(`Tool: Getting shard status for ${target}${collection ? ` '${collection}'` : ''}`)
3014 |
3015 | try {
3016 | const adminDb = mongoClient.db('admin')
3017 | await adminDb.command({ listShards: 1 })
3018 | } catch (error) {
3019 | if (error.code === 72 || error.message.includes('not running with sharding') ||
3020 | error.codeName === 'InvalidOptions') {
3021 | return {
3022 | content: [{
3023 | type: 'text',
3024 | text: `Sharding is not enabled on your MongoDB deployment.\n\nThis command requires MongoDB to be running as a sharded cluster.\nYou appear to be running a standalone server or replica set without sharding enabled.\n\nTo use sharding features, you need to set up a sharded cluster with:\n- Config servers\n- Mongos router(s)\n- Shard replica sets`
3025 | }]
3026 | }
3027 | }
3028 | throw error
3029 | }
3030 |
3031 | const adminDb = mongoClient.db('admin')
3032 | let result
3033 |
3034 | if (target === 'database') {
3035 | const listShards = await adminDb.command({ listShards: 1 })
3036 | const dbStats = await adminDb.command({ dbStats: 1, scale: 1 })
3037 | const dbShardStatus = await getShardingDbStatus(currentDbName)
3038 | result = formatShardDbStatus(listShards, dbStats, dbShardStatus, currentDbName)
3039 | } else {
3040 | if (!collection) throw new Error('Collection name is required when target is collection')
3041 | const collStats = await currentDb.command({ collStats: collection })
3042 | const collShardStatus = await getShardingCollectionStatus(currentDbName, collection)
3043 | result = formatShardCollectionStatus(collStats, collShardStatus, collection)
3044 | }
3045 |
3046 | return {
3047 | content: [{
3048 | type: 'text',
3049 | text: result
3050 | }]
3051 | }
3052 | }, `Error getting shard status for ${target}${collection ? ` '${collection}'` : ''}`)
3053 | }
3054 | )
3055 | }
3056 |
3057 | if (!isDisabled('tools', 'export-data')) {
3058 | server.tool(
3059 | 'export-data',
3060 | 'Export query results to formatted JSON or CSV',
3061 | {
3062 | collection: z.string().min(1).describe('Collection name'),
3063 | filter: z.string().default('{}').describe('Filter as JSON string'),
3064 | format: z.enum(['json', 'csv']).default('json').describe('Export format'),
3065 | fields: z.string().optional().describe('Comma-separated list of fields to include (for CSV)'),
3066 | limit: z.number().int().min(1).default(1000).describe('Maximum documents to export'),
3067 | sort: z.string().optional().describe('Sort specification as JSON string (e.g. {"date": -1} for descending)')
3068 | },
3069 | async ({ collection, filter, format, fields, limit, sort }) => {
3070 | return withErrorHandling(async () => {
3071 | log(`Tool: Exporting data from collection '${collection}' in ${format} format…`)
3072 | log(`Tool: Using filter: ${filter}`)
3073 | if (sort) log(`Tool: Using sort: ${sort}`)
3074 | log(`Tool: Max documents: ${limit}`)
3075 | const parsedFilter = filter ? JSON.parse(filter) : {}
3076 | const parsedSort = sort ? JSON.parse(sort) : null
3077 | let fieldsArray = fields ? fields.split(',').map(f => f.trim()) : null
3078 | const documents = await findDocuments(collection, parsedFilter, null, limit, 0, parsedSort)
3079 | log(`Tool: Found ${documents.length} documents to export.`)
3080 | const exportData = await formatExport(documents, format, fieldsArray)
3081 | log(`Tool: Data exported successfully in ${format} format.`)
3082 | return {
3083 | content: [{
3084 | type: 'text',
3085 | text: exportData
3086 | }]
3087 | }
3088 | }, `Error exporting data from collection '${collection}'`)
3089 | }
3090 | )
3091 | }
3092 |
3093 | const totalRegisteredTools = Object.keys(server._registeredTools).length
3094 | log(`Total MCP tools: ${totalRegisteredTools}`)
3095 | }
3096 |
3097 | const withErrorHandling = async (operation, errorMessage, defaultValue = null) => {
3098 | try {
3099 | return await operation()
3100 | } catch (error) {
3101 | const formattedError = `${errorMessage}: ${error.message}`
3102 | log(formattedError, true)
3103 |
3104 | let errorCode = JSONRPC_ERROR_CODES.SERVER_ERROR_START
3105 | let errorType = error.name || 'Error'
3106 |
3107 | if (error.name === 'MongoError' || error.name === 'MongoServerError') {
3108 | switch(error.code) {
3109 | case 13:
3110 | errorCode = JSONRPC_ERROR_CODES.RESOURCE_ACCESS_DENIED; break
3111 | case 59: case 61:
3112 | errorCode = JSONRPC_ERROR_CODES.MONGODB_CONNECTION_ERROR; break
3113 | case 121:
3114 | errorCode = JSONRPC_ERROR_CODES.MONGODB_SCHEMA_ERROR; break
3115 | case 11000:
3116 | errorCode = JSONRPC_ERROR_CODES.MONGODB_DUPLICATE_KEY; break
3117 | case 112: case 16500:
3118 | errorCode = JSONRPC_ERROR_CODES.MONGODB_WRITE_ERROR; break
3119 | case 50: case 57:
3120 | errorCode = JSONRPC_ERROR_CODES.MONGODB_TIMEOUT_ERROR; break
3121 | default:
3122 | errorCode = JSONRPC_ERROR_CODES.MONGODB_QUERY_ERROR
3123 | }
3124 | } else if (error.message.match(/not found|does not exist|cannot find/i)) {
3125 | errorCode = JSONRPC_ERROR_CODES.RESOURCE_NOT_FOUND
3126 | } else if (error.message.match(/already exists|duplicate/i)) {
3127 | errorCode = JSONRPC_ERROR_CODES.RESOURCE_ALREADY_EXISTS
3128 | } else if (error.message.match(/permission|access denied|unauthorized/i)) {
3129 | errorCode = JSONRPC_ERROR_CODES.RESOURCE_ACCESS_DENIED
3130 | } else if (error.message.match(/timeout|timed out/i)) {
3131 | errorCode = JSONRPC_ERROR_CODES.MONGODB_TIMEOUT_ERROR
3132 | }
3133 |
3134 | return {
3135 | content: [{
3136 | type: 'text',
3137 | text: formattedError
3138 | }],
3139 | isError: true,
3140 | error: {
3141 | code: errorCode,
3142 | message: error.message,
3143 | data: {
3144 | type: errorType,
3145 | details: error.code ? `MongoDB error code: ${error.code}` : undefined
3146 | }
3147 | }
3148 | }
3149 | }
3150 | }
3151 |
3152 | const ensureValidMongoUri = (uri) => {
3153 | if (uri.includes('://') || uri.includes('@') || mongoUriMap.has(uri.toLowerCase())) return uri
3154 |
3155 | const validHostRegex = new RegExp([
3156 | // Domain name pattern (with optional port)
3157 | '^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\\.)+' +
3158 | '[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9](?::\\d+)?$',
3159 | // localhost pattern (with optional port)
3160 | '|^localhost(?::\\d+)?$',
3161 | // IPv4 address pattern (with optional port)
3162 | '|^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}(?::\\d+)?$'
3163 | ].join(''))
3164 |
3165 | if (validHostRegex.test(uri)) {
3166 | const modifiedUri = `mongodb://${uri}`
3167 | log(`Automatically added protocol to URI: ${obfuscateMongoUri(modifiedUri)}`)
3168 | return modifiedUri
3169 | }
3170 |
3171 | return uri
3172 | }
3173 |
3174 | const isDatabaseName = async (name) => {
3175 | try {
3176 | const dbs = await listDatabases()
3177 | if (dbs.some(db => db.name === name)) return true
3178 |
3179 | if (name.length > 0 && name.length < 64 &&
3180 | !name.includes('/') && !name.includes(':')) {
3181 | const dbPatterns = [
3182 | /^sample_/,
3183 | /^test/,
3184 | /^db/,
3185 | /^admin$|^local$|^config$/
3186 | ]
3187 |
3188 | if (dbPatterns.some(pattern => pattern.test(name))) return true
3189 | }
3190 |
3191 | return false
3192 | } catch (error) {
3193 | return false
3194 | }
3195 | }
3196 |
3197 | const listDatabases = async () => {
3198 | log('DB Operation: Listing databases…')
3199 | const adminDb = mongoClient.db('admin')
3200 | const result = await adminDb.admin().listDatabases()
3201 | log(`DB Operation: Found ${result.databases.length} databases.`)
3202 | return result.databases
3203 | }
3204 |
3205 | const createDatabase = async (dbName, validateName = true) => {
3206 | log(`DB Operation: Creating database '${dbName}'…`)
3207 | if (validateName.toLowerCase() === 'true' && config.security.strictDatabaseNameValidation) {
3208 | const invalidChars = /[\/\\.\s"$*<>:|?]/
3209 | if (invalidChars.test(dbName)) {
3210 | throw new Error(`Invalid database name: '${dbName}'. Database names cannot contain spaces or special characters like /, \\, ., ", $, *, <, >, :, |, ?`)
3211 | }
3212 | if (dbName.length > 63) {
3213 | throw new Error(`Invalid database name: '${dbName}'. Database names must be shorter than 64 characters.`)
3214 | }
3215 | }
3216 |
3217 | const db = mongoClient.db(dbName)
3218 | const metadataCollectionName = 'metadata'
3219 | const timestamp = new Date()
3220 | const serverInfo = await mongoClient.db('admin').command({ buildInfo: 1 }).catch(() => ({ version: 'Unknown' }))
3221 | const clientInfo = await mongoClient.db('admin').command({ connectionStatus: 1 }).catch(() => ({ authInfo: { authenticatedUsers: [] } }))
3222 | const metadata = {
3223 | created: {
3224 | timestamp,
3225 | tool: `MongoDB Lens v${getPackageVersion()}`,
3226 | user: clientInfo.authInfo?.authenticatedUsers[0]?.user || 'anonymous'
3227 | },
3228 | mongodb: {
3229 | version: serverInfo.version,
3230 | connectionInfo: {
3231 | host: mongoClient.s?.options?.hosts?.map(h => `${h.host}:${h.port}`).join(',') || 'unknown',
3232 | readPreference: mongoClient.s?.readPreference?.mode || 'primary'
3233 | }
3234 | },
3235 | database: {
3236 | name: dbName,
3237 | description: 'Created via MongoDB Lens'
3238 | },
3239 | system: {
3240 | hostname: process.env.HOSTNAME || 'unknown',
3241 | platform: process.platform,
3242 | nodeVersion: process.version
3243 | },
3244 | lens: {
3245 | version: getPackageVersion(),
3246 | startTimestamp: new Date(Date.now() - (process.uptime() * 1000))
3247 | }
3248 | }
3249 |
3250 | try {
3251 | await db.createCollection(metadataCollectionName)
3252 | await db.collection(metadataCollectionName).insertOne(metadata)
3253 | log(`Tool: Database '${dbName}' created successfully with metadata collection.`)
3254 | } catch (error) {
3255 | log(`Warning: Created database '${dbName}' but metadata insertion failed: ${error.message}`, true)
3256 | }
3257 |
3258 | return db
3259 | }
3260 |
3261 | const switchDatabase = async (dbName) => {
3262 | log(`DB Operation: Switching to database '${dbName}'…`)
3263 | try {
3264 | if (config.security.strictDatabaseNameValidation) {
3265 | const invalidChars = /[\/\\.\s"$*<>:|?]/
3266 | if (invalidChars.test(dbName)) {
3267 | throw new Error(`Invalid database name: '${dbName}'. Database names cannot contain spaces or special characters like /, \\, ., ", $, *, <, >, :, |, ?`)
3268 | }
3269 | if (dbName.length > 63) {
3270 | throw new Error(`Invalid database name: '${dbName}'. Database names must be shorter than 64 characters.`)
3271 | }
3272 | }
3273 |
3274 | const dbs = await listDatabases()
3275 | const dbExists = dbs.some(db => db.name === dbName)
3276 | if (!dbExists) throw new Error(`Database '${dbName}' does not exist`)
3277 |
3278 | const oldDbName = currentDbName
3279 | currentDbName = dbName
3280 | currentDb = mongoClient.db(dbName)
3281 | invalidateRelatedCaches(oldDbName)
3282 |
3283 | await setProfilerSlowMs()
3284 |
3285 | log(`DB Operation: Successfully switched to database '${dbName}'.`)
3286 | return currentDb
3287 | } catch (error) {
3288 | log(`DB Operation: Failed to switch to database '${dbName}': ${error.message}`)
3289 | throw error
3290 | }
3291 | }
3292 |
3293 | const dropDatabase = async (dbName) => {
3294 | log(`DB Operation: Dropping database '${dbName}'…`)
3295 | if (dbName.toLowerCase() === 'admin') throw new Error(`Dropping the 'admin' database is prohibited.`)
3296 | try {
3297 | const wasConnected = currentDbName === dbName
3298 | const db = mongoClient.db(dbName)
3299 | await db.dropDatabase()
3300 | invalidateRelatedCaches(dbName)
3301 |
3302 | if (wasConnected) {
3303 | currentDbName = 'admin'
3304 | currentDb = mongoClient.db('admin')
3305 | log(`DB Operation: Switched to 'admin' database after dropping '${dbName}'`)
3306 | }
3307 |
3308 | log(`DB Operation: Database '${dbName}' dropped successfully.`)
3309 |
3310 | const message = `Database '${dbName}' has been permanently deleted.${
3311 | wasConnected ? '\n\nYou were previously connected to this database, you have been automatically switched to the \'admin\' database.' : ''
3312 | }`
3313 |
3314 | return { success: true, name: dbName, message }
3315 | } catch (error) {
3316 | log(`DB Operation: Database drop failed: ${error.message}`)
3317 | throw error
3318 | }
3319 | }
3320 |
3321 | const createUser = async (username, password, roles) => {
3322 | log(`DB Operation: Creating user '${username}' with roles: ${JSON.stringify(roles)}…`)
3323 | try {
3324 | await currentDb.command({
3325 | createUser: username,
3326 | pwd: password,
3327 | roles: roles
3328 | })
3329 | log(`DB Operation: User created successfully.`)
3330 | } catch (error) {
3331 | log(`DB Operation: Failed to create user: ${error.message}`)
3332 | throw error
3333 | }
3334 | }
3335 |
3336 | const dropUser = async (username) => {
3337 | log(`DB Operation: Dropping user '${username}'…`)
3338 | try {
3339 | await currentDb.command({
3340 | dropUser: username
3341 | })
3342 | log(`DB Operation: User dropped successfully.`)
3343 | } catch (error) {
3344 | log(`DB Operation: Failed to drop user: ${error.message}`)
3345 | throw error
3346 | }
3347 | }
3348 |
3349 | const throwIfCollectionNotExists = async (collectionName) => {
3350 | if (!await collectionExists(collectionName)) {
3351 | throw new Error(`Collection '${collectionName}' does not exist`)
3352 | }
3353 | }
3354 |
3355 | const collectionExists = async (collectionName) => {
3356 | if (!currentDb) throw new Error('No database selected')
3357 | const collections = await currentDb.listCollections().toArray()
3358 | return collections.some(coll => coll.name === collectionName)
3359 | }
3360 |
3361 | const listCollections = async () => {
3362 | log(`DB Operation: Listing collections in database '${currentDbName}'…`)
3363 | try {
3364 | if (!currentDb) throw new Error('No database selected')
3365 | const cachedCollections = getCachedValue('collections', currentDbName)
3366 | if (cachedCollections) return cachedCollections
3367 | const collections = await currentDb.listCollections().toArray()
3368 | setCachedValue('collections', currentDbName, collections)
3369 | log(`DB Operation: Found ${collections.length} collections.`)
3370 | return collections
3371 | } catch (error) {
3372 | log(`DB Operation: Failed to list collections: ${error.message}`)
3373 | throw error
3374 | }
3375 | }
3376 |
3377 | const validateCollection = async (collectionName, full = false) => {
3378 | log(`DB Operation: Validating collection '${collectionName}'…`)
3379 | try {
3380 | await throwIfCollectionNotExists(collectionName)
3381 | const result = await currentDb.command({ validate: collectionName, full })
3382 | if (!result) throw new Error(`Validation returned no result`)
3383 | log(`DB Operation: Collection validation complete.`)
3384 | return result
3385 | } catch (error) {
3386 | log(`DB Operation: Collection validation failed: ${error.message}`)
3387 | throw error
3388 | }
3389 | }
3390 |
3391 | const createCollection = async (name, options = {}) => {
3392 | log(`DB Operation: Creating collection '${name}'…`)
3393 | try {
3394 | const result = await currentDb.createCollection(name, options)
3395 | invalidateRelatedCaches(currentDbName)
3396 | if (result === true) return { success: true, name }
3397 | if (result && result.ok === 1) return { success: true, name }
3398 | if (result && result.collectionName === name) return { success: true, name }
3399 | const errorMsg = "Collection creation did not return a valid collection"
3400 | log(`DB Operation: Collection creation failed: ${errorMsg}`)
3401 | throw new Error(errorMsg)
3402 | } catch (error) {
3403 | log(`DB Operation: Collection creation failed: ${error.message}`)
3404 | throw error
3405 | }
3406 | }
3407 |
3408 | const dropCollection = async (name) => {
3409 | log(`DB Operation: Dropping collection '${name}'…`)
3410 | try {
3411 | const result = await currentDb.collection(name).drop()
3412 | invalidateRelatedCaches(currentDbName, name)
3413 | if (result === true) return { success: true, name }
3414 | if (result && result.ok === 1) return { success: true, name }
3415 | if (result && result.dropped === name) return { success: true, name }
3416 | const errorMsg = "Collection drop operation did not return success"
3417 | log(`DB Operation: Collection drop failed: ${errorMsg}`)
3418 | throw new Error(errorMsg)
3419 | } catch (error) {
3420 | log(`DB Operation: Collection drop failed: ${error.message}`)
3421 | throw error
3422 | }
3423 | }
3424 |
3425 | const renameCollection = async (oldName, newName, dropTarget = false) => {
3426 | log(`DB Operation: Renaming collection from '${oldName}' to '${newName}'…`)
3427 | try {
3428 | const result = await currentDb.collection(oldName).rename(newName, { dropTarget })
3429 | invalidateRelatedCaches(currentDbName, oldName)
3430 | invalidateRelatedCaches(currentDbName, newName)
3431 | if (result === true) return { success: true, oldName, newName }
3432 | if (result && result.ok === 1) return { success: true, oldName, newName }
3433 | if (result && result.collectionName === newName) return { success: true, oldName, newName }
3434 | const errorMsg = "Collection rename did not return a valid result"
3435 | log(`DB Operation: Collection rename failed: ${errorMsg}`)
3436 | throw new Error(errorMsg)
3437 | } catch (error) {
3438 | log(`DB Operation: Collection rename failed: ${error.message}`)
3439 | throw error
3440 | }
3441 | }
3442 |
3443 | const getCollectionStats = async (collectionName) => {
3444 | log(`DB Operation: Getting statistics for collection '${collectionName}'…`)
3445 | try {
3446 | await throwIfCollectionNotExists(collectionName)
3447 |
3448 | const cacheKey = `${currentDbName}.${collectionName}`
3449 | const cachedStats = getCachedValue('stats', cacheKey)
3450 | if (cachedStats) return cachedStats
3451 |
3452 | const adminDb = mongoClient.db('admin')
3453 | let serverInfo
3454 | try {
3455 | serverInfo = await adminDb.command({ buildInfo: 1 })
3456 | } catch (verError) {
3457 | log(`DB Operation: Warning: Unable to determine server version: ${verError.message}`)
3458 | serverInfo = { version: '0.0.0' }
3459 | }
3460 |
3461 | const versionParts = serverInfo.version.split('.').map(Number)
3462 | const isVersion4Plus = versionParts[0] >= 4
3463 |
3464 | const statsCmd = isVersion4Plus
3465 | ? { collStats: collectionName, scale: 1 }
3466 | : { collStats: collectionName }
3467 |
3468 | const stats = await currentDb.command(statsCmd)
3469 | const normalizedStats = { ...stats }
3470 |
3471 | if (stats.wiredTiger && isVersion4Plus) {
3472 | normalizedStats.wiredTigerVersion = stats.wiredTiger.creationString || 'unknown'
3473 | }
3474 |
3475 | setCachedValue('stats', cacheKey, normalizedStats)
3476 |
3477 | log(`DB Operation: Retrieved statistics for collection '${collectionName}'.`)
3478 | return normalizedStats
3479 | } catch (error) {
3480 | log(`DB Operation: Failed to get statistics for collection '${collectionName}': ${error.message}`)
3481 | throw error
3482 | }
3483 | }
3484 |
3485 | const getCollectionIndexes = async (collectionName) => {
3486 | log(`DB Operation: Getting indexes for collection '${collectionName}'…`)
3487 | try {
3488 | await throwIfCollectionNotExists(collectionName)
3489 |
3490 | const cacheKey = `${currentDbName}.${collectionName}`
3491 | const cachedIndexes = getCachedValue('indexes', cacheKey)
3492 | if (cachedIndexes) return cachedIndexes
3493 |
3494 | const indexes = await currentDb.collection(collectionName).indexes()
3495 | log(`DB Operation: Retrieved ${indexes.length} indexes for collection '${collectionName}'.`)
3496 |
3497 | try {
3498 | const stats = await currentDb.command({ collStats: collectionName, indexDetails: true })
3499 | if (stats && stats.indexDetails) {
3500 | for (const index of indexes) {
3501 | if (stats.indexDetails[index.name]) {
3502 | index.usage = stats.indexDetails[index.name]
3503 | }
3504 | }
3505 | }
3506 | } catch (statsError) {
3507 | log(`DB Operation: Index usage stats not available: ${statsError.message}`)
3508 | }
3509 |
3510 | setCachedValue('indexes', cacheKey, indexes)
3511 |
3512 | return indexes
3513 | } catch (error) {
3514 | log(`DB Operation: Failed to get indexes for collection '${collectionName}': ${error.message}`)
3515 | throw error
3516 | }
3517 | }
3518 |
3519 | const findDocuments = async (collectionName, filter = {}, projection = null, limit = config.defaults.queryLimit, skip = 0, sort = null) => {
3520 | log(`DB Operation: Finding documents in collection '${collectionName}'…`)
3521 | try {
3522 | await throwIfCollectionNotExists(collectionName)
3523 | const collection = currentDb.collection(collectionName)
3524 | let query = collection.find(filter)
3525 | if (projection) query = query.project(projection)
3526 | if (skip) query = query.skip(skip)
3527 | if (limit) query = query.limit(limit)
3528 | if (sort) query = query.sort(sort)
3529 | const results = await query.toArray()
3530 | log(`DB Operation: Found ${results.length} documents.`)
3531 | return results
3532 | } catch (error) {
3533 | log(`DB Operation: Failed to find documents: ${error.message}`)
3534 | throw error
3535 | }
3536 | }
3537 |
3538 | const countDocuments = async (collectionName, filter = {}) => {
3539 | log(`DB Operation: Counting documents in collection '${collectionName}'…`)
3540 | try {
3541 | await throwIfCollectionNotExists(collectionName)
3542 | const collection = currentDb.collection(collectionName)
3543 | let count
3544 | try {
3545 | count = await collection.countDocuments(filter)
3546 | } catch (countError) {
3547 | log(`DB Operation: countDocuments not available, falling back to count: ${countError.message}`)
3548 | count = await collection.count(filter)
3549 | }
3550 | log(`DB Operation: Count result: ${count} documents.`)
3551 | return count
3552 | } catch (error) {
3553 | log(`DB Operation: Failed to count documents: ${error.message}`)
3554 | throw error
3555 | }
3556 | }
3557 |
3558 | const insertDocument = async (collectionName, document, options = {}) => {
3559 | log(`DB Operation: Inserting document(s) into collection '${collectionName}'…`)
3560 | try {
3561 | const collection = currentDb.collection(collectionName)
3562 |
3563 | if (Array.isArray(document)) {
3564 | const result = await collection.insertMany(document, options)
3565 |
3566 | if (result && result.ops && Array.isArray(result.ops)) {
3567 | return {
3568 | acknowledged: true,
3569 | insertedCount: result.insertedCount || result.ops.length,
3570 | insertedIds: result.insertedIds || result.ops.reduce((ids, doc, i) => {
3571 | ids[i] = doc._id || 'unknown'
3572 | return ids
3573 | }, {})
3574 | }
3575 | }
3576 |
3577 | if (result === document.length || result === true) return {
3578 | acknowledged: true,
3579 | insertedCount: document.length,
3580 | insertedIds: document.reduce((ids, doc, i) => {
3581 | ids[i] = doc._id || 'unknown'
3582 | return ids
3583 | }, {})
3584 | }
3585 |
3586 | if (result && typeof result.insertedCount === 'number') return result
3587 |
3588 | if (result && result.result && result.result.ok === 1) return {
3589 | acknowledged: true,
3590 | insertedCount: result.result.n || document.length,
3591 | insertedIds: result.insertedIds || {}
3592 | }
3593 |
3594 | return {
3595 | acknowledged: true,
3596 | insertedCount: document.length,
3597 | insertedIds: {}
3598 | }
3599 | }
3600 |
3601 | const result = await collection.insertOne(document, options)
3602 |
3603 | if (result === 1 || result === true) return { acknowledged: true, insertedId: document._id || 'unknown' }
3604 |
3605 | if (result && result.insertedCount === 1)
3606 | return {
3607 | acknowledged: true,
3608 | insertedId: result.insertedId || result.ops?.[0]?._id || document._id
3609 | }
3610 |
3611 | if (result && result.acknowledged && result.insertedId) return result
3612 |
3613 | if (result && result.result && result.result.ok === 1)
3614 | return {
3615 | acknowledged: true,
3616 | insertedId: result.insertedId || document._id,
3617 | insertedCount: result.result.n || 1
3618 | }
3619 |
3620 | const errorMsg = "Insert operation failed or was not acknowledged by MongoDB"
3621 | log(`DB Operation: Document insertion failed: ${errorMsg}`)
3622 | throw new Error(errorMsg)
3623 | } catch (error) {
3624 | log(`DB Operation: Document insertion failed: ${error.message}`)
3625 | throw error
3626 | }
3627 | }
3628 |
3629 | const updateDocument = async (collectionName, filter, update, options = {}) => {
3630 | log(`DB Operation: Updating document(s) in collection '${collectionName}'…`)
3631 | try {
3632 | const collection = currentDb.collection(collectionName)
3633 | const hasUpdateOperators = Object.keys(update).some(key => key.startsWith('$'))
3634 |
3635 | if (!hasUpdateOperators) update = { $set: update }
3636 |
3637 | let result
3638 | if (options.multi === true || options.many === true) {
3639 | result = await collection.updateMany(filter, update, options)
3640 | } else {
3641 | result = await collection.updateOne(filter, update, options)
3642 | }
3643 |
3644 | if (result === 1 || result === true) return { acknowledged: true, matchedCount: 1, modifiedCount: 1 }
3645 |
3646 | if (result && typeof result.modifiedCount === 'number') return result
3647 |
3648 | if (result && result.result && result.result.ok === 1)
3649 | return {
3650 | acknowledged: true,
3651 | matchedCount: result.result.n || 0,
3652 | modifiedCount: result.result.nModified || 0,
3653 | upsertedId: result.upsertedId || null
3654 | }
3655 |
3656 | if (result && result.acknowledged !== false)
3657 | return {
3658 | acknowledged: true,
3659 | matchedCount: result.n || result.matchedCount || 0,
3660 | modifiedCount: result.nModified || result.modifiedCount || 0
3661 | }
3662 |
3663 | const errorMsg = "Update operation failed or was not acknowledged by MongoDB"
3664 | log(`DB Operation: Document update failed: ${errorMsg}`)
3665 | throw new Error(errorMsg)
3666 | } catch (error) {
3667 | log(`DB Operation: Document update failed: ${error.message}`)
3668 | throw error
3669 | }
3670 | }
3671 |
3672 | const deleteDocument = async (collectionName, filter, options = {}) => {
3673 | log(`DB Operation: Deleting document(s) from collection '${collectionName}'…`)
3674 | try {
3675 | const collection = currentDb.collection(collectionName)
3676 |
3677 | let result
3678 | if (options.many === true) {
3679 | result = await collection.deleteMany(filter, options)
3680 | } else {
3681 | result = await collection.deleteOne(filter, options)
3682 | }
3683 |
3684 | if (result === 1 || result === true) return { acknowledged: true, deletedCount: 1 }
3685 | if (result && typeof result.deletedCount === 'number') return result
3686 | if (result && result.result && result.result.ok === 1) return { acknowledged: true, deletedCount: result.result.n || 0 }
3687 | if (result && result.acknowledged !== false) return {acknowledged: true, deletedCount: result.n || 0 }
3688 |
3689 | const errorMsg = "Delete operation failed or was not acknowledged by MongoDB"
3690 | log(`DB Operation: Document deletion failed: ${errorMsg}`)
3691 | throw new Error(errorMsg)
3692 | } catch (error) {
3693 | log(`DB Operation: Document deletion failed: ${error.message}`)
3694 | throw error
3695 | }
3696 | }
3697 |
3698 | const aggregateData = async (collectionName, pipeline) => {
3699 | log(`DB Operation: Running aggregation on collection '${collectionName}'…`)
3700 | try {
3701 | await throwIfCollectionNotExists(collectionName)
3702 | log(`DB Operation: Pipeline has ${pipeline.length} stages.`)
3703 | const collection = currentDb.collection(collectionName)
3704 | const cursor = collection.aggregate(pipeline, { allowDiskUse: config.defaults.allowDiskUse })
3705 | let results
3706 | if (cursor && typeof cursor.toArray === 'function') results = await cursor.toArray()
3707 | else if (cursor && cursor.result) results = cursor.result
3708 | else if (Array.isArray(cursor)) results = cursor
3709 | else results = cursor || []
3710 |
3711 | log(`DB Operation: Aggregation returned ${results.length} results.`)
3712 | return results
3713 | } catch (error) {
3714 | log(`DB Operation: Failed to run aggregation: ${error.message}`)
3715 | throw error
3716 | }
3717 | }
3718 |
3719 | const getDatabaseStats = async () => {
3720 | log(`DB Operation: Getting statistics for database '${currentDbName}'…`)
3721 | const stats = await currentDb.stats()
3722 | log(`DB Operation: Retrieved database statistics.`)
3723 | return stats
3724 | }
3725 |
3726 | const inferSchema = async (collectionName, sampleSize = config.defaults.schemaSampleSize) => {
3727 | log(`DB Operation: Inferring schema for collection '${collectionName}' with sample size ${sampleSize}…`)
3728 | try {
3729 | await throwIfCollectionNotExists(collectionName)
3730 |
3731 | const cacheKey = `${currentDbName}.${collectionName}.${sampleSize}`
3732 | const cachedSchema = getCachedValue('schemas', cacheKey)
3733 | if (cachedSchema) return cachedSchema
3734 |
3735 | const collection = currentDb.collection(collectionName)
3736 |
3737 | const pipeline = [{ $sample: { size: sampleSize } }]
3738 |
3739 | const cursor = collection.aggregate(pipeline, {
3740 | allowDiskUse: config.defaults.allowDiskUse,
3741 | cursor: { batchSize: config.defaults.aggregationBatchSize }
3742 | })
3743 |
3744 | const documents = []
3745 | const fieldPaths = new Set()
3746 | const schema = {}
3747 | let processed = 0
3748 |
3749 | for await (const doc of cursor) {
3750 | documents.push(doc)
3751 | collectFieldPaths(doc, '', fieldPaths)
3752 | processed++
3753 | if (processed % 50 === 0) log(`DB Operation: Processed ${processed} documents for schema inference…`)
3754 | }
3755 |
3756 | log(`DB Operation: Retrieved ${documents.length} sample documents for schema inference.`)
3757 |
3758 | if (documents.length === 0) {
3759 | // Return a minimal schema for empty collections instead of throwing error
3760 | const result = {
3761 | collectionName,
3762 | sampleSize: 0,
3763 | fields: {},
3764 | timestamp: new Date().toISOString(),
3765 | isEmpty: true
3766 | }
3767 |
3768 | setCachedValue('schemas', cacheKey, result)
3769 | setCachedValue('fields', `${currentDbName}.${collectionName}`, [])
3770 |
3771 | log(`DB Operation: Collection '${collectionName}' is empty, returning minimal schema.`)
3772 | return result
3773 | }
3774 |
3775 | fieldPaths.forEach(path => {
3776 | schema[path] = {
3777 | types: new Set(),
3778 | count: 0,
3779 | sample: null,
3780 | path: path
3781 | }
3782 | })
3783 |
3784 | documents.forEach(doc => {
3785 | fieldPaths.forEach(path => {
3786 | const value = getValueAtPath(doc, path)
3787 | if (value !== undefined) {
3788 | if (!schema[path].sample) {
3789 | schema[path].sample = value
3790 | }
3791 | schema[path].types.add(getTypeName(value))
3792 | schema[path].count++
3793 | }
3794 | })
3795 | })
3796 |
3797 | for (const key in schema) {
3798 | schema[key].types = Array.from(schema[key].types)
3799 | schema[key].coverage = Math.round((schema[key].count / documents.length) * 100)
3800 | }
3801 |
3802 | const result = {
3803 | collectionName,
3804 | sampleSize: documents.length,
3805 | fields: schema,
3806 | timestamp: new Date().toISOString()
3807 | }
3808 |
3809 | setCachedValue('schemas', cacheKey, result)
3810 |
3811 | const fieldsArray = Object.keys(schema)
3812 | log(`DB Operation: Schema inference complete, identified ${fieldsArray.length} fields.`)
3813 |
3814 | setCachedValue('fields', `${currentDbName}.${collectionName}`, fieldsArray)
3815 |
3816 | return result
3817 | } catch (error) {
3818 | log(`DB Operation: Failed to infer schema: ${error.message}`)
3819 | throw error
3820 | }
3821 | }
3822 |
3823 | const collectFieldPaths = (obj, prefix = '', paths = new Set()) => {
3824 | if (!obj || typeof obj !== 'object') return
3825 |
3826 | Object.entries(obj).forEach(([key, value]) => {
3827 | const path = prefix ? `${prefix}.${key}` : key
3828 | paths.add(path)
3829 |
3830 | if (value && typeof value === 'object') {
3831 | if (Array.isArray(value)) {
3832 | if (value.length > 0) {
3833 | if (typeof value[0] === 'object' && value[0] !== null) {
3834 | collectFieldPaths(value[0], `${path}[]`, paths)
3835 | }
3836 | }
3837 | } else if (!(value instanceof ObjectId) && !(value instanceof Date)) {
3838 | collectFieldPaths(value, path, paths)
3839 | }
3840 | }
3841 | })
3842 |
3843 | return paths
3844 | }
3845 |
3846 | const getTypeName = (value) => {
3847 | if (value === null) return 'null'
3848 | if (value === undefined) return 'undefined'
3849 | if (Array.isArray(value)) return 'array'
3850 | if (value instanceof ObjectId) return 'ObjectId'
3851 | if (value instanceof Date) return 'Date'
3852 | return typeof value
3853 | }
3854 |
3855 | const createIndex = async (collectionName, keys, options = {}) => {
3856 | log(`DB Operation: Creating index on collection '${collectionName}'…`)
3857 | log(`DB Operation: Index keys: ${JSON.stringify(keys)}`)
3858 | if (Object.keys(options).length > 0) log(`DB Operation: Index options: ${JSON.stringify(options)}`)
3859 | try {
3860 | const collection = currentDb.collection(collectionName)
3861 | const result = await collection.createIndex(keys, options)
3862 | invalidateRelatedCaches(currentDbName, collectionName)
3863 | if (typeof result === 'string') return result
3864 | if (result && result.name) return result.name
3865 | if (result && result.ok === 1) return result.name || 'index'
3866 | const errorMsg = "Index creation did not return a valid index name"
3867 | log(`DB Operation: Index creation failed: ${errorMsg}`)
3868 | throw new Error(errorMsg)
3869 | } catch (error) {
3870 | log(`DB Operation: Index creation failed: ${error.message}`)
3871 | throw error
3872 | }
3873 | }
3874 |
3875 | const dropIndex = async (collectionName, indexName) => {
3876 | log(`DB Operation: Dropping index '${indexName}' from collection '${collectionName}'…`)
3877 | try {
3878 | await throwIfCollectionNotExists(collectionName)
3879 | const collection = currentDb.collection(collectionName)
3880 | invalidateRelatedCaches(currentDbName, collectionName)
3881 | const result = await collection.dropIndex(indexName)
3882 | if (result === true) return true
3883 | if (result && result.ok === 1) return true
3884 | if (result && typeof result === 'object') return true
3885 | if (result === undefined || result === null) return true
3886 | log(`DB Operation: Index dropped with unexpected result: ${JSON.stringify(result)}`)
3887 | return true
3888 | } catch (error) {
3889 | log(`DB Operation: Failed to drop index: ${error.message}`)
3890 | throw error
3891 | }
3892 | }
3893 |
3894 | const explainQuery = async (collectionName, filter, verbosity = 'executionStats') => {
3895 | log(`DB Operation: Explaining query on collection '${collectionName}'…`)
3896 | try {
3897 | await throwIfCollectionNotExists(collectionName)
3898 | const collection = currentDb.collection(collectionName)
3899 | const explanation = await collection.find(filter).explain(verbosity)
3900 | if (!explanation) throw new Error(`Explain operation returned no result`)
3901 | log(`DB Operation: Query explanation generated.`)
3902 | return explanation
3903 | } catch (error) {
3904 | log(`DB Operation: Query explanation failed: ${error.message}`)
3905 | throw error
3906 | }
3907 | }
3908 |
3909 | const getSuggestedProjection = (filter, fields) => {
3910 | if (!fields || fields.length === 0) return null
3911 |
3912 | const filterFields = new Set(Object.keys(filter))
3913 | const commonFields = new Set(['_id', 'name', 'title', 'date', 'createdAt', 'updatedAt'])
3914 |
3915 | const projectionFields = {}
3916 |
3917 | filterFields.forEach(field => {
3918 | if (fields.includes(field)) projectionFields[field] = 1
3919 | })
3920 |
3921 | fields.forEach(field => {
3922 | if (commonFields.has(field)) projectionFields[field] = 1
3923 | })
3924 |
3925 | if (Object.keys(projectionFields).length === 0) return null
3926 |
3927 | return JSON.stringify(projectionFields)
3928 | }
3929 |
3930 | const getServerStatus = async () => {
3931 | log('DB Operation: Getting server status…')
3932 | try {
3933 | const cachedStatus = getCachedValue('serverStatus', 'server_status')
3934 | if (cachedStatus) return cachedStatus
3935 |
3936 | const adminDb = mongoClient.db('admin')
3937 | const status = await adminDb.command({ serverStatus: 1 })
3938 |
3939 | const versionParts = status.version ? status.version.split('.').map(Number) : [0, 0]
3940 | const versionDetails = {
3941 | major: versionParts[0] || 0,
3942 | minor: versionParts[1] || 0,
3943 | isV3OrLower: (versionParts[0] || 0) <= 3,
3944 | isV4: (versionParts[0] || 0) === 4,
3945 | isV5OrHigher: (versionParts[0] || 0) >= 5
3946 | }
3947 |
3948 | const normalizedStatus = { ...status, versionDetails }
3949 |
3950 | if (versionDetails.isV5OrHigher && !normalizedStatus.wiredTiger && normalizedStatus.wiredTiger3) {
3951 | normalizedStatus.wiredTiger = normalizedStatus.wiredTiger3
3952 | }
3953 |
3954 | setCachedValue('serverStatus', 'server_status', normalizedStatus)
3955 |
3956 | log('DB Operation: Retrieved and normalized server status.')
3957 | return normalizedStatus
3958 | } catch (error) {
3959 | log(`DB Operation: Error getting server status: ${error.message}`)
3960 | return {
3961 | host: mongoClient.s?.options?.host || mongoClient.s?.options?.hosts?.[0]?.host || 'unknown',
3962 | port: mongoClient.s?.options?.port || mongoClient.s?.options?.hosts?.[0]?.port || 'unknown',
3963 | version: 'Information unavailable',
3964 | error: error.message
3965 | }
3966 | }
3967 | }
3968 |
3969 | const getReplicaSetStatus = async () => {
3970 | log('DB Operation: Getting replica set status…')
3971 | try {
3972 | const adminDb = mongoClient.db('admin')
3973 | const status = await adminDb.command({ replSetGetStatus: 1 })
3974 | log('DB Operation: Retrieved replica set status.')
3975 | return status
3976 | } catch (error) {
3977 | log(`DB Operation: Error getting replica set status: ${error.message}`)
3978 | return {
3979 | isReplicaSet: false,
3980 | info: 'This server is not part of a replica set or you may not have permissions to view replica set status.',
3981 | error: error.message,
3982 | replicaSetRequired: true
3983 | }
3984 | }
3985 | }
3986 |
3987 | const getCollectionValidation = async (collectionName) => {
3988 | log(`DB Operation: Getting validation rules for collection '${collectionName}'…`)
3989 | try {
3990 | await throwIfCollectionNotExists(collectionName)
3991 | const collections = await currentDb.listCollections({ name: collectionName }, { validator: 1 }).toArray()
3992 | log(`DB Operation: Retrieved validation information for collection '${collectionName}'.`)
3993 | if (collections.length === 0) return { hasValidation: false }
3994 | return {
3995 | hasValidation: !!collections[0].options?.validator,
3996 | validator: collections[0].options?.validator || {},
3997 | validationLevel: collections[0].options?.validationLevel || 'strict',
3998 | validationAction: collections[0].options?.validationAction || 'error'
3999 | }
4000 | } catch (error) {
4001 | log(`DB Operation: Error getting validation for ${collectionName}: ${error.message}`)
4002 | throw error
4003 | }
4004 | }
4005 |
4006 | const getCollectionFields = async (collectionName) => {
4007 | const cacheKey = `${currentDbName}.${collectionName}`
4008 | const cachedFields = getCachedValue('fields', cacheKey)
4009 | if (cachedFields) return cachedFields
4010 | const schema = await inferSchema(collectionName, 10)
4011 | const fieldsArray = Object.keys(schema.fields)
4012 | setCachedValue('fields', cacheKey, fieldsArray)
4013 | return fieldsArray
4014 | }
4015 |
4016 | const setProfilerSlowMs = async () => {
4017 | try {
4018 | await currentDb.command({ profile: -1 })
4019 | await currentDb.command({ profile: 0, slowms: config.defaults.slowMs })
4020 | log(`DB Operation: Set slow query threshold to ${config.defaults.slowMs}ms`)
4021 | return true
4022 | } catch (error) {
4023 | log(`Error setting profiler: ${error.message}`)
4024 | return false
4025 | }
4026 | }
4027 |
4028 | const getDatabaseUsers = async () => {
4029 | log(`DB Operation: Getting users for database '${currentDbName}'…`)
4030 | try {
4031 | const users = await currentDb.command({ usersInfo: 1 })
4032 | log(`DB Operation: Retrieved user information.`)
4033 | return users
4034 | } catch (error) {
4035 | log(`DB Operation: Error getting users: ${error.message}`)
4036 | return {
4037 | users: [],
4038 | info: 'Could not retrieve user information. You may not have sufficient permissions.',
4039 | error: error.message
4040 | }
4041 | }
4042 | }
4043 |
4044 | const getStoredFunctions = async () => {
4045 | log(`DB Operation: Getting stored JavaScript functions…`)
4046 | try {
4047 | const system = currentDb.collection('system.js')
4048 | const functions = await system.find({}).toArray()
4049 | log(`DB Operation: Retrieved ${functions.length} stored functions.`)
4050 | return functions
4051 | } catch (error) {
4052 | log(`DB Operation: Error getting stored functions: ${error.message}`)
4053 | return []
4054 | }
4055 | }
4056 |
4057 | const getPerformanceMetrics = async () => {
4058 | try {
4059 | await setProfilerSlowMs()
4060 |
4061 | const adminDb = mongoClient.db('admin')
4062 | const serverStatus = await adminDb.command({ serverStatus: 1 })
4063 | const profileStats = await currentDb.command({ profile: -1 })
4064 |
4065 | const currentOps = await adminDb.command({
4066 | currentOp: 1,
4067 | active: true,
4068 | secs_running: { $gt: 1 }
4069 | })
4070 |
4071 | const perfStats = await currentDb.command({ dbStats: 1 })
4072 |
4073 | const slowQueries = await currentDb.collection('system.profile')
4074 | .find({ millis: { $gt: config.defaults.slowMs } })
4075 | .sort({ ts: -1 })
4076 | .limit(10)
4077 | .toArray()
4078 |
4079 | return {
4080 | serverStatus: {
4081 | connections: serverStatus.connections,
4082 | network: serverStatus.network,
4083 | opcounters: serverStatus.opcounters,
4084 | wiredTiger: serverStatus.wiredTiger?.cache,
4085 | mem: serverStatus.mem,
4086 | locks: serverStatus.locks
4087 | },
4088 | profileSettings: profileStats,
4089 | currentOperations: currentOps.inprog,
4090 | performance: perfStats,
4091 | slowQueries
4092 | }
4093 | } catch (error) {
4094 | log(`Error getting performance metrics: ${error.message}`)
4095 | return { error: error.message }
4096 | }
4097 | }
4098 |
4099 | const getDatabaseTriggers = async () => {
4100 | try {
4101 | try {
4102 | const coll = currentDb.collection('system.version')
4103 | const testStream = coll.watch()
4104 | await testStream.close()
4105 |
4106 | const changeStreamInfo = {
4107 | supported: true,
4108 | resumeTokenSupported: true,
4109 | updateLookupSupported: true,
4110 | fullDocumentBeforeChangeSupported: true
4111 | }
4112 |
4113 | const triggerCollections = await currentDb.listCollections({ name: /trigger|event|notification/i }).toArray()
4114 | const system = currentDb.collection('system.js')
4115 | const triggerFunctions = await system.find({ _id: /trigger|event|watch|notify/i }).toArray()
4116 |
4117 | return {
4118 | changeStreams: changeStreamInfo,
4119 | triggerCollections,
4120 | triggerFunctions
4121 | }
4122 | } catch (error) {
4123 | if (error.code === 40573 || error.message.includes('only supported on replica sets')) {
4124 | return {
4125 | changeStreams: {
4126 | supported: false,
4127 | reason: "Change streams require a replica set or sharded cluster",
4128 | howToEnable: "To enable change streams for development, configure MongoDB as a single-node replica set"
4129 | },
4130 | triggerCollections: [],
4131 | triggerFunctions: []
4132 | }
4133 | }
4134 | throw error
4135 | }
4136 | } catch (error) {
4137 | log(`Error getting database triggers: ${error.message}`)
4138 | return {
4139 | error: error.message,
4140 | supported: false
4141 | }
4142 | }
4143 | }
4144 |
4145 | const getDistinctValues = async (collectionName, field, filter = {}) => {
4146 | log(`DB Operation: Getting distinct values for field '${field}' in collection '${collectionName}'…`)
4147 | try {
4148 | await throwIfCollectionNotExists(collectionName)
4149 | if (!isValidFieldName(field)) throw new Error(`Invalid field name: ${field}`)
4150 | const collection = currentDb.collection(collectionName)
4151 | const values = await collection.distinct(field, filter)
4152 | log(`DB Operation: Found ${values.length} distinct values.`)
4153 | return values
4154 | } catch (error) {
4155 | log(`DB Operation: Failed to get distinct values: ${error.message}`)
4156 | throw error
4157 | }
4158 | }
4159 |
4160 | const isValidFieldName = (field) =>
4161 | typeof field === 'string' && field.length > 0 && !field.startsWith('$')
4162 |
4163 | const bulkOperations = async (collectionName, operations, ordered = config.tools.bulkOperations.ordered) => {
4164 | log(`DB Operation: Performing bulk operations on collection '${collectionName}'…`)
4165 | try {
4166 | await throwIfCollectionNotExists(collectionName)
4167 | const collection = currentDb.collection(collectionName)
4168 |
4169 | let bulk
4170 | try {
4171 | bulk = ordered ? collection.initializeOrderedBulkOp() : collection.initializeUnorderedBulkOp()
4172 | } catch (bulkError) {
4173 | log(`DB Operation: Modern bulk API unavailable, trying legacy method: ${bulkError.message}`)
4174 | if (typeof collection.bulkWrite === 'function') {
4175 | const result = await collection.bulkWrite(operations, { ordered })
4176 | log(`DB Operation: Bulk operations complete (using bulkWrite).`)
4177 | return normalizeBulkResult(result)
4178 | } else {
4179 | throw new Error('Bulk operations not supported by this MongoDB version/driver')
4180 | }
4181 | }
4182 |
4183 | for (const op of operations) {
4184 | if (op.insertOne) bulk.insert(op.insertOne.document)
4185 | else if (op.updateOne) bulk.find(op.updateOne.filter).updateOne(op.updateOne.update)
4186 | else if (op.updateMany) bulk.find(op.updateMany.filter).update(op.updateMany.update)
4187 | else if (op.deleteOne) bulk.find(op.deleteOne.filter).deleteOne()
4188 | else if (op.deleteMany) bulk.find(op.deleteMany.filter).delete()
4189 | else if (op.replaceOne) bulk.find(op.replaceOne.filter).replaceOne(op.replaceOne.replacement)
4190 | }
4191 |
4192 | const result = await bulk.execute()
4193 | log(`DB Operation: Bulk operations complete.`)
4194 | return normalizeBulkResult(result)
4195 | } catch (error) {
4196 | log(`DB Operation: Bulk operations failed: ${error.message}`)
4197 | throw error
4198 | }
4199 | }
4200 |
4201 | const normalizeBulkResult = (result) => {
4202 | if (!result) return { acknowledged: false }
4203 |
4204 | if (typeof result.insertedCount === 'number' ||
4205 | typeof result.matchedCount === 'number' ||
4206 | typeof result.deletedCount === 'number') {
4207 | return {
4208 | acknowledged: true,
4209 | insertedCount: result.insertedCount || 0,
4210 | matchedCount: result.matchedCount || 0,
4211 | modifiedCount: result.modifiedCount || 0,
4212 | deletedCount: result.deletedCount || 0,
4213 | upsertedCount: result.upsertedCount || 0,
4214 | upsertedIds: result.upsertedIds || {},
4215 | insertedIds: result.insertedIds || {}
4216 | }
4217 | }
4218 |
4219 | if (result.ok === 1 || (result.result && result.result.ok === 1)) {
4220 | const nInserted = result.nInserted || result.result?.nInserted || 0
4221 | const nMatched = result.nMatched || result.result?.nMatched || 0
4222 | const nModified = result.nModified || result.result?.nModified || 0
4223 | const nUpserted = result.nUpserted || result.result?.nUpserted || 0
4224 | const nRemoved = result.nRemoved || result.result?.nRemoved || 0
4225 |
4226 | return {
4227 | acknowledged: true,
4228 | insertedCount: nInserted,
4229 | matchedCount: nMatched,
4230 | modifiedCount: nModified,
4231 | deletedCount: nRemoved,
4232 | upsertedCount: nUpserted,
4233 | upsertedIds: result.upserted || result.result?.upserted || {},
4234 | insertedIds: {}
4235 | }
4236 | }
4237 |
4238 | if (typeof result === 'number') {
4239 | return {
4240 | acknowledged: true,
4241 | insertedCount: 0,
4242 | matchedCount: 0,
4243 | modifiedCount: 0,
4244 | deletedCount: 0,
4245 | upsertedCount: 0,
4246 | result: { n: result }
4247 | }
4248 | }
4249 |
4250 | return { acknowledged: false }
4251 | }
4252 |
4253 | const getShardingDbStatus = async (dbName) => {
4254 | try {
4255 | const config = mongoClient.db('config')
4256 | return await config.collection('databases').findOne({ _id: dbName })
4257 | } catch (error) {
4258 | log(`Error getting database sharding status: ${error.message}`)
4259 | return null
4260 | }
4261 | }
4262 |
4263 | const getShardingCollectionStatus = async (dbName, collName) => {
4264 | try {
4265 | const config = mongoClient.db('config')
4266 | return await config.collection('collections').findOne({ _id: `${dbName}.${collName}` })
4267 | } catch (error) {
4268 | log(`Error getting collection sharding status: ${error.message}`)
4269 | return null
4270 | }
4271 | }
4272 |
4273 | const formatDatabasesList = (databases) => {
4274 | return `Databases (${databases.length}):\n` +
4275 | databases.map(db => `- ${db.name} (${formatSize(db.sizeOnDisk)})`).join('\n')
4276 | }
4277 |
4278 | const formatCollectionsList = (collections) => {
4279 | if (!collections || collections.length === 0) {
4280 | return `No collections found in database '${currentDbName}'`
4281 | }
4282 |
4283 | return `Collections in ${currentDbName} (${collections.length}):\n` +
4284 | collections.map(coll => `- ${coll.name} (${coll.type})`).join('\n')
4285 | }
4286 |
4287 | const formatDocuments = (documents, limit) => {
4288 | if (!documents || documents.length === 0) {
4289 | return 'No documents found'
4290 | }
4291 |
4292 | const count = documents.length
4293 | let result = `${count} document${count === 1 ? '' : 's'}`
4294 | if (count === limit) {
4295 | result += ` (limit: ${limit})`
4296 | }
4297 | result += ':\n'
4298 |
4299 | result += documents.map(doc => JSON.stringify(serializeDocument(doc), null, 2)).join('\n\n')
4300 | return result
4301 | }
4302 |
4303 | const formatSchema = (schema) => {
4304 | const { collectionName, sampleSize, fields } = schema
4305 | let result = `Schema for '${collectionName}' (sampled ${sampleSize} documents):\n`
4306 |
4307 | for (const [field, info] of Object.entries(fields)) {
4308 | const types = info.types.join(' | ')
4309 | const coverage = Math.round((info.count / sampleSize) * 100)
4310 | let sample = ''
4311 |
4312 | if (info.sample !== null && info.sample !== undefined) {
4313 | if (typeof info.sample === 'object') {
4314 | sample = JSON.stringify(serializeDocument(info.sample))
4315 | } else {
4316 | sample = String(info.sample)
4317 | }
4318 |
4319 | if (sample.length > 50) {
4320 | sample = sample.substring(0, 47) + '…'
4321 | }
4322 |
4323 | sample = ` (example: ${sample})`
4324 | }
4325 |
4326 | result += `- ${field}: ${types} (${coverage}% coverage)${sample}\n`
4327 | }
4328 |
4329 | return result
4330 | }
4331 |
4332 | const formatStats = (stats) => {
4333 | if (!stats) return 'No statistics available'
4334 |
4335 | const keyMetrics = [
4336 | ['ns', 'Namespace'],
4337 | ['count', 'Document Count'],
4338 | ['size', 'Data Size'],
4339 | ['avgObjSize', 'Average Object Size'],
4340 | ['storageSize', 'Storage Size'],
4341 | ['totalIndexSize', 'Total Index Size'],
4342 | ['nindexes', 'Number of Indexes']
4343 | ]
4344 |
4345 | let result = 'Statistics:\n'
4346 |
4347 | for (const [key, label] of keyMetrics) {
4348 | if (stats[key] !== undefined) {
4349 | let value = stats[key]
4350 | if (key.toLowerCase().includes('size')) {
4351 | value = formatSize(value)
4352 | }
4353 | result += `- ${label}: ${value}\n`
4354 | }
4355 | }
4356 |
4357 | return result
4358 | }
4359 |
4360 | const formatIndexes = (indexes) => {
4361 | if (!indexes || indexes.length === 0) {
4362 | return 'No indexes found'
4363 | }
4364 |
4365 | let result = `Indexes (${indexes.length}):\n`
4366 |
4367 | for (const idx of indexes) {
4368 | const keys = Object.entries(idx.key)
4369 | .map(([field, direction]) => `${field}: ${direction}`)
4370 | .join(', ')
4371 |
4372 | result += `- ${idx.name}: { ${keys} }`
4373 |
4374 | if (idx.unique) result += ' (unique)'
4375 | if (idx.sparse) result += ' (sparse)'
4376 | if (idx.background) result += ' (background)'
4377 |
4378 | result += '\n'
4379 | }
4380 |
4381 | return result
4382 | }
4383 |
4384 | const formatExplanation = (explanation) => {
4385 | let result = 'Query Explanation:\n'
4386 |
4387 | if (explanation.queryPlanner) {
4388 | result += '\nQuery Planner:\n'
4389 | result += `- Namespace: ${explanation.queryPlanner.namespace}\n`
4390 | result += `- Index Filter: ${JSON.stringify(explanation.queryPlanner.indexFilterSet) || 'None'}\n`
4391 | result += `- Winning Plan: ${JSON.stringify(explanation.queryPlanner.winningPlan, null, 2)}\n`
4392 | }
4393 |
4394 | if (explanation.executionStats) {
4395 | result += '\nExecution Stats:\n'
4396 | result += `- Execution Success: ${explanation.executionStats.executionSuccess}\n`
4397 | result += `- Documents Examined: ${explanation.executionStats.totalDocsExamined}\n`
4398 | result += `- Keys Examined: ${explanation.executionStats.totalKeysExamined}\n`
4399 | result += `- Execution Time: ${explanation.executionStats.executionTimeMillis}ms\n`
4400 | }
4401 |
4402 | return result
4403 | }
4404 |
4405 | const formatServerStatus = (status) => {
4406 | if (!status) return 'Server status information not available'
4407 |
4408 | let result = 'MongoDB Server Status:\n'
4409 |
4410 | if (status.error) {
4411 | result += `Note: Limited information available. ${status.error}\n\n`
4412 | }
4413 |
4414 | result += '## Server Information\n'
4415 | result += `- Host: ${status.host || 'Unknown'}\n`
4416 | result += `- Version: ${status.version || 'Unknown'}\n`
4417 | result += `- Process: ${status.process || 'Unknown'}\n`
4418 | result += `- Uptime: ${formatUptime(status.uptime)}\n`
4419 |
4420 | if (status.connections) {
4421 | result += '\n## Connections\n'
4422 | result += `- Current: ${status.connections.current}\n`
4423 | result += `- Available: ${status.connections.available}\n`
4424 | result += `- Total Created: ${status.connections.totalCreated}\n`
4425 | }
4426 |
4427 | if (status.mem) {
4428 | result += '\n## Memory Usage\n'
4429 | result += `- Resident: ${formatSize(status.mem.resident * 1024 * 1024)}\n`
4430 | result += `- Virtual: ${formatSize(status.mem.virtual * 1024 * 1024)}\n`
4431 | result += `- Page Faults: ${status.extra_info?.page_faults || 'N/A'}\n`
4432 | }
4433 |
4434 | if (status.opcounters) {
4435 | result += '\n## Operation Counters\n'
4436 | result += `- Insert: ${status.opcounters.insert}\n`
4437 | result += `- Query: ${status.opcounters.query}\n`
4438 | result += `- Update: ${status.opcounters.update}\n`
4439 | result += `- Delete: ${status.opcounters.delete}\n`
4440 | result += `- Getmore: ${status.opcounters.getmore}\n`
4441 | result += `- Command: ${status.opcounters.command}\n`
4442 | }
4443 |
4444 | return result
4445 | }
4446 |
4447 | const formatReplicaSetStatus = (status) => {
4448 | if (!status) return 'Replica set status information not available'
4449 |
4450 | if (status.error) {
4451 | if (status.replicaSetRequired) {
4452 | return `Replica Set Status: Not available\n\n${status.info}\n\nYou can set up a single-node replica set for development purposes by following these steps:\n\n1. Stop your MongoDB server\n2. Start it with the --replSet option: \`mongod --replSet rs0\`\n3. Connect to it and initialize the replica set: \`rs.initiate()\``
4453 | }
4454 | return `Replica Set Status: Not available (${status.info})\n\n${status.error}`
4455 | }
4456 |
4457 | let result = `Replica Set: ${status.set}\n`
4458 | result += `Status: ${status.myState === 1 ? 'PRIMARY' : status.myState === 2 ? 'SECONDARY' : 'OTHER'}\n`
4459 | result += `Current Time: ${new Date(status.date.$date || status.date).toISOString()}\n\n`
4460 |
4461 | result += '## Members:\n'
4462 | if (status.members) {
4463 | for (const member of status.members) {
4464 | result += `- ${member.name} (${member.stateStr})\n`
4465 | result += ` Health: ${member.health}\n`
4466 | result += ` Uptime: ${formatUptime(member.uptime)}\n`
4467 | if (member.syncingTo) {
4468 | result += ` Syncing to: ${member.syncingTo}\n`
4469 | }
4470 | result += '\n'
4471 | }
4472 | }
4473 |
4474 | return result
4475 | }
4476 |
4477 | const formatValidationRules = (validation) => {
4478 | if (!validation) return 'Validation information not available'
4479 |
4480 | if (!validation.hasValidation) {
4481 | return 'This collection does not have any validation rules configured.'
4482 | }
4483 |
4484 | let result = 'Collection Validation Rules:\n'
4485 | result += `- Validation Level: ${validation.validationLevel}\n`
4486 | result += `- Validation Action: ${validation.validationAction}\n\n`
4487 |
4488 | result += 'Validator:\n'
4489 | result += JSON.stringify(validation.validator, null, 2)
4490 |
4491 | return result
4492 | }
4493 |
4494 | const formatDatabaseUsers = (usersInfo) => {
4495 | if (!usersInfo) return 'User information not available'
4496 |
4497 | if (usersInfo.error) {
4498 | return `Users: Not available\n\n${usersInfo.info}\n${usersInfo.error}`
4499 | }
4500 |
4501 | const users = usersInfo.users || []
4502 | if (users.length === 0) {
4503 | return 'No users found in the current database.'
4504 | }
4505 |
4506 | let result = `Users in database '${currentDbName}' (${users.length}):\n\n`
4507 |
4508 | for (const user of users) {
4509 | result += `## ${user.user}${user.customData ? ' (' + JSON.stringify(user.customData) + ')' : ''}\n`
4510 | result += `- User ID: ${user._id || 'N/A'}\n`
4511 | result += `- Database: ${user.db}\n`
4512 |
4513 | if (user.roles && user.roles.length > 0) {
4514 | result += '- Roles:\n'
4515 | for (const role of user.roles) {
4516 | result += ` - ${role.role} on ${role.db}\n`
4517 | }
4518 | } else {
4519 | result += '- Roles: None\n'
4520 | }
4521 |
4522 | result += '\n'
4523 | }
4524 |
4525 | return result
4526 | }
4527 |
4528 | const formatStoredFunctions = (functions) => {
4529 | if (!functions || !Array.isArray(functions)) return 'Stored functions information not available'
4530 |
4531 | if (functions.length === 0) {
4532 | return 'No stored JavaScript functions found in the current database.'
4533 | }
4534 |
4535 | let result = `Stored Functions in database '${currentDbName}' (${functions.length}):\n\n`
4536 |
4537 | for (const func of functions) {
4538 | result += `## ${func._id}\n`
4539 |
4540 | if (typeof func.value === 'function') {
4541 | result += `${func.value.toString()}\n\n`
4542 | } else {
4543 | result += `${func.value}\n\n`
4544 | }
4545 | }
4546 |
4547 | return result
4548 | }
4549 |
4550 | const formatDistinctValues = (field, values) => {
4551 | if (!values || !Array.isArray(values)) return `No distinct values found for field '${field}'`
4552 | let result = `Distinct values for field '${field}' (${values.length}):\n`
4553 | for (const value of values) result += `- ${formatValue(value)}\n`
4554 | return result
4555 | }
4556 |
4557 | const formatValidationResults = (results) => {
4558 | if (!results) return 'Validation results not available'
4559 |
4560 | let result = 'Collection Validation Results:\n'
4561 | result += `- Collection: ${results.ns}\n`
4562 | result += `- Valid: ${results.valid}\n`
4563 |
4564 | if (results.errors && results.errors.length > 0) {
4565 | result += `- Errors: ${results.errors}\n`
4566 | }
4567 |
4568 | if (results.warnings && results.warnings.length > 0) {
4569 | result += `- Warnings: ${results.warnings}\n`
4570 | }
4571 |
4572 | if (results.nrecords !== undefined) {
4573 | result += `- Records Validated: ${results.nrecords}\n`
4574 | }
4575 |
4576 | if (results.nInvalidDocuments !== undefined) {
4577 | result += `- Invalid Documents: ${results.nInvalidDocuments}\n`
4578 | }
4579 |
4580 | if (results.advice) result += `- Advice: ${results.advice}\n`
4581 |
4582 | return result
4583 | }
4584 |
4585 | const formatInsertResult = (result) => {
4586 | if (!result) return 'Insert operation result not available'
4587 | let output = 'Document inserted successfully\n'
4588 | output += `- ID: ${result.insertedId}\n`
4589 | output += `- Acknowledged: ${result.acknowledged}\n`
4590 | return output
4591 | }
4592 |
4593 | const formatUpdateResult = (result) => {
4594 | if (!result) return 'Update operation result not available'
4595 | let output = 'Document update operation complete\n'
4596 | output += `- Matched: ${result.matchedCount}\n`
4597 | output += `- Modified: ${result.modifiedCount}\n`
4598 | output += `- Acknowledged: ${result.acknowledged}\n`
4599 | if (result.upsertedId) output += `- Upserted ID: ${result.upsertedId}\n`
4600 | return output
4601 | }
4602 |
4603 | const formatBulkResult = (result) => {
4604 | if (!result) return 'Bulk operation results not available'
4605 |
4606 | let output = 'Bulk Operations Results:\n'
4607 | output += `- Acknowledged: ${result.acknowledged}\n`
4608 |
4609 | if (result.insertedCount) output += `- Inserted: ${result.insertedCount}\n`
4610 | if (result.matchedCount) output += `- Matched: ${result.matchedCount}\n`
4611 | if (result.modifiedCount) output += `- Modified: ${result.modifiedCount}\n`
4612 | if (result.deletedCount) output += `- Deleted: ${result.deletedCount}\n`
4613 | if (result.upsertedCount) output += `- Upserted: ${result.upsertedCount}\n`
4614 |
4615 | if (result.insertedIds && Object.keys(result.insertedIds).length > 0) {
4616 | output += '- Inserted IDs:\n'
4617 | for (const [index, id] of Object.entries(result.insertedIds)) {
4618 | output += ` - Index ${index}: ${id}\n`
4619 | }
4620 | }
4621 |
4622 | if (result.upsertedIds && Object.keys(result.upsertedIds).length > 0) {
4623 | output += '- Upserted IDs:\n'
4624 | for (const [index, id] of Object.entries(result.upsertedIds)) {
4625 | output += ` - Index ${index}: ${id}\n`
4626 | }
4627 | }
4628 |
4629 | return output
4630 | }
4631 |
4632 | const formatSize = (sizeInBytes) => {
4633 | if (sizeInBytes < 1024) return `${sizeInBytes} bytes`
4634 | if (sizeInBytes < 1024 * 1024) return `${(sizeInBytes / 1024).toFixed(2)} KB`
4635 | if (sizeInBytes < 1024 * 1024 * 1024) return `${(sizeInBytes / (1024 * 1024)).toFixed(2)} MB`
4636 | return `${(sizeInBytes / (1024 * 1024 * 1024)).toFixed(2)} GB`
4637 | }
4638 |
4639 | const formatUptime = (seconds) => {
4640 | if (seconds === undefined) return 'Unknown'
4641 | const days = Math.floor(seconds / 86400)
4642 | const hours = Math.floor((seconds % 86400) / 3600)
4643 | const minutes = Math.floor((seconds % 3600) / 60)
4644 | const remainingSeconds = Math.floor(seconds % 60)
4645 | const parts = []
4646 | if (days > 0) parts.push(`${days}d`)
4647 | if (hours > 0) parts.push(`${hours}h`)
4648 | if (minutes > 0) parts.push(`${minutes}m`)
4649 | if (remainingSeconds > 0 || parts.length === 0) parts.push(`${remainingSeconds}s`)
4650 | return parts.join(' ')
4651 | }
4652 |
4653 | const formatValue = (value) => {
4654 | if (value === null) return 'null'
4655 | if (value === undefined) return 'undefined'
4656 | if (typeof value === 'object') return JSON.stringify(value)
4657 | return String(value)
4658 | }
4659 |
4660 | const formatChangeStreamResults = (changes, duration) => {
4661 | if (changes.length === 0) {
4662 | return `No changes detected during ${duration} second window.`
4663 | }
4664 |
4665 | let result = `Detected ${changes.length} changes during ${duration} second window:\n\n`
4666 |
4667 | for (const change of changes) {
4668 | result += `Operation: ${change.operationType}\n`
4669 | result += `Timestamp: ${new Date(change.clusterTime.high * 1000).toISOString()}\n`
4670 |
4671 | if (change.documentKey) {
4672 | result += `Document ID: ${JSON.stringify(change.documentKey)}\n`
4673 | }
4674 |
4675 | if (change.fullDocument) {
4676 | result += `Document: ${JSON.stringify(change.fullDocument, null, 2)}\n`
4677 | }
4678 |
4679 | if (change.updateDescription) {
4680 | result += `Updated Fields: ${JSON.stringify(change.updateDescription.updatedFields, null, 2)}\n`
4681 | result += `Removed Fields: ${JSON.stringify(change.updateDescription.removedFields)}\n`
4682 | }
4683 |
4684 | result += '\n'
4685 | }
4686 |
4687 | return result
4688 | }
4689 |
4690 | const formatTextSearchResults = (results, searchText) => {
4691 | if (results.length === 0) {
4692 | return `No documents found matching: "${searchText}"`
4693 | }
4694 |
4695 | let output = `Found ${results.length} documents matching: "${searchText}"\n\n`
4696 |
4697 | for (const doc of results) {
4698 | const score = doc.score
4699 | delete doc.score
4700 | output += `Score: ${score.toFixed(2)}\n`
4701 | output += `Document: ${JSON.stringify(serializeDocument(doc), null, 2)}\n\n`
4702 | }
4703 |
4704 | output += `Note: Make sure text indexes exist on relevant fields. Create with: create-index {"collection": "yourCollection", "keys": "{\\"fieldName\\": \\"text\\"}"}`
4705 |
4706 | return output
4707 | }
4708 |
4709 | const formatTransactionResults = (results) => {
4710 | let output = 'Transaction completed successfully:\n\n'
4711 |
4712 | for (const result of results) {
4713 | output += `Step ${result.step}: ${result.operation}\n`
4714 |
4715 | if (result.operation === 'insert') {
4716 | output += `- Inserted ID: ${result.result.insertedId}\n`
4717 | } else if (result.operation === 'update') {
4718 | output += `- Matched: ${result.result.matchedCount}\n`
4719 | output += `- Modified: ${result.result.modifiedCount}\n`
4720 | } else if (result.operation === 'delete') {
4721 | output += `- Deleted: ${result.result.deletedCount}\n`
4722 | } else if (result.operation === 'find') {
4723 | output += `- Document: ${JSON.stringify(serializeDocument(result.result), null, 2)}\n`
4724 | }
4725 |
4726 | output += '\n'
4727 | }
4728 |
4729 | return output
4730 | }
4731 |
4732 | const formatGridFSList = (files) => {
4733 | if (files.length === 0) return 'No files found in GridFS'
4734 |
4735 | let result = `GridFS Files (${files.length}):\n\n`
4736 |
4737 | for (const file of files) {
4738 | result += `Filename: ${file.filename}\n`
4739 | result += `Size: ${formatSize(file.length)}\n`
4740 | result += `Upload Date: ${file.uploadDate.toISOString()}\n`
4741 | result += `ID: ${file._id}\n`
4742 | if (file.metadata) result += `Metadata: ${JSON.stringify(file.metadata)}\n`
4743 | result += '\n'
4744 | }
4745 |
4746 | return result
4747 | }
4748 |
4749 | const getFileId = async (bucket, filename) => {
4750 | const file = await currentDb.collection(`${bucket}.files`).findOne({ filename })
4751 | if (!file) throw new Error(`File '${filename}' not found`)
4752 | return file._id
4753 | }
4754 |
4755 | const formatGridFSInfo = (file) => {
4756 | let result = 'GridFS File Information:\n\n'
4757 | result += `Filename: ${file.filename}\n`
4758 | result += `Size: ${formatSize(file.length)}\n`
4759 | result += `Chunk Size: ${formatSize(file.chunkSize)}\n`
4760 | result += `Upload Date: ${file.uploadDate.toISOString()}\n`
4761 | result += `ID: ${file._id}\n`
4762 | result += `MD5: ${file.md5}\n`
4763 | if (file.contentType) result += `Content Type: ${file.contentType}\n`
4764 | if (file.aliases && file.aliases.length > 0) result += `Aliases: ${file.aliases.join(', ')}\n`
4765 | if (file.metadata) result += `Metadata: ${JSON.stringify(file.metadata, null, 2)}\n`
4766 | return result
4767 | }
4768 |
4769 | const formatCollationResults = (results, locale, strength, caseLevel) => {
4770 | if (results.length === 0) return 'No documents found matching the query with the specified collation'
4771 | let output = `Found ${results.length} documents using collation:\n`
4772 | output += `- Locale: ${locale}\n`
4773 | output += `- Strength: ${strength} (${getStrengthDescription(strength)})\n`
4774 | output += `- Case Level: ${caseLevel}\n\n`
4775 | output += results.map(doc => JSON.stringify(serializeDocument(doc), null, 2)).join('\n\n')
4776 | return output
4777 | }
4778 |
4779 | const getStrengthDescription = (strength) => {
4780 | const descriptions = {
4781 | 1: 'Primary - base characters only',
4782 | 2: 'Secondary - base + accents',
4783 | 3: 'Tertiary - base + accents + case + variants',
4784 | 4: 'Quaternary - base + accents + case + variants + punctuation',
4785 | 5: 'Identical - exact matches only'
4786 | }
4787 | return descriptions[strength] || 'Custom'
4788 | }
4789 |
4790 | const formatShardDbStatus = (shards, dbStats, dbShardStatus, dbName) => {
4791 | let result = `Sharding Status for Database: ${dbName}\n\n`
4792 |
4793 | if (!shards || !shards.shards || shards.shards.length === 0) {
4794 | return result + 'This MongoDB deployment is not a sharded cluster.'
4795 | }
4796 |
4797 | result += `Cluster consists of ${shards.shards.length} shards:\n`
4798 | for (const shard of shards.shards) {
4799 | result += `- ${shard._id}: ${shard.host}\n`
4800 | }
4801 | result += '\n'
4802 |
4803 | if (dbShardStatus) {
4804 | result += `Database Sharding Status: ${dbShardStatus.partitioned ? 'Enabled' : 'Not Enabled'}\n`
4805 | if (dbShardStatus.primary) result += `Primary Shard: ${dbShardStatus.primary}\n\n`
4806 | } else {
4807 | result += 'Database is not sharded.\n\n'
4808 | }
4809 |
4810 | if (dbStats && dbStats.raw) {
4811 | result += 'Data Distribution:\n'
4812 | for (const shard in dbStats.raw) {
4813 | result += `- ${shard}: ${formatSize(dbStats.raw[shard].totalSize)} (${dbStats.raw[shard].objects} objects)\n`
4814 | }
4815 | }
4816 |
4817 | return result
4818 | }
4819 |
4820 | const formatShardCollectionStatus = (stats, shardStatus, collName) => {
4821 | let result = `Sharding Status for Collection: ${collName}\n\n`
4822 |
4823 | if (!stats.sharded) {
4824 | return result + 'This collection is not sharded.'
4825 | }
4826 |
4827 | result += 'Collection is sharded.\n\n'
4828 |
4829 | if (shardStatus) {
4830 | result += `Shard Key: ${JSON.stringify(shardStatus.key)}\n`
4831 | if (shardStatus.unique) result += 'Unique: true\n'
4832 | result += `Distribution Mode: ${shardStatus.dropped ? 'Dropped' : shardStatus.distributionMode || 'hashed'}\n\n`
4833 | }
4834 |
4835 | if (stats.shards) {
4836 | result += 'Data Distribution:\n'
4837 | for (const shard in stats.shards) {
4838 | result += `- ${shard}: ${formatSize(stats.shards[shard].size)} (${stats.shards[shard].count} documents)\n`
4839 | }
4840 | }
4841 |
4842 | if (stats.chunks) {
4843 | result += `\nTotal Chunks: ${stats.chunks}\n`
4844 | }
4845 |
4846 | return result
4847 | }
4848 |
4849 | const formatPerformanceMetrics = (metrics) => {
4850 | let result = 'MongoDB Performance Metrics:\n\n'
4851 |
4852 | if (metrics.error) {
4853 | return `Error retrieving metrics: ${metrics.error}`
4854 | }
4855 |
4856 | result += '## Server Status\n'
4857 | if (metrics.serverStatus?.connections) {
4858 | result += `- Current Connections: ${metrics.serverStatus.connections.current}\n`
4859 | result += `- Available Connections: ${metrics.serverStatus.connections.available}\n`
4860 | }
4861 |
4862 | if (metrics.serverStatus?.opcounters) {
4863 | result += '\n## Operation Counters (since server start)\n'
4864 | for (const [op, count] of Object.entries(metrics.serverStatus.opcounters)) {
4865 | result += `- ${op}: ${count}\n`
4866 | }
4867 | }
4868 |
4869 | if (metrics.serverStatus?.wiredTiger) {
4870 | result += '\n## Cache Utilization\n'
4871 | result += `- Pages Read: ${metrics.serverStatus.wiredTiger.pages_read || 'N/A'}\n`
4872 | result += `- Max Bytes: ${formatSize(metrics.serverStatus.wiredTiger.maximum_bytes_configured || 0)}\n`
4873 | result += `- Current Bytes: ${formatSize(metrics.serverStatus.wiredTiger.bytes_currently_in_cache || 0)}\n`
4874 | result += `- Dirty Bytes: ${formatSize(metrics.serverStatus.wiredTiger.tracked_dirty_bytes || 0)}\n`
4875 | }
4876 |
4877 | result += '\n## Database Profiling\n'
4878 | result += `- Profiling Level: ${metrics.profileSettings?.was || 'N/A'}\n`
4879 | result += `- Slow Query Threshold: ${metrics.profileSettings?.slowms || 0}ms\n`
4880 |
4881 | if (metrics.currentOperations && metrics.currentOperations.length > 0) {
4882 | result += '\n## Long-Running Operations (Limited)\n'
4883 | // Only show limited operations with simplified content
4884 | const limitedOps = metrics.currentOperations.slice(0, 3)
4885 | for (const op of limitedOps) {
4886 | result += `- Op: ${op.op || 'unknown'} running for ${op.secs_running || 0}s\n`
4887 | result += ` - Namespace: ${op.ns || 'unknown'}\n`
4888 | // Avoid stringifying complex objects
4889 | if (op.command) {
4890 | const cmdKeys = Object.keys(op.command).slice(0, 3)
4891 | result += ` - Command keys: ${cmdKeys.join(', ')}\n`
4892 | }
4893 | result += '\n'
4894 | }
4895 |
4896 | if (metrics.currentOperations.length > 3) {
4897 | result += `(${metrics.currentOperations.length - 3} more operations not shown)\n`
4898 | }
4899 | }
4900 |
4901 | if (metrics.slowQueries && metrics.slowQueries.length > 0) {
4902 | result += '\n## Slow Queries (Limited)\n'
4903 | // Only show top few slow queries
4904 | const limitedQueries = metrics.slowQueries.slice(0, 3)
4905 | for (const query of limitedQueries) {
4906 | result += `- Collection: ${query.ns?.split('.')[1] || 'unknown'}\n`
4907 | result += ` - Duration: ${query.millis || 0}ms\n`
4908 | result += ` - Date: ${query.ts ? new Date(query.ts).toISOString() : 'unknown'}\n`
4909 | result += '\n'
4910 | }
4911 |
4912 | if (metrics.slowQueries.length > 3) {
4913 | result += `(${metrics.slowQueries.length - 3} more slow queries not shown)\n`
4914 | }
4915 | }
4916 |
4917 | return result
4918 | }
4919 |
4920 | const formatTriggerConfiguration = (triggers) => {
4921 | if (triggers.error) return `Trigger information not available: ${triggers.error}`
4922 |
4923 | let result = 'MongoDB Event Trigger Configuration:\n\n'
4924 |
4925 | result += '## Change Stream Support\n'
4926 | if (triggers.changeStreams.supported) {
4927 | result += '- Change streams are supported in this MongoDB version\n'
4928 | result += `- Resume token support: ${triggers.changeStreams.resumeTokenSupported ? 'Yes' : 'No'}\n`
4929 | result += `- Update lookup support: ${triggers.changeStreams.updateLookupSupported ? 'Yes' : 'No'}\n`
4930 | result += `- Full document before change: ${triggers.changeStreams.fullDocumentBeforeChangeSupported ? 'Yes' : 'No'}\n`
4931 | } else {
4932 | result += '- Change streams are not supported in this MongoDB deployment\n'
4933 | if (triggers.changeStreams.reason) {
4934 | result += ` Reason: ${triggers.changeStreams.reason}\n`
4935 | }
4936 | if (triggers.changeStreams.howToEnable) {
4937 | result += ` How to Enable: ${triggers.changeStreams.howToEnable}\n`
4938 | }
4939 | }
4940 |
4941 | result += '\n## Potential Trigger Collections\n'
4942 | if (triggers.triggerCollections && triggers.triggerCollections.length > 0) {
4943 | for (const coll of triggers.triggerCollections) {
4944 | result += `- ${coll.name} (${coll.type})\n`
4945 | }
4946 | } else {
4947 | result += '- No collections found with trigger-related naming\n'
4948 | }
4949 |
4950 | result += '\n## Stored Trigger Functions\n'
4951 | if (triggers.triggerFunctions && triggers.triggerFunctions.length > 0) {
4952 | for (const func of triggers.triggerFunctions) {
4953 | result += `- ${func._id}\n`
4954 | if (typeof func.value === 'function') {
4955 | result += ` ${func.value.toString().split('\n')[0]}…\n`
4956 | }
4957 | }
4958 | } else {
4959 | result += '- No stored JavaScript functions with trigger-related naming found\n'
4960 | }
4961 |
4962 | return result
4963 | }
4964 |
4965 | const formatSchemaComparison = (comparison, sourceCollection, targetCollection) => {
4966 | const { source, target, commonFields, sourceOnlyFields, targetOnlyFields, typeDifferences, stats } = comparison
4967 |
4968 | let result = `# Schema Comparison: '${source}' vs '${target}'\n\n`
4969 |
4970 | result += `## Summary\n`
4971 | result += `- Source Collection: ${source} (${stats.sourceFieldCount} fields)\n`
4972 | result += `- Target Collection: ${target} (${stats.targetFieldCount} fields)\n`
4973 | result += `- Common Fields: ${stats.commonFieldCount}\n`
4974 | result += `- Fields Only in Source: ${sourceOnlyFields.length}\n`
4975 | result += `- Fields Only in Target: ${targetOnlyFields.length}\n`
4976 | result += `- Type Mismatches: ${typeDifferences.length}\n\n`
4977 |
4978 | if (typeDifferences.length > 0) {
4979 | result += `## Type Differences\n`
4980 | typeDifferences.forEach(diff => {
4981 | result += `- ${diff.field}: ${diff.sourceTypes.join(', ')} (${source}) vs ${diff.targetTypes.join(', ')} (${target})\n`
4982 | })
4983 | result += '\n'
4984 | }
4985 |
4986 | if (sourceOnlyFields.length > 0) {
4987 | result += `## Fields Only in ${source}\n`
4988 | sourceOnlyFields.forEach(field => {
4989 | result += `- ${field.name}: ${field.types.join(', ')}\n`
4990 | })
4991 | result += '\n'
4992 | }
4993 |
4994 | if (targetOnlyFields.length > 0) {
4995 | result += `## Fields Only in ${target}\n`
4996 | targetOnlyFields.forEach(field => {
4997 | result += `- ${field.name}: ${field.types.join(', ')}\n`
4998 | })
4999 | result += '\n'
5000 | }
5001 |
5002 | if (stats.commonFieldCount > 0) {
5003 | result += `## Common Fields\n`
5004 | commonFields.forEach(field => {
5005 | const statusSymbol = field.typesMatch ? '✓' : '✗'
5006 | result += `- ${statusSymbol} ${field.name}\n`
5007 | })
5008 | }
5009 |
5010 | return result
5011 | }
5012 |
5013 | const formatQueryAnalysis = (analysis) => {
5014 | const { collection, indexRecommendations, queryOptimizations, unusedIndexes, schemaIssues, queryStats } = analysis
5015 |
5016 | let result = `# Query Pattern Analysis for '${collection}'\n\n`
5017 |
5018 | if (indexRecommendations.length > 0) {
5019 | result += `## Index Recommendations\n`
5020 | indexRecommendations.forEach((rec, i) => {
5021 | result += `### ${i+1}. Create index on: ${rec.fields.join(', ')}\n`
5022 | if (rec.filter && !rec.automatic) {
5023 | result += `- Based on query filter: ${rec.filter}\n`
5024 | if (rec.millis) {
5025 | result += `- Current execution time: ${rec.millis}ms\n`
5026 | }
5027 | } else if (rec.automatic) {
5028 | result += `- Automatic recommendation based on field name patterns\n`
5029 | }
5030 | result += `- Create using: \`create-index {"collection": "${collection}", "keys": "{\\"${rec.fields[0]}\\": 1}"}\`\n\n`
5031 | })
5032 | }
5033 |
5034 | if (unusedIndexes.length > 0) {
5035 | result += `## Unused Indexes\n`
5036 | result += 'The following indexes appear to be unused and could potentially be removed:\n'
5037 | unusedIndexes.forEach((idx) => {
5038 | result += `- ${idx.name} on fields: ${idx.fields.join(', ')}\n`
5039 | })
5040 | result += '\n'
5041 | }
5042 |
5043 | if (schemaIssues.length > 0) {
5044 | result += `## Schema Concerns\n`
5045 | schemaIssues.forEach((issue) => {
5046 | result += `- ${issue.field}: ${issue.issue} - ${issue.description}\n`
5047 | })
5048 | result += '\n'
5049 | }
5050 |
5051 | if (queryStats.length > 0) {
5052 | result += `## Recent Queries\n`
5053 | result += 'Most recent query patterns observed:\n'
5054 |
5055 | const uniquePatterns = {}
5056 | queryStats.forEach(stat => {
5057 | const key = stat.filter
5058 | if (!uniquePatterns[key]) {
5059 | uniquePatterns[key] = {
5060 | filter: stat.filter,
5061 | fields: stat.fields,
5062 | count: 1,
5063 | totalTime: stat.millis,
5064 | avgTime: stat.millis,
5065 | scanType: stat.scanType
5066 | }
5067 | } else {
5068 | uniquePatterns[key].count++
5069 | uniquePatterns[key].totalTime += stat.millis
5070 | uniquePatterns[key].avgTime = uniquePatterns[key].totalTime / uniquePatterns[key].count
5071 | }
5072 | })
5073 |
5074 | Object.values(uniquePatterns)
5075 | .sort((a, b) => b.avgTime - a.avgTime)
5076 | .slice(0, 5)
5077 | .forEach(pattern => {
5078 | result += `- Filter: ${pattern.filter}\n`
5079 | result += ` - Fields: ${pattern.fields.join(', ')}\n`
5080 | result += ` - Count: ${pattern.count}\n`
5081 | result += ` - Avg Time: ${pattern.avgTime.toFixed(2)}ms\n`
5082 | result += ` - Scan Type: ${pattern.scanType}\n\n`
5083 | })
5084 | }
5085 |
5086 | return result
5087 | }
5088 |
5089 | const formatExport = async (documents, format = config.tools.export.defaultFormat, fields = null, limit = config.tools.export.defaultLimit) => {
5090 | log(`DB Operation: Formatting ${documents.length} documents for export in ${format} format…`)
5091 | try {
5092 | const limitedDocs = limit !== -1
5093 | ? documents.slice(0, limit)
5094 | : documents
5095 |
5096 | if (format === 'json') {
5097 | return JSON.stringify(limitedDocs, (key, value) => serializeForExport(value), 2)
5098 | } else if (format === 'csv') {
5099 | if (!fields || !fields.length) {
5100 | if (limitedDocs.length > 0) {
5101 | fields = Object.keys(limitedDocs[0])
5102 | } else {
5103 | return 'No documents found for export'
5104 | }
5105 | }
5106 |
5107 | let csv = fields.join(',') + '\n'
5108 |
5109 | for (const doc of limitedDocs) {
5110 | const row = fields.map(field => {
5111 | const value = getValueAtPath(doc, field)
5112 | return formatCsvValue(value)
5113 | })
5114 | csv += row.join(',') + '\n'
5115 | }
5116 |
5117 | return csv
5118 | }
5119 |
5120 | throw new Error(`Unsupported export format: ${format}`)
5121 | } catch (error) {
5122 | log(`DB Operation: Export formatting failed: ${error.message}`)
5123 | throw error
5124 | }
5125 | }
5126 |
5127 | const serializeForExport = (value) => {
5128 | if (value instanceof ObjectId) {
5129 | return value.toString()
5130 | } else if (value instanceof Date) {
5131 | return value.toISOString()
5132 | }
5133 | return value
5134 | }
5135 |
5136 | const formatCsvValue = (value) => {
5137 | if (value === null || value === undefined) return ''
5138 |
5139 | const stringValue = typeof value === 'object' ? JSON.stringify(value) : String(value)
5140 |
5141 | if (stringValue.includes(',') || stringValue.includes('\n') || stringValue.includes('"')) {
5142 | return `"${stringValue.replace(/"/g, '""')}"`
5143 | }
5144 |
5145 | return stringValue
5146 | }
5147 |
5148 | const serializeDocument = (doc) => {
5149 | if (Array.isArray(doc)) return doc.map(serializeDocument)
5150 |
5151 | if (doc === null || typeof doc !== 'object') return doc
5152 |
5153 | const result = {}
5154 |
5155 | for (const [key, value] of Object.entries(doc)) {
5156 | if (value instanceof ObjectId) {
5157 | result[key] = `ObjectId("${value.toString()}")`
5158 | } else if (value instanceof Date) {
5159 | result[key] = `ISODate("${value.toISOString()}")`
5160 | } else if (Array.isArray(value)) {
5161 | result[key] = value.map(serializeDocument)
5162 | } else if (typeof value === 'object' && value !== null) {
5163 | result[key] = serializeDocument(value)
5164 | } else {
5165 | result[key] = value
5166 | }
5167 | }
5168 |
5169 | return result
5170 | }
5171 |
5172 | const compareSchemas = (sourceSchema, targetSchema) => {
5173 | const result = {
5174 | source: sourceSchema.collectionName,
5175 | target: targetSchema.collectionName,
5176 | commonFields: [],
5177 | sourceOnlyFields: [],
5178 | targetOnlyFields: [],
5179 | typeDifferences: []
5180 | }
5181 |
5182 | const sourceFields = Object.keys(sourceSchema.fields)
5183 | const targetFields = Object.keys(targetSchema.fields)
5184 |
5185 | sourceFields.forEach(field => {
5186 | if (targetFields.includes(field)) {
5187 | const sourceTypes = sourceSchema.fields[field].types
5188 | const targetTypes = targetSchema.fields[field].types
5189 | const typesMatch = arraysEqual(sourceTypes, targetTypes)
5190 |
5191 | result.commonFields.push({
5192 | name: field,
5193 | sourceTypes,
5194 | targetTypes,
5195 | typesMatch
5196 | })
5197 |
5198 | if (!typesMatch) {
5199 | result.typeDifferences.push({
5200 | field,
5201 | sourceTypes,
5202 | targetTypes
5203 | })
5204 | }
5205 | } else {
5206 | result.sourceOnlyFields.push({
5207 | name: field,
5208 | types: sourceSchema.fields[field].types
5209 | })
5210 | }
5211 | })
5212 |
5213 | targetFields.forEach(field => {
5214 | if (!sourceFields.includes(field)) {
5215 | result.targetOnlyFields.push({
5216 | name: field,
5217 | types: targetSchema.fields[field].types
5218 | })
5219 | }
5220 | })
5221 |
5222 | result.stats = {
5223 | sourceFieldCount: sourceFields.length,
5224 | targetFieldCount: targetFields.length,
5225 | commonFieldCount: result.commonFields.length,
5226 | mismatchCount: result.typeDifferences.length
5227 | }
5228 |
5229 | return result
5230 | }
5231 |
5232 | const analyzeQueryPatterns = (collection, schema, indexes, queryStats) => {
5233 | const analysis = {
5234 | collection,
5235 | indexRecommendations: [],
5236 | queryOptimizations: [],
5237 | unusedIndexes: [],
5238 | schemaIssues: [],
5239 | queryStats: []
5240 | }
5241 |
5242 | const indexMap = {}
5243 | indexes.forEach(idx => {
5244 | indexMap[idx.name] = {
5245 | key: idx.key,
5246 | unique: !!idx.unique,
5247 | sparse: !!idx.sparse,
5248 | fields: Object.keys(idx.key),
5249 | usage: idx.usage || { ops: 0, since: new Date() }
5250 | }
5251 | })
5252 |
5253 | for (const [name, idx] of Object.entries(indexMap)) {
5254 | if (name !== '_id_' && (!idx.usage || idx.usage.ops === 0)) {
5255 | analysis.unusedIndexes.push({
5256 | name,
5257 | fields: idx.fields,
5258 | properties: idx.unique ? 'unique' : ''
5259 | })
5260 | }
5261 | }
5262 |
5263 | if (queryStats && queryStats.length > 0) {
5264 | queryStats.forEach(stat => {
5265 | if (stat.command && stat.command.filter) {
5266 | const filter = stat.command.filter
5267 | const queryFields = Object.keys(filter)
5268 | const millis = stat.millis || 0
5269 |
5270 | analysis.queryStats.push({
5271 | filter: JSON.stringify(filter),
5272 | fields: queryFields,
5273 | millis,
5274 | scanType: stat.planSummary || 'Unknown',
5275 | timestamp: stat.ts
5276 | })
5277 |
5278 | const hasMatchingIndex = indexes.some(idx => {
5279 | const indexFields = Object.keys(idx.key)
5280 | return queryFields.every(field => indexFields.includes(field))
5281 | })
5282 |
5283 | if (!hasMatchingIndex && queryFields.length > 0 && millis > config.defaults.slowMs) {
5284 | analysis.indexRecommendations.push({
5285 | fields: queryFields,
5286 | filter: JSON.stringify(filter),
5287 | millis
5288 | })
5289 | }
5290 | }
5291 | })
5292 | }
5293 |
5294 | const schemaFields = Object.entries(schema.fields)
5295 |
5296 | schemaFields.forEach(([fieldName, info]) => {
5297 | if (info.types.includes('array') && info.sample && Array.isArray(info.sample) && info.sample.length > 50) {
5298 | analysis.schemaIssues.push({
5299 | field: fieldName,
5300 | issue: 'Large array',
5301 | description: `Field contains arrays with ${info.sample.length}+ items, which can cause performance issues.`
5302 | })
5303 | }
5304 | })
5305 |
5306 | const likelyQueryFields = schemaFields
5307 | .filter(([name, info]) => {
5308 | const lowerName = name.toLowerCase()
5309 | return (
5310 | lowerName.includes('id') ||
5311 | lowerName.includes('key') ||
5312 | lowerName.includes('date') ||
5313 | lowerName.includes('time') ||
5314 | lowerName === 'email' ||
5315 | lowerName === 'name' ||
5316 | lowerName === 'status'
5317 | ) && !indexMap._id_ && !indexMap[name + '_1']
5318 | })
5319 | .map(([name]) => name)
5320 |
5321 | if (likelyQueryFields.length > 0) {
5322 | analysis.indexRecommendations.push({
5323 | fields: likelyQueryFields,
5324 | filter: 'Common query field pattern',
5325 | automatic: true
5326 | })
5327 | }
5328 |
5329 | return analysis
5330 | }
5331 |
5332 | const generateJsonSchemaValidator = (schema, strictness) => {
5333 | const validator = {
5334 | $jsonSchema: {
5335 | bsonType: "object",
5336 | required: [],
5337 | properties: {}
5338 | }
5339 | }
5340 |
5341 | const requiredThreshold =
5342 | strictness === 'strict' ? 90 :
5343 | strictness === 'moderate' ? 75 :
5344 | 60
5345 |
5346 | Object.entries(schema.fields).forEach(([fieldPath, info]) => {
5347 | if (fieldPath.includes('.')) return
5348 |
5349 | const cleanFieldPath = fieldPath.replace('[]', '')
5350 |
5351 | let bsonTypes = []
5352 | info.types.forEach(type => {
5353 | switch(type) {
5354 | case 'string':
5355 | bsonTypes.push('string')
5356 | break
5357 | case 'number':
5358 | bsonTypes.push('number', 'double', 'int')
5359 | break
5360 | case 'boolean':
5361 | bsonTypes.push('bool')
5362 | break
5363 | case 'array':
5364 | bsonTypes.push('array')
5365 | break
5366 | case 'object':
5367 | bsonTypes.push('object')
5368 | break
5369 | case 'null':
5370 | bsonTypes.push('null')
5371 | break
5372 | case 'Date':
5373 | bsonTypes.push('date')
5374 | break
5375 | case 'ObjectId':
5376 | bsonTypes.push('objectId')
5377 | break
5378 | }
5379 | })
5380 |
5381 | const fieldSchema = bsonTypes.length === 1
5382 | ? { bsonType: bsonTypes[0] }
5383 | : { bsonType: bsonTypes }
5384 |
5385 | validator.$jsonSchema.properties[cleanFieldPath] = fieldSchema
5386 |
5387 | const coverage = info.coverage || Math.round((info.count / schema.sampleSize) * 100)
5388 | if (coverage >= requiredThreshold && !info.types.includes('null')) {
5389 | validator.$jsonSchema.required.push(cleanFieldPath)
5390 | }
5391 | })
5392 |
5393 | if (strictness === 'strict') {
5394 | validator.$jsonSchema.additionalProperties = false
5395 | }
5396 |
5397 | return validator
5398 | }
5399 |
5400 | const processAggregationPipeline = (pipeline) => {
5401 | if (!pipeline || !Array.isArray(pipeline)) return pipeline
5402 | return pipeline.map(stage => {
5403 | for (const operator in stage) {
5404 | const value = stage[operator]
5405 | if (typeof value === 'object' && value !== null) {
5406 | if (operator === '$match' && value.$text) {
5407 | if (value.$text.$search && typeof value.$text.$search === 'string') {
5408 | const sanitizedSearch = sanitizeTextSearch(value.$text.$search)
5409 | const textQuery = { $search: sanitizedSearch }
5410 | if (value.$text.$language) textQuery.$language = value.$text.$language
5411 | if (value.$text.$caseSensitive !== undefined) textQuery.$caseSensitive = value.$text.$caseSensitive
5412 | if (value.$text.$diacriticSensitive !== undefined) textQuery.$diacriticSensitive = value.$text.$diacriticSensitive
5413 | value.$text = textQuery
5414 | }
5415 | }
5416 | }
5417 | }
5418 | return stage
5419 | })
5420 | }
5421 |
5422 | const sanitizeTextSearch = (searchText) => {
5423 | if (!searchText) return ''
5424 | return searchText.replace(/\$/g, '').replace(/[\u0000-\u001F\u007F-\u009F]/g, '')
5425 | }
5426 |
5427 | const generateDropToken = () => {
5428 | const maxToken = Math.pow(10, config.security.tokenLength)
5429 | return String(Math.floor(Math.random() * maxToken)).padStart(config.security.tokenLength, '0')
5430 | }
5431 |
5432 | const storeDropDatabaseToken = (dbName) => {
5433 | const token = generateDropToken()
5434 | confirmationTokens.dropDatabase.set(dbName, {
5435 | token,
5436 | expires: Date.now() + config.security.tokenExpirationMinutes * 60 * 1000
5437 | })
5438 | return token
5439 | }
5440 |
5441 | const validateDropDatabaseToken = (dbName, token) => {
5442 | const storedData = confirmationTokens.dropDatabase.get(dbName)
5443 | if (!storedData) return false
5444 | if (Date.now() > storedData.expires) {
5445 | confirmationTokens.dropDatabase.delete(dbName)
5446 | return false
5447 | }
5448 | if (storedData.token !== token) return false
5449 | confirmationTokens.dropDatabase.delete(dbName)
5450 | return true
5451 | }
5452 |
5453 | const storeDropCollectionToken = (collectionName) => {
5454 | const token = generateDropToken()
5455 | confirmationTokens.dropCollection.set(collectionName, {
5456 | token,
5457 | expires: Date.now() + 5 * 60 * 1000
5458 | })
5459 | return token
5460 | }
5461 |
5462 | const validateDropCollectionToken = (collectionName, token) => {
5463 | const storedData = confirmationTokens.dropCollection.get(collectionName)
5464 | if (!storedData) return false
5465 | if (Date.now() > storedData.expires) {
5466 | confirmationTokens.dropCollection.delete(collectionName)
5467 | return false
5468 | }
5469 | if (storedData.token !== token) return false
5470 | confirmationTokens.dropCollection.delete(collectionName)
5471 | return true
5472 | }
5473 |
5474 | const storeDeleteDocumentToken = (collectionName, filter) => {
5475 | const key = `${collectionName}:${JSON.stringify(filter)}`
5476 | const token = generateDropToken()
5477 | confirmationTokens.deleteDocument.set(key, {
5478 | token,
5479 | expires: Date.now() + 5 * 60 * 1000
5480 | })
5481 | return token
5482 | }
5483 |
5484 | const validateDeleteDocumentToken = (collectionName, filter, token) => {
5485 | const key = `${collectionName}:${JSON.stringify(filter)}`
5486 | const storedData = confirmationTokens.deleteDocument.get(key)
5487 | if (!storedData) return false
5488 | if (Date.now() > storedData.expires) {
5489 | confirmationTokens.deleteDocument.delete(key)
5490 | return false
5491 | }
5492 | if (storedData.token !== token) return false
5493 | confirmationTokens.deleteDocument.delete(key)
5494 | return true
5495 | }
5496 |
5497 | const storeBulkOperationsToken = (collectionName, operations) => {
5498 | const key = `${collectionName}:${operations.length}`
5499 | const token = generateDropToken()
5500 | confirmationTokens.bulkOperations.set(key, {
5501 | token,
5502 | expires: Date.now() + 5 * 60 * 1000,
5503 | operations
5504 | })
5505 | return token
5506 | }
5507 |
5508 | const validateBulkOperationsToken = (collectionName, operations, token) => {
5509 | const key = `${collectionName}:${operations.length}`
5510 | const storedData = confirmationTokens.bulkOperations.get(key)
5511 | if (!storedData) return false
5512 | if (Date.now() > storedData.expires) {
5513 | confirmationTokens.bulkOperations.delete(key)
5514 | return false
5515 | }
5516 | if (storedData.token !== token) return false
5517 | confirmationTokens.bulkOperations.delete(key)
5518 | return true
5519 | }
5520 |
5521 | const storeRenameCollectionToken = (oldName, newName, dropTarget) => {
5522 | const key = `${oldName}:${newName}:${dropTarget}`
5523 | const token = generateDropToken()
5524 | confirmationTokens.renameCollection.set(key, {
5525 | token,
5526 | expires: Date.now() + 5 * 60 * 1000
5527 | })
5528 | return token
5529 | }
5530 |
5531 | const validateRenameCollectionToken = (oldName, newName, dropTarget, token) => {
5532 | const key = `${oldName}:${newName}:${dropTarget}`
5533 | const storedData = confirmationTokens.renameCollection.get(key)
5534 | if (!storedData) return false
5535 | if (Date.now() > storedData.expires) {
5536 | confirmationTokens.renameCollection.delete(key)
5537 | return false
5538 | }
5539 | if (storedData.token !== token) return false
5540 | confirmationTokens.renameCollection.delete(key)
5541 | return true
5542 | }
5543 |
5544 | const storeDropUserToken = (username) => {
5545 | const token = generateDropToken()
5546 | confirmationTokens.dropUser.set(username, {
5547 | token,
5548 | expires: Date.now() + 5 * 60 * 1000
5549 | })
5550 | return token
5551 | }
5552 |
5553 | const validateDropUserToken = (username, token) => {
5554 | const storedData = confirmationTokens.dropUser.get(username)
5555 | if (!storedData) return false
5556 | if (Date.now() > storedData.expires) {
5557 | confirmationTokens.dropUser.delete(username)
5558 | return false
5559 | }
5560 | if (storedData.token !== token) return false
5561 | confirmationTokens.dropUser.delete(username)
5562 | return true
5563 | }
5564 |
5565 | const storeDropIndexToken = (collectionName, indexName) => {
5566 | const key = `${collectionName}:${indexName}`
5567 | const token = generateDropToken()
5568 | confirmationTokens.dropIndex.set(key, {
5569 | token,
5570 | expires: Date.now() + 5 * 60 * 1000
5571 | })
5572 | return token
5573 | }
5574 |
5575 | const validateDropIndexToken = (collectionName, indexName, token) => {
5576 | const key = `${collectionName}:${indexName}`
5577 | const storedData = confirmationTokens.dropIndex.get(key)
5578 | if (!storedData) return false
5579 | if (Date.now() > storedData.expires) {
5580 | confirmationTokens.dropIndex.delete(key)
5581 | return false
5582 | }
5583 | if (storedData.token !== token) return false
5584 | confirmationTokens.dropIndex.delete(key)
5585 | return true
5586 | }
5587 |
5588 | const loadConfig = () => {
5589 | let config = { ...defaultConfig }
5590 |
5591 | const configPathEnv = process.env.CONFIG_PATH
5592 |
5593 | let configPath = null
5594 | if (configPathEnv) {
5595 | configPath = configPathEnv
5596 | } else {
5597 | const homeDir = process.env.HOME || process.env.USERPROFILE || __dirname
5598 |
5599 | const jsoncPath = join(homeDir, '.mongodb-lens.jsonc')
5600 | const jsonPath = join(homeDir, '.mongodb-lens.json')
5601 |
5602 | if (existsSync(jsoncPath)) {
5603 | configPath = jsoncPath
5604 | } else if (existsSync(jsonPath)) {
5605 | configPath = jsonPath
5606 | }
5607 | }
5608 |
5609 | if (configPath && existsSync(configPath)) {
5610 | try {
5611 | log(`Loading config file: ${configPath}`)
5612 | const fileContent = readFileSync(configPath, 'utf8')
5613 | const strippedContent = stripJsonComments(fileContent)
5614 | const configFile = JSON.parse(strippedContent)
5615 | config = mergeConfigs(config, configFile)
5616 | } catch (error) {
5617 | console.error(`Error loading config file: ${error.message}`)
5618 | }
5619 | }
5620 |
5621 | config = applyEnvOverrides(config)
5622 |
5623 | if (process.argv.length > 2) {
5624 | const cliArg = process.argv[2]
5625 | config._cliMongoUri = cliArg
5626 | }
5627 |
5628 | setupMongoUriMap(config)
5629 |
5630 | if (config._cliMongoUri) {
5631 | const resolvedUri = resolveMongoUri(config._cliMongoUri)
5632 | config.mongoUri = resolvedUri
5633 | delete config._cliMongoUri
5634 | }
5635 |
5636 | return config
5637 | }
5638 |
5639 | const setupMongoUriMap = (config) => {
5640 | mongoUriMap.clear()
5641 |
5642 | if (typeof config.mongoUri === 'object' && config.mongoUri !== null) {
5643 | Object.entries(config.mongoUri).forEach(([alias, uri]) => {
5644 | mongoUriMap.set(alias.toLowerCase(), uri)
5645 | })
5646 |
5647 | const firstEntry = Object.entries(config.mongoUri)[0]
5648 | if (firstEntry) {
5649 | config.mongoUri = firstEntry[1]
5650 | mongoUriAlias = firstEntry[0]
5651 | log(`Using default connection alias: ${mongoUriAlias}`)
5652 | } else {
5653 | config.mongoUri = 'mongodb://localhost:27017'
5654 | }
5655 | } else if (typeof config.mongoUri === 'string') {
5656 | mongoUriMap.set('default', config.mongoUri)
5657 | }
5658 | }
5659 |
5660 | const mergeConfigs = (target, source) =>
5661 | _.merge({}, target, source)
5662 |
5663 | const applyEnvOverrides = (config) => {
5664 | const result = { ...config }
5665 |
5666 | const mapping = createEnvMapping(config)
5667 |
5668 | Object.entries(mapping).forEach(([configPath, envKey]) => {
5669 | if (process.env[envKey] === undefined) return
5670 | const defaultValue = getValueAtPath(config, configPath)
5671 | const value = parseEnvValue(process.env[envKey], defaultValue, configPath)
5672 | setValueAtPath(result, configPath, value)
5673 | })
5674 |
5675 | return result
5676 | }
5677 |
5678 | const createEnvMapping = (obj, prefix = 'CONFIG', path = '') => {
5679 | let mapping = {}
5680 |
5681 | Object.entries(obj).forEach(([key, value]) => {
5682 | const currentPath = path ? `${path}.${key}` : key
5683 | const envKey = `${prefix}_${key.replace(/([A-Z])/g, '_$1').toUpperCase()}`
5684 |
5685 | if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
5686 | const nestedMapping = createEnvMapping(value, `${prefix}_${key.toUpperCase()}`, currentPath)
5687 | mapping = { ...mapping, ...nestedMapping }
5688 | } else {
5689 | mapping[currentPath] = envKey
5690 | }
5691 | })
5692 |
5693 | return mapping
5694 | }
5695 |
5696 | const parseEnvValue = (value, defaultValue, path) => {
5697 | try {
5698 | const normalizedValue = value.trim().replace(/^['"]|['"]$/g, '')
5699 |
5700 | if (typeof defaultValue === 'number') {
5701 | const parsed = Number(normalizedValue)
5702 | if (isNaN(parsed)) throw new Error(`Invalid number for config [${path}]: ${value}`)
5703 | return parsed
5704 | }
5705 |
5706 | if (typeof defaultValue === 'boolean') {
5707 | const cleanBool = normalizedValue.toLowerCase()
5708 | if (cleanBool !== 'true' && cleanBool !== 'false') {
5709 | throw new Error(`Invalid boolean for config [${path}]: ${value}`)
5710 | }
5711 | return cleanBool === 'true'
5712 | }
5713 |
5714 | if (Array.isArray(defaultValue)) {
5715 | try {
5716 | const parsed = JSON.parse(normalizedValue)
5717 | if (!Array.isArray(parsed)) throw new Error(`Config [${path}] is not an array`)
5718 | return parsed
5719 | } catch (e) {
5720 | return normalizedValue.split(',').map(item => item.trim())
5721 | }
5722 | }
5723 |
5724 | if (path === 'logLevel') {
5725 | const validLogLevels = ['info', 'verbose']
5726 | const cleanLogLevel = normalizedValue.toLowerCase()
5727 | if (!validLogLevels.includes(cleanLogLevel)) {
5728 | throw new Error(`Config [${path}] is invalid: ${value}`)
5729 | }
5730 | return cleanLogLevel
5731 | }
5732 |
5733 | return normalizedValue
5734 | } catch (error) {
5735 | console.error(`Error parsing environment variable for config [${path}]: ${error.message}. Using default: ${defaultValue}`)
5736 | return defaultValue
5737 | }
5738 | }
5739 |
5740 | const parseJsonString = (jsonString) => {
5741 | if (!jsonString || typeof jsonString !== 'string') return jsonString
5742 | try {
5743 | return EJSON.parse(jsonString, { relaxed: false })
5744 | } catch (error) {
5745 | throw new Error(`Invalid JSON: ${error.message}`)
5746 | }
5747 | }
5748 |
5749 | const getValueAtPath = (obj, path) =>
5750 | _.get(obj, path)
5751 |
5752 | const setValueAtPath = (obj, path, value) => {
5753 | _.set(obj, path, value)
5754 | return obj
5755 | }
5756 |
5757 | const getPackageVersion = () => {
5758 | if (packageVersion) return packageVersion
5759 | const __filename = fileURLToPath(import.meta.url)
5760 | const __dirname = dirname(__filename)
5761 | const packageJson = JSON.parse(readFileSync(join(__dirname, 'package.json'), 'utf8'))
5762 | packageVersion = packageJson.version
5763 | return packageVersion
5764 | }
5765 |
5766 | const createBooleanSchema = (description, defaultValue = 'true') =>
5767 | z.string()
5768 | .transform(val => val?.toLowerCase())
5769 | .pipe(z.enum(['true', 'false']))
5770 | .default(defaultValue)
5771 | .describe(description)
5772 |
5773 | const arraysEqual = (a, b) => {
5774 | if (a.length !== b.length) return false
5775 | return _.isEqual(_.sortBy(a), _.sortBy(b))
5776 | }
5777 |
5778 | const log = (message, forceLog = false) => {
5779 | const logLevel = process.env.CONFIG_LOG_LEVEL &&
5780 | typeof process.env.CONFIG_LOG_LEVEL === 'string' &&
5781 | process.env.CONFIG_LOG_LEVEL.trim().replace(/^['"]+|['"]+$/g, '')
5782 | if (forceLog || logLevel === 'verbose' || globalThis.config && config.logLevel === 'verbose') console.error(message)
5783 | }
5784 |
5785 | const cleanup = async () => {
5786 | if (watchdog) {
5787 | clearInterval(watchdog)
5788 | watchdog = null
5789 | }
5790 |
5791 | if (server) {
5792 | try {
5793 | log('Closing MCP server…')
5794 | await server.close()
5795 | log('MCP server closed.')
5796 | } catch (error) {
5797 | log(`Error closing MCP server: ${error.message}`, true)
5798 | }
5799 | }
5800 |
5801 | if (transport) {
5802 | try {
5803 | log('Closing transport…')
5804 | await transport.close()
5805 | log('Transport closed.')
5806 | } catch (error) {
5807 | log(`Error closing transport: ${error.message}`, true)
5808 | }
5809 | }
5810 |
5811 | if (mongoClient) {
5812 | try {
5813 | log('Closing MongoDB client…')
5814 | await mongoClient.close()
5815 | log('MongoDB client closed.')
5816 | } catch (error) {
5817 | log(`Error closing MongoDB client: ${error.message}`, true)
5818 | }
5819 | }
5820 |
5821 | clearMemoryCache()
5822 | }
5823 |
5824 | const setupSignalHandlers = () => {
5825 | process.on('SIGTERM', async () => {
5826 | isShuttingDown = true
5827 | log('Received SIGTERM, shutting down…')
5828 | await cleanup()
5829 | exit()
5830 | })
5831 |
5832 | process.on('SIGINT', async () => {
5833 | isShuttingDown = true
5834 | log('Received SIGINT, shutting down…')
5835 | await cleanup()
5836 | exit()
5837 | })
5838 | }
5839 |
5840 | const exit = (exitCode = 0) => {
5841 | log('Exiting…', true)
5842 | process.exit(exitCode)
5843 | }
5844 |
5845 | let server = null
5846 | let watchdog = null
5847 | let transport = null
5848 | let currentDb = null
5849 | let mongoClient = null
5850 | let currentDbName = null
5851 | let mongoUriAlias = null
5852 | let packageVersion = null
5853 | let connectionRetries = 0
5854 | let isShuttingDown = false
5855 | let mongoUriCurrent = null
5856 | let mongoUriMap = new Map()
5857 | let mongoUriOriginal = null
5858 | let isChangingConnection = false
5859 |
5860 | const memoryCache = {
5861 | stats: new Map(),
5862 | fields: new Map(),
5863 | schemas: new Map(),
5864 | indexes: new Map(),
5865 | collections: new Map(),
5866 | serverStatus: new Map(),
5867 | }
5868 |
5869 | const confirmationTokens = {
5870 | dropUser: new Map(),
5871 | dropIndex: new Map(),
5872 | dropDatabase: new Map(),
5873 | bulkOperations: new Map(),
5874 | deleteDocument: new Map(),
5875 | dropCollection: new Map(),
5876 | renameCollection: new Map(),
5877 | }
5878 |
5879 | const JSONRPC_ERROR_CODES = {
5880 | // Standard JSON-RPC error codes (keep for compatibility)
5881 | PARSE_ERROR: -32700,
5882 | INVALID_REQUEST: -32600,
5883 | METHOD_NOT_FOUND: -32601,
5884 | INVALID_PARAMS: -32602,
5885 | INTERNAL_ERROR: -32603,
5886 | SERVER_ERROR_START: -32000,
5887 |
5888 | // MongoDB-specific error codes (expanded)
5889 | MONGODB_CONNECTION_ERROR: -32050,
5890 | MONGODB_QUERY_ERROR: -32051,
5891 | MONGODB_SCHEMA_ERROR: -32052,
5892 | MONGODB_WRITE_ERROR: -32053,
5893 | MONGODB_DUPLICATE_KEY: -32054,
5894 | MONGODB_TIMEOUT_ERROR: -32055,
5895 |
5896 | // Resource-related error codes
5897 | RESOURCE_NOT_FOUND: -32040,
5898 | RESOURCE_ACCESS_DENIED: -32041,
5899 | RESOURCE_ALREADY_EXISTS: -32042
5900 | }
5901 |
5902 | const instructions = `
5903 | MongoDB-Lens: NL→MongoDB via MCP
5904 |
5905 | CAPS:
5906 | - DB: list/create/switch/drop
5907 | - COLL: create/rename/drop/validate
5908 | - DOC: find/count/insert-doc/update-doc/delete
5909 | - SCH: infer/validate/compare/analyze
5910 | - IDX: create/analyze/optimize
5911 | - AGG: pipeline/distinct
5912 | - PERF: explain/analyze/monitor
5913 | - UTIL: clear-cache/export/validate
5914 | - ADV: text/geo/timeseries/bulk/txn/gridfs/sharding/export
5915 | - CONN: connect-mongodb{uri|alias}/add-alias/list/connect-original
5916 |
5917 | PTRNS:
5918 | - DB_NAV: databases→use-database→collections
5919 | - QRY: find-docs/count-docs/distinct-values
5920 | - MOD: insert-doc/update-doc/delete-doc/bulk-ops
5921 | - SCH: collection-schema/analyze-schema/compare-schemas
5922 | - OPT: explain-query/analyze-patterns/create-index
5923 | - AGG: aggregate-data
5924 | - MON: server-status/performance-metrics/watch-changes
5925 | - CACHE: clear-cache{all|specific}
5926 | - CONN: add-alias→connect-mongodb→db-ops→connect-original
5927 |
5928 | FLOWS:
5929 | 1. NAV: list-dbs→use-db→list-collections
5930 | 2. QRY: find-docs{filter,project,sort}
5931 | 3. MOD: insert-doc/update-doc
5932 | 4. SCH: analyze-schema→generate-validator
5933 | 5. PERF: explain-query→analyze-patterns→create-index
5934 | 6. AGG: aggregate-data{multi-stage}
5935 | 7. BULK: bulk-ops{batch}
5936 | 8. TXN: transaction{atomic}
5937 | 9. CONN: add-alias→connect-mongodb{uri|alias}→ops→connect-original
5938 | 10. FRESH: clear-cache→list-collections/find-docs
5939 |
5940 | SAFE: destructive ops require confirmation tokens
5941 |
5942 | DOCS: github.com/furey/mongodb-lens/blob/main/README.md
5943 | `
5944 |
5945 | const importantNoticeToAI = `
5946 | IMPORTANT NOTICE TO AI ASSISTANT: You must NEVER correct, auto-complete, or suggest the confirmation
5947 | code if the user enters it incorrectly or asks you to replace it for them. The user MUST type the
5948 | exact code shown above, with no assistance, to proceed. This is a critical security measure.
5949 | Under no circumstances should you automate the sending of the prior prompt with the confirmation
5950 | code inserted. You MUST wait for the user to enter the code themselves. No exceptions.
5951 | `
5952 |
5953 | const defaultConfig = {
5954 | mongoUri: 'mongodb://localhost:27017',
5955 | connectionOptions: {
5956 | maxPoolSize: 20,
5957 | retryWrites: false,
5958 | connectTimeoutMS: 30000,
5959 | socketTimeoutMS: 360000,
5960 | heartbeatFrequencyMS: 10000,
5961 | serverSelectionTimeoutMS: 30000
5962 | },
5963 | defaultDbName: 'admin',
5964 | connection: {
5965 | maxRetries: 5,
5966 | maxRetryDelayMs: 30000,
5967 | reconnectionRetries: 10,
5968 | initialRetryDelayMs: 1000
5969 | },
5970 | cacheTTL: {
5971 | stats: 15 * 1000,
5972 | fields: 30 * 1000,
5973 | schemas: 60 * 1000,
5974 | indexes: 120 * 1000,
5975 | collections: 30 * 1000,
5976 | serverStatus: 20 * 1000
5977 | },
5978 | enabledCaches: [
5979 | 'stats',
5980 | 'fields',
5981 | 'schemas',
5982 | 'indexes',
5983 | 'collections',
5984 | 'serverStatus'
5985 | ],
5986 | memory: {
5987 | enableGC: true,
5988 | warningThresholdMB: 1500,
5989 | criticalThresholdMB: 2000
5990 | },
5991 | logLevel: 'info',
5992 | disableDestructiveOperationTokens: false,
5993 | watchdogIntervalMs: 30000,
5994 | defaults: {
5995 | slowMs: 100,
5996 | queryLimit: 10,
5997 | allowDiskUse: true,
5998 | schemaSampleSize: 100,
5999 | aggregationBatchSize: 50
6000 | },
6001 | security: {
6002 | tokenLength: 4,
6003 | tokenExpirationMinutes: 5,
6004 | strictDatabaseNameValidation: true
6005 | },
6006 | tools: {
6007 | transaction: {
6008 | readConcern: 'snapshot',
6009 | writeConcern: { w: 'majority' }
6010 | },
6011 | bulkOperations: {
6012 | ordered: true
6013 | },
6014 | export: {
6015 | defaultLimit: -1,
6016 | defaultFormat: 'json'
6017 | },
6018 | watchChanges: {
6019 | maxDurationSeconds: 60,
6020 | defaultDurationSeconds: 10
6021 | },
6022 | queryAnalysis: {
6023 | defaultDurationSeconds: 10
6024 | }
6025 | },
6026 | disabled: {
6027 | tools: undefined,
6028 | prompts: undefined,
6029 | resources: undefined
6030 | },
6031 | enabled: {
6032 | tools: undefined,
6033 | prompts: undefined,
6034 | resources: undefined
6035 | }
6036 | }
6037 |
6038 | setupSignalHandlers()
6039 |
6040 | const config = loadConfig()
6041 |
6042 | start(config.mongoUri)
6043 |
```