#
tokens: 67106/50000 1/12 files (page 3/3)
lines: on (toggle) GitHub
raw markdown copy reset
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 | 
```
Page 3/3FirstPrevNextLast