This is page 2 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.test.js:
--------------------------------------------------------------------------------
```javascript
1 | #!/usr/bin/env node
2 |
3 | import { spawn } from 'child_process'
4 | import { dirname, join } from 'path'
5 | import { fileURLToPath } from 'url'
6 | import { existsSync } from 'fs'
7 | import mongodb from 'mongodb'
8 |
9 | const { MongoClient, ObjectId } = mongodb
10 |
11 | const __filename = fileURLToPath(import.meta.url)
12 | const __dirname = dirname(__filename)
13 |
14 | const runTests = async () => {
15 | parseCommandLineArgs()
16 |
17 | if (process.argv.includes('--list')) return listAllTests()
18 |
19 | try {
20 | logHeader('MongoDB Lens Test Suite', 'margin:bottom')
21 |
22 | await initialize()
23 |
24 | logHeader('Testing Tools', 'margin:top,bottom')
25 |
26 | for (const group of TEST_GROUPS) {
27 | if (group.name !== 'Resources' && group.name !== 'Prompts') {
28 | await runTestGroup(group.name, group.tests)
29 | }
30 | }
31 |
32 | logHeader('Testing Resources', 'margin:top,bottom')
33 |
34 | const resourcesGroup = TEST_GROUPS.find(g => g.name === 'Resources')
35 | if (resourcesGroup) await runTestGroup(resourcesGroup.name, resourcesGroup.tests)
36 |
37 | logHeader('Testing Prompts', 'margin:top,bottom')
38 |
39 | const promptsGroup = TEST_GROUPS.find(g => g.name === 'Prompts')
40 | if (promptsGroup) await runTestGroup(promptsGroup.name, promptsGroup.tests)
41 | } finally {
42 | await cleanup()
43 | displayTestSummary()
44 | }
45 | }
46 |
47 | const runTestGroup = async (groupName, tests) => {
48 | let anyTestsInGroup = false
49 |
50 | for (const test of tests) {
51 | if (shouldRunTest(test.name, groupName)) {
52 | anyTestsInGroup = true
53 | break
54 | }
55 | }
56 |
57 | if (!anyTestsInGroup) return console.log(`${COLORS.yellow}Skipping group: ${groupName} - No matching tests${COLORS.reset}`)
58 |
59 | console.log(`${COLORS.blue}Running tests in group: ${groupName}${COLORS.reset}`)
60 |
61 | for (const test of tests) {
62 | if (shouldRunTest(test.name, groupName)) {
63 | await runTest(test.name, test.fn)
64 | } else {
65 | skipTest(test.name, 'Not selected for execution')
66 | }
67 | }
68 | }
69 |
70 | const shouldRunTest = (testName, groupName) => {
71 | if (!testFilters.length && !groupFilters.length && !patternFilters.length) return true
72 |
73 | if (testFilters.includes(testName)) return true
74 |
75 | if (groupFilters.includes(groupName)) return true
76 |
77 | for (const pattern of patternFilters) {
78 | const regexPattern = pattern.replace(/\*/g, '.*')
79 | const regex = new RegExp(regexPattern, 'i')
80 | if (regex.test(testName)) return true
81 | }
82 |
83 | return false
84 | }
85 |
86 | const listAllTests = () => {
87 | logHeader('Available Test Groups & Tests', 'margin:bottom')
88 |
89 | TEST_GROUPS.forEach(group => {
90 | console.log(`${COLORS.yellow}${group.name}:${COLORS.reset}`)
91 | group.tests.forEach(test => {
92 | console.log(`- ${test.name}`)
93 | })
94 | console.log('')
95 | })
96 |
97 | console.log(`${COLORS.yellow}Usage:${COLORS.reset}`)
98 | console.log(' --test=<test-name> Run specific test(s), comma separated')
99 | console.log(' --group=<group-name> Run all tests in specific group(s), comma separated')
100 | console.log(' --pattern=<pattern> Run tests matching pattern (glob style, e.g. *collection*)')
101 | console.log(' --list List all available tests without running them')
102 |
103 | process.exit(0)
104 | }
105 |
106 | const parseCommandLineArgs = () => {
107 | for (let i = 2; i < process.argv.length; i++) {
108 | const arg = process.argv[i]
109 |
110 | if (arg.startsWith('--test=')) {
111 | const tests = arg.replace('--test=', '').split(',')
112 | testFilters.push(...tests.map(t => t.trim()))
113 | } else if (arg.startsWith('--group=')) {
114 | const groups = arg.replace('--group=', '').split(',')
115 | groupFilters.push(...groups.map(g => g.trim()))
116 | } else if (arg.startsWith('--pattern=')) {
117 | const patterns = arg.replace('--pattern=', '').split(',')
118 | patternFilters.push(...patterns.map(p => p.trim()))
119 | }
120 | }
121 |
122 | if (testFilters.length || groupFilters.length || patternFilters.length) {
123 | console.log(`${COLORS.blue}Running with filters:${COLORS.reset}`)
124 | if (testFilters.length) console.log(`${COLORS.blue}Tests: ${testFilters.join(', ')}${COLORS.reset}`)
125 | if (groupFilters.length) console.log(`${COLORS.blue}Groups: ${groupFilters.join(', ')}${COLORS.reset}`)
126 | if (patternFilters.length) console.log(`${COLORS.blue}Patterns: ${patternFilters.join(', ')}${COLORS.reset}`)
127 | }
128 | }
129 |
130 | const initialize = async () => {
131 | mongoUri = await setupMongoUri()
132 | await connectToMongo()
133 | await setupTestEnvironment()
134 | }
135 |
136 | const setupMongoUri = async () => {
137 | const uri = process.env.CONFIG_MONGO_URI
138 |
139 | if (uri === 'mongodb-memory-server') return await startInMemoryMongo()
140 |
141 | if (!uri) {
142 | console.error(`${COLORS.red}No MongoDB URI provided. Please set CONFIG_MONGO_URI environment variable.${COLORS.reset}`)
143 | console.error(`${COLORS.yellow}Example: CONFIG_MONGO_URI=mongodb://localhost:27017 node mongodb-lens.test.js${COLORS.reset}`)
144 | console.error(`${COLORS.yellow}Example: CONFIG_MONGO_URI=mongodb-memory-server node mongodb-lens.test.js${COLORS.reset}`)
145 | process.exit(1)
146 | }
147 |
148 | return uri
149 | }
150 |
151 | const startInMemoryMongo = async () => {
152 | console.log(`${COLORS.yellow}In-memory MongoDB requested. Checking if mongodb-memory-server is available…${COLORS.reset}`)
153 | try {
154 | const { MongoMemoryServer } = await import('mongodb-memory-server')
155 | const mongod = await MongoMemoryServer.create()
156 | const uri = mongod.getUri()
157 | console.log(`${COLORS.green}In-memory MongoDB instance started at: ${uri}${COLORS.reset}`)
158 |
159 | process.on('exit', async () => {
160 | console.log(`${COLORS.yellow}Stopping in-memory MongoDB server…${COLORS.reset}`)
161 | await mongod.stop()
162 | })
163 |
164 | return uri
165 | } catch (err) {
166 | console.error(`${COLORS.red}Failed to start in-memory MongoDB: ${err.message}${COLORS.reset}`)
167 | console.error(`${COLORS.yellow}Install with: npm install mongodb-memory-server --save-dev${COLORS.reset}`)
168 | process.exit(1)
169 | }
170 | }
171 |
172 | const connectToMongo = async () => {
173 | console.log(`${COLORS.blue}Connecting to MongoDB: ${obfuscateMongoUri(mongoUri)}${COLORS.reset}`)
174 |
175 | if (!existsSync(MONGODB_LENS_PATH)) {
176 | console.error(`${COLORS.red}MongoDB Lens script not found at: ${MONGODB_LENS_PATH}${COLORS.reset}`)
177 | process.exit(1)
178 | }
179 |
180 | directMongoClient = new MongoClient(mongoUri, {
181 | retryWrites: true
182 | })
183 |
184 | await directMongoClient.connect()
185 | console.log(`${COLORS.green}Connected to MongoDB successfully.${COLORS.reset}`)
186 | }
187 |
188 | const obfuscateMongoUri = uri => {
189 | if (!uri || typeof uri !== 'string') return uri
190 | try {
191 | if (uri.includes('@') && uri.includes('://')) {
192 | const parts = uri.split('@')
193 | const authPart = parts[0]
194 | const restPart = parts.slice(1).join('@')
195 | const authIndex = authPart.lastIndexOf('://')
196 | if (authIndex !== -1) {
197 | const protocol = authPart.substring(0, authIndex + 3)
198 | const credentials = authPart.substring(authIndex + 3)
199 | if (credentials.includes(':')) {
200 | const [username] = credentials.split(':')
201 | return `${protocol}${username}:********@${restPart}`
202 | }
203 | }
204 | }
205 | return uri
206 | } catch (error) {
207 | return uri
208 | }
209 | }
210 |
211 | const setupTestEnvironment = async () => {
212 | await checkServerCapabilities()
213 | testDb = directMongoClient.db(TEST_DB_NAME)
214 | await cleanupTestDatabase()
215 | await setupTestData()
216 | console.log(`${COLORS.blue}Running tests against MongoDB Lens…${COLORS.reset}`)
217 | }
218 |
219 | const checkServerCapabilities = async () => {
220 | try {
221 | const adminDb = directMongoClient.db('admin')
222 |
223 | const replSetStatus = await adminDb.command({ replSetGetStatus: 1 }).catch(() => null)
224 | isReplSet = !!replSetStatus
225 | console.log(`${COLORS.blue}MongoDB instance ${isReplSet ? 'is' : 'is not'} a replica set.${COLORS.reset}`)
226 |
227 | const listShards = await adminDb.command({ listShards: 1 }).catch(() => null)
228 | isSharded = !!listShards
229 | console.log(`${COLORS.blue}MongoDB instance ${isSharded ? 'is' : 'is not'} a sharded cluster.${COLORS.reset}`)
230 | } catch (error) {
231 | console.log(`${COLORS.yellow}Not a replica set: ${error.message}${COLORS.reset}`)
232 | }
233 | }
234 |
235 | const setupTestData = async () => {
236 | try {
237 | testCollection = testDb.collection(TEST_COLLECTION_NAME)
238 | await createTestDocuments()
239 | await createTestIndexes()
240 | await createTestGeospatialData()
241 | await createTestUser()
242 | console.log(`${COLORS.green}Test data setup complete.${COLORS.reset}`)
243 | } catch (err) {
244 | console.error(`${COLORS.red}Error setting up test data: ${err.message}${COLORS.reset}`)
245 | }
246 | }
247 |
248 | const createTestDocuments = async () => {
249 | const testDocuments = Array(50).fill(0).map((_, i) => ({
250 | _id: new ObjectId(),
251 | name: `Test Document ${i}`,
252 | value: i,
253 | tags: [`tag${i % 5}`, `category${i % 3}`],
254 | isActive: i % 2 === 0,
255 | createdAt: new Date(Date.now() - i * 86400000)
256 | }))
257 |
258 | await testCollection.insertMany(testDocuments)
259 |
260 | const anotherCollection = testDb.collection(ANOTHER_TEST_COLLECTION)
261 | await anotherCollection.insertMany([
262 | { name: 'Test 1', value: 10 },
263 | { name: 'Test 2', value: 20 }
264 | ])
265 |
266 | await testCollection.insertOne({
267 | name: 'Date Test',
268 | startDate: new Date('2023-01-01'),
269 | endDate: new Date('2023-12-31')
270 | })
271 | }
272 |
273 | const createTestIndexes = async () => {
274 | await testCollection.createIndex({ name: 1 })
275 | await testCollection.createIndex({ value: -1 })
276 | await testCollection.createIndex({ tags: 1 })
277 | await testCollection.createIndex({ name: 'text', tags: 'text' })
278 | await testCollection.createIndex({ location: '2dsphere' })
279 | }
280 |
281 | const createTestGeospatialData = async () => {
282 | await testCollection.insertMany([
283 | {
284 | name: 'Geo Test 1',
285 | location: { type: 'Point', coordinates: [-73.9857, 40.7484] }
286 | },
287 | {
288 | name: 'Geo Test 2',
289 | location: { type: 'Point', coordinates: [-118.2437, 34.0522] }
290 | }
291 | ])
292 | }
293 |
294 | const createTestUser = async () => {
295 | try {
296 | await testDb.command({
297 | createUser: 'testuser',
298 | pwd: 'testpassword',
299 | roles: [{ role: 'read', db: TEST_DB_NAME }]
300 | })
301 | } catch (e) {
302 | console.log(`${COLORS.yellow}Could not create test user: ${e.message}${COLORS.reset}`)
303 | }
304 | }
305 |
306 | const runTest = async (name, testFn) => {
307 | stats.total++
308 | console.log(`${COLORS.blue}Running test: ${name}${COLORS.reset}`)
309 | try {
310 | const startTime = Date.now()
311 | await testFn()
312 | const duration = Date.now() - startTime
313 | console.log(`${COLORS.green}✓ PASS: ${name} (${duration}ms)${COLORS.reset}`)
314 | stats.passed++
315 | } catch (err) {
316 | console.error(`${COLORS.red}✗ FAIL: ${name} - ${err.message}${COLORS.reset}`)
317 | console.error(`${COLORS.red}Stack: ${err.stack}${COLORS.reset}`)
318 | stats.failed++
319 | }
320 | }
321 |
322 | const skipTest = (name, reason) => {
323 | stats.total++
324 | stats.skipped++
325 | console.log(`${COLORS.yellow}⚠ SKIP: ${name} - ${reason}${COLORS.reset}`)
326 | }
327 |
328 | const startLensServer = async () => {
329 | console.log('Starting MongoDB Lens server…')
330 |
331 | const env = {
332 | ...process.env,
333 | CONFIG_MONGO_URI: mongoUri,
334 | CONFIG_LOG_LEVEL: isDebugging ? 'verbose' : 'info',
335 | }
336 |
337 | if (testConfig.disableTokens) {
338 | env.CONFIG_DISABLE_DESTRUCTIVE_OPERATION_TOKENS = 'true'
339 | }
340 |
341 | lensProcess = spawn('node', [MONGODB_LENS_PATH], {
342 | env: env,
343 | stdio: ['pipe', 'pipe', 'pipe']
344 | })
345 |
346 | setupResponseHandling()
347 | return waitForServerStart()
348 | }
349 |
350 | const setupResponseHandling = () => {
351 | lensProcess.stdout.on('data', data => {
352 | const response = data.toString().trim()
353 | try {
354 | const parsed = JSON.parse(response)
355 | if (parsed.id && responseHandlers.has(parsed.id)) {
356 | const handler = responseHandlers.get(parsed.id)
357 | responseHandlers.delete(parsed.id)
358 | handler.resolve(parsed)
359 | }
360 | } catch (e) {
361 | console.error(`Parse error: ${e.message}`)
362 | console.error(`Response was: ${response.substring(0, 200)}${response.length > 200 ? '…' : ''}`)
363 | }
364 | })
365 |
366 | lensProcess.stderr.on('data', data => {
367 | const output = data.toString().trim()
368 | if (isDebugging)
369 | console.log(`${COLORS.gray}[SERVER] ${output.split('\n').join(`\n[SERVER] `)}${COLORS.reset}`)
370 | })
371 | }
372 |
373 | const waitForServerStart = () => {
374 | return new Promise((resolve, reject) => {
375 | const handler = data => {
376 | if (data.toString().includes('MongoDB Lens server running.')) {
377 | lensProcess.stderr.removeListener('data', handler)
378 | console.log('MongoDB Lens server started successfully.')
379 | resolve()
380 | }
381 | }
382 |
383 | lensProcess.stderr.on('data', handler)
384 | setTimeout(() => reject(new Error('Server startup timed out')), testConfig.serverStartupTimeout)
385 | })
386 | }
387 |
388 | const runLensCommand = async ({ command, params = {} }) => {
389 | if (!lensProcess) await startLensServer()
390 |
391 | const requestId = nextRequestId++
392 | const { method, methodParams } = mapToLensMethod(command, params)
393 |
394 | return new Promise((resolve, reject) => {
395 | const request = {
396 | jsonrpc: '2.0',
397 | id: requestId,
398 | method: method,
399 | params: methodParams
400 | }
401 |
402 | responseHandlers.set(requestId, { resolve, reject })
403 |
404 | if (isDebugging) console.log(`Sending request #${requestId}:`, JSON.stringify(request))
405 |
406 | lensProcess.stdin.write(JSON.stringify(request) + '\n')
407 |
408 | setTimeout(() => {
409 | if (responseHandlers.has(requestId)) {
410 | responseHandlers.delete(requestId)
411 | reject(new Error(`Request ${method} timed out after ${testConfig.requestTimeout/1000} seconds`))
412 | }
413 | }, testConfig.requestTimeout)
414 | })
415 | }
416 |
417 | const mapToLensMethod = (command, params) => {
418 | let method, methodParams
419 |
420 | switch(command) {
421 | case 'mcp.resource.get':
422 | method = 'resources/read'
423 | methodParams = { uri: params.uri }
424 | break
425 | case 'mcp.tool.invoke':
426 | method = 'tools/call'
427 | methodParams = {
428 | name: params.name,
429 | arguments: params.args || {}
430 | }
431 | break
432 | case 'mcp.prompt.start':
433 | method = 'prompts/get'
434 | methodParams = {
435 | name: params.name,
436 | arguments: params.args || {}
437 | }
438 | break
439 | case 'initialize':
440 | method = 'initialize'
441 | methodParams = params
442 | break
443 | default:
444 | method = command
445 | methodParams = params
446 | }
447 |
448 | return { method, methodParams }
449 | }
450 |
451 | const useTestDatabase = async () => {
452 | await runLensCommand({
453 | command: 'mcp.tool.invoke',
454 | params: {
455 | name: 'use-database',
456 | args: {
457 | database: TEST_DB_NAME
458 | }
459 | }
460 | })
461 | }
462 |
463 | const handleDestructiveOperationToken = async (tokenResponse, toolName, params) => {
464 | if (testConfig.disableTokens) {
465 | return assertToolSuccess(tokenResponse, 'has been permanently deleted')
466 | }
467 |
468 | assert(tokenResponse?.result?.content[0].text.includes('Confirmation code:'), 'Confirmation message not found')
469 | const tokenMatch = tokenResponse.result.content[0].text.match(/Confirmation code:\s+(\d+)/)
470 | assert(tokenMatch && tokenMatch[1], 'Confirmation code not found in text')
471 |
472 | const token = tokenMatch[1]
473 | const completeResponse = await runLensCommand({
474 | command: 'mcp.tool.invoke',
475 | params: {
476 | name: toolName,
477 | args: {
478 | ...params,
479 | token
480 | }
481 | }
482 | })
483 |
484 | return assertToolSuccess(completeResponse, 'has been permanently deleted')
485 | }
486 |
487 | const assertToolSuccess = (response, successIndicator) => {
488 | assert(response?.result?.content, 'No content in response')
489 | assert(Array.isArray(response.result.content), 'Content not an array')
490 | assert(response.result.content.length > 0, 'Empty content array')
491 | assert(response.result.content[0].text.includes(successIndicator), `Success message not found: "${successIndicator}"`)
492 | return response
493 | }
494 |
495 | const assertResourceSuccess = (response, successIndicator) => {
496 | assert(response?.result?.contents, 'No contents in response')
497 | assert(Array.isArray(response.result.contents), 'Contents not an array')
498 | assert(response.result.contents.length > 0, 'Empty contents array')
499 | assert(response.result.contents[0].text.includes(successIndicator), `Success message not found: "${successIndicator}"`)
500 | return response
501 | }
502 |
503 | const assertPromptSuccess = (response, successIndicator) => {
504 | assert(response?.result?.messages, 'No messages in response')
505 | assert(Array.isArray(response.result.messages), 'Messages not an array')
506 | assert(response.result.messages.length > 0, 'Empty messages array')
507 | assert(response.result.messages[0].content.text.includes(successIndicator), `Success message not found: "${successIndicator}"`)
508 | return response
509 | }
510 |
511 | const assert = (condition, message, context = null) => {
512 | if (condition) return
513 | if (context) console.error('CONTEXT:', JSON.stringify(context, null, 2))
514 | throw new Error(message || 'Assertion failed')
515 | }
516 |
517 | const testConnectMongodbTool = async () => {
518 | const response = await runLensCommand({
519 | command: 'mcp.tool.invoke',
520 | params: {
521 | name: 'connect-mongodb',
522 | args: {
523 | uri: mongoUri,
524 | validateConnection: 'true'
525 | }
526 | }
527 | })
528 |
529 | assertToolSuccess(response, 'Successfully connected')
530 | }
531 |
532 | const testConnectOriginalTool = async () => {
533 | await runLensCommand({
534 | command: 'mcp.tool.invoke',
535 | params: {
536 | name: 'connect-mongodb',
537 | args: {
538 | uri: mongoUri,
539 | validateConnection: 'true'
540 | }
541 | }
542 | })
543 |
544 | const response = await runLensCommand({
545 | command: 'mcp.tool.invoke',
546 | params: {
547 | name: 'connect-original',
548 | args: {
549 | validateConnection: 'true'
550 | }
551 | }
552 | })
553 |
554 | assertToolSuccess(response, 'Successfully connected')
555 | }
556 |
557 | const testAddConnectionAliasTool = async () => {
558 | const aliasName = 'test_alias'
559 | const response = await runLensCommand({
560 | command: 'mcp.tool.invoke',
561 | params: {
562 | name: 'add-connection-alias',
563 | args: {
564 | alias: aliasName,
565 | uri: mongoUri
566 | }
567 | }
568 | })
569 |
570 | assertToolSuccess(response, `Successfully added connection alias '${aliasName}'`)
571 | }
572 |
573 | const testListConnectionsTool = async () => {
574 | const aliasName = 'test_alias_for_list'
575 |
576 | await runLensCommand({
577 | command: 'mcp.tool.invoke',
578 | params: {
579 | name: 'add-connection-alias',
580 | args: {
581 | alias: aliasName,
582 | uri: mongoUri
583 | }
584 | }
585 | })
586 |
587 | const response = await runLensCommand({
588 | command: 'mcp.tool.invoke',
589 | params: {
590 | name: 'list-connections'
591 | }
592 | })
593 |
594 | assertToolSuccess(response, aliasName)
595 | }
596 |
597 | const testListDatabasesTool = async () => {
598 | const response = await runLensCommand({
599 | command: 'mcp.tool.invoke',
600 | params: {
601 | name: 'list-databases'
602 | }
603 | })
604 |
605 | assertToolSuccess(response, TEST_DB_NAME)
606 | }
607 |
608 | const testCurrentDatabaseTool = async () => {
609 | await useTestDatabase()
610 |
611 | const response = await runLensCommand({
612 | command: 'mcp.tool.invoke',
613 | params: {
614 | name: 'current-database'
615 | }
616 | })
617 |
618 | assertToolSuccess(response, `Current database: ${TEST_DB_NAME}`)
619 | }
620 |
621 | const testCreateDatabaseTool = async () => {
622 | const testDbName = `test_create_db_${Date.now()}`
623 |
624 | const response = await runLensCommand({
625 | command: 'mcp.tool.invoke',
626 | params: {
627 | name: 'create-database',
628 | args: {
629 | name: testDbName,
630 | switch: 'true',
631 | validateName: 'true'
632 | }
633 | }
634 | })
635 |
636 | assertToolSuccess(response, `Database '${testDbName}' created`)
637 |
638 | const dbs = await directMongoClient.db('admin').admin().listDatabases()
639 | const dbExists = dbs.databases.some(db => db.name === testDbName)
640 | assert(dbExists, `Created database '${testDbName}' not found in database list`)
641 |
642 | await directMongoClient.db(testDbName).dropDatabase()
643 | }
644 |
645 | const testUseDatabaseTool = async () => {
646 | const response = await runLensCommand({
647 | command: 'mcp.tool.invoke',
648 | params: {
649 | name: 'use-database',
650 | args: {
651 | database: TEST_DB_NAME
652 | }
653 | }
654 | })
655 |
656 | assertToolSuccess(response, `Switched to database: ${TEST_DB_NAME}`)
657 | }
658 |
659 | const testDropDatabaseTool = async () => {
660 | const testDbToDrop = `test_drop_db_${Date.now()}`
661 |
662 | await directMongoClient.db(testDbToDrop).collection('test').insertOne({ test: 1 })
663 |
664 | await runLensCommand({
665 | command: 'mcp.tool.invoke',
666 | params: {
667 | name: 'use-database',
668 | args: {
669 | database: testDbToDrop
670 | }
671 | }
672 | })
673 |
674 | const tokenResponse = await runLensCommand({
675 | command: 'mcp.tool.invoke',
676 | params: {
677 | name: 'drop-database',
678 | args: {
679 | name: testDbToDrop
680 | }
681 | }
682 | })
683 |
684 | await handleDestructiveOperationToken(tokenResponse, 'drop-database', { name: testDbToDrop })
685 |
686 | const dbs = await directMongoClient.db('admin').admin().listDatabases()
687 | const dbExists = dbs.databases.some(db => db.name === testDbToDrop)
688 | assert(!dbExists, `Dropped database '${testDbToDrop}' still exists`)
689 | }
690 |
691 | const testCreateUserTool = async () => {
692 | const username = `test_user_${Date.now()}`
693 |
694 | await useTestDatabase()
695 |
696 | const response = await runLensCommand({
697 | command: 'mcp.tool.invoke',
698 | params: {
699 | name: 'create-user',
700 | args: {
701 | username: username,
702 | password: 'test_password',
703 | roles: JSON.stringify([{ role: 'read', db: TEST_DB_NAME }])
704 | }
705 | }
706 | })
707 |
708 | assertToolSuccess(response, `User '${username}' created`)
709 |
710 | const usersInfo = await testDb.command({ usersInfo: username })
711 | assert(usersInfo.users.length > 0, `Created user '${username}' not found`)
712 | }
713 |
714 | const testDropUserTool = async () => {
715 | const username = `test_drop_user_${Date.now()}`
716 |
717 | await useTestDatabase()
718 |
719 | await runLensCommand({
720 | command: 'mcp.tool.invoke',
721 | params: {
722 | name: 'create-user',
723 | args: {
724 | username: username,
725 | password: 'test_password',
726 | roles: JSON.stringify([{ role: 'read', db: TEST_DB_NAME }])
727 | }
728 | }
729 | })
730 |
731 | const tokenResponse = await runLensCommand({
732 | command: 'mcp.tool.invoke',
733 | params: {
734 | name: 'drop-user',
735 | args: {
736 | username: username
737 | }
738 | }
739 | })
740 |
741 | let dropped = false
742 |
743 | if (testConfig.disableTokens) {
744 | assertToolSuccess(tokenResponse, 'dropped successfully')
745 | dropped = true
746 | } else {
747 | assert(tokenResponse?.result?.content[0].text.includes('Confirmation code:'), 'Confirmation message not found')
748 |
749 | const tokenMatch = tokenResponse.result.content[0].text.match(/Confirmation code:\s+(\d+)/)
750 | assert(tokenMatch && tokenMatch[1], 'Confirmation code not found in text')
751 |
752 | const token = tokenMatch[1]
753 |
754 | const dropResponse = await runLensCommand({
755 | command: 'mcp.tool.invoke',
756 | params: {
757 | name: 'drop-user',
758 | args: {
759 | username: username,
760 | token
761 | }
762 | }
763 | })
764 |
765 | assertToolSuccess(dropResponse, 'dropped successfully')
766 | dropped = true
767 | }
768 |
769 | if (dropped) {
770 | const usersInfo = await testDb.command({ usersInfo: username })
771 | assert(usersInfo.users.length === 0, `Dropped user '${username}' still exists`)
772 | }
773 | }
774 |
775 | const testListCollectionsTool = async () => {
776 | await useTestDatabase()
777 |
778 | const response = await runLensCommand({
779 | command: 'mcp.tool.invoke',
780 | params: {
781 | name: 'list-collections'
782 | }
783 | })
784 |
785 | assertToolSuccess(response, TEST_COLLECTION_NAME)
786 | }
787 |
788 | const testCreateCollectionTool = async () => {
789 | const collectionName = `test_create_coll_${Date.now()}`
790 |
791 | await useTestDatabase()
792 |
793 | const response = await runLensCommand({
794 | command: 'mcp.tool.invoke',
795 | params: {
796 | name: 'create-collection',
797 | args: {
798 | name: collectionName,
799 | options: JSON.stringify({})
800 | }
801 | }
802 | })
803 |
804 | assertToolSuccess(response, `Collection '${collectionName}' created`)
805 |
806 | const collections = await testDb.listCollections().toArray()
807 | const collExists = collections.some(coll => coll.name === collectionName)
808 | assert(collExists, `Created collection '${collectionName}' not found`)
809 | }
810 |
811 | const testDropCollectionTool = async () => {
812 | const collectionName = `test_drop_coll_${Date.now()}`
813 |
814 | await useTestDatabase()
815 |
816 | await runLensCommand({
817 | command: 'mcp.tool.invoke',
818 | params: {
819 | name: 'create-collection',
820 | args: {
821 | name: collectionName,
822 | options: JSON.stringify({})
823 | }
824 | }
825 | })
826 |
827 | const tokenResponse = await runLensCommand({
828 | command: 'mcp.tool.invoke',
829 | params: {
830 | name: 'drop-collection',
831 | args: {
832 | name: collectionName
833 | }
834 | }
835 | })
836 |
837 | await handleDestructiveOperationToken(tokenResponse, 'drop-collection', { name: collectionName })
838 |
839 | const collections = await testDb.listCollections().toArray()
840 | const collExists = collections.some(coll => coll.name === collectionName)
841 | assert(!collExists, `Dropped collection '${collectionName}' still exists`)
842 | }
843 |
844 | const testRenameCollectionTool = async () => {
845 | const oldName = `test_rename_old_${Date.now()}`
846 | const newName = `test_rename_new_${Date.now()}`
847 |
848 | await useTestDatabase()
849 |
850 | await runLensCommand({
851 | command: 'mcp.tool.invoke',
852 | params: {
853 | name: 'create-collection',
854 | args: {
855 | name: oldName,
856 | options: JSON.stringify({})
857 | }
858 | }
859 | })
860 |
861 | const response = await runLensCommand({
862 | command: 'mcp.tool.invoke',
863 | params: {
864 | name: 'rename-collection',
865 | args: {
866 | oldName: oldName,
867 | newName: newName,
868 | dropTarget: 'false'
869 | }
870 | }
871 | })
872 |
873 | assertToolSuccess(response, `renamed to '${newName}'`)
874 |
875 | const collections = await testDb.listCollections().toArray()
876 | const oldExists = collections.some(coll => coll.name === oldName)
877 | const newExists = collections.some(coll => coll.name === newName)
878 | assert(!oldExists, `Old collection '${oldName}' still exists`)
879 | assert(newExists, `New collection '${newName}' not found`)
880 | }
881 |
882 | const testValidateCollectionTool = async () => {
883 | await useTestDatabase()
884 |
885 | const response = await runLensCommand({
886 | command: 'mcp.tool.invoke',
887 | params: {
888 | name: 'validate-collection',
889 | args: {
890 | collection: TEST_COLLECTION_NAME,
891 | full: 'false'
892 | }
893 | }
894 | })
895 |
896 | assertToolSuccess(response, 'Validation Results')
897 |
898 | const content = response.result.content[0].text
899 | assert(content.includes('Collection'), 'Collection name not found')
900 | assert(content.includes('Valid:') || content.includes('Records Validated:'), 'Validation results not found')
901 | }
902 |
903 | const testDistinctValuesTool = async () => {
904 | await useTestDatabase()
905 |
906 | const response = await runLensCommand({
907 | command: 'mcp.tool.invoke',
908 | params: {
909 | name: 'distinct-values',
910 | args: {
911 | collection: TEST_COLLECTION_NAME,
912 | field: 'tags',
913 | filter: '{}'
914 | }
915 | }
916 | })
917 |
918 | assertToolSuccess(response, 'Distinct values for field')
919 |
920 | const content = response.result.content[0].text
921 | assert(content.includes('tag') || content.includes('category'), 'Expected distinct tag values not found')
922 | }
923 |
924 | const testFindDocumentsTool = async () => {
925 | await useTestDatabase()
926 |
927 | const response = await runLensCommand({
928 | command: 'mcp.tool.invoke',
929 | params: {
930 | name: 'find-documents',
931 | args: {
932 | collection: TEST_COLLECTION_NAME,
933 | filter: '{"value": {"$lt": 10}}',
934 | limit: 5
935 | }
936 | }
937 | })
938 |
939 | const content = response.result.content[0].text
940 | assert(content.includes('"value":'), 'Value field not found')
941 | assert(content.includes('"name":'), 'Name field not found')
942 | }
943 |
944 | const testCountDocumentsTool = async () => {
945 | await useTestDatabase()
946 |
947 | const response = await runLensCommand({
948 | command: 'mcp.tool.invoke',
949 | params: {
950 | name: 'count-documents',
951 | args: {
952 | collection: TEST_COLLECTION_NAME,
953 | filter: '{"isActive": true}'
954 | }
955 | }
956 | })
957 |
958 | assertToolSuccess(response, 'Count:')
959 | assert(/Count: \d+ document/.test(response.result.content[0].text), 'Count number not found')
960 | }
961 |
962 | const testInsertDocumentTool = async () => {
963 | await useTestDatabase()
964 |
965 | const testDoc = {
966 | name: 'Test Insert',
967 | value: 999,
968 | tags: ['test', 'insert'],
969 | isActive: true,
970 | createdAt: new Date().toISOString()
971 | }
972 |
973 | const response = await runLensCommand({
974 | command: 'mcp.tool.invoke',
975 | params: {
976 | name: 'insert-document',
977 | args: {
978 | collection: TEST_COLLECTION_NAME,
979 | document: JSON.stringify(testDoc)
980 | }
981 | }
982 | })
983 |
984 | assertToolSuccess(response, 'inserted successfully')
985 |
986 | const countResponse = await runLensCommand({
987 | command: 'mcp.tool.invoke',
988 | params: {
989 | name: 'count-documents',
990 | args: {
991 | collection: TEST_COLLECTION_NAME,
992 | filter: '{"value": 999}'
993 | }
994 | }
995 | })
996 |
997 | assert(countResponse.result.content[0].text.includes('Count: 1'), 'Inserted document not found')
998 | }
999 |
1000 | const testUpdateDocumentTool = async () => {
1001 | await useTestDatabase()
1002 |
1003 | const testDoc = {
1004 | name: 'Update Test',
1005 | value: 888,
1006 | tags: ['update'],
1007 | isActive: true
1008 | }
1009 |
1010 | await runLensCommand({
1011 | command: 'mcp.tool.invoke',
1012 | params: {
1013 | name: 'insert-document',
1014 | args: {
1015 | collection: TEST_COLLECTION_NAME,
1016 | document: JSON.stringify(testDoc)
1017 | }
1018 | }
1019 | })
1020 |
1021 | const response = await runLensCommand({
1022 | command: 'mcp.tool.invoke',
1023 | params: {
1024 | name: 'update-document',
1025 | args: {
1026 | collection: TEST_COLLECTION_NAME,
1027 | filter: '{"value": 888}',
1028 | update: '{"$set": {"name": "Updated Test", "tags": ["updated"]}}'
1029 | }
1030 | }
1031 | })
1032 |
1033 | assertToolSuccess(response, 'Matched:')
1034 | assert(response.result.content[0].text.includes('Modified:'), 'Modified count not found')
1035 |
1036 | const findResponse = await runLensCommand({
1037 | command: 'mcp.tool.invoke',
1038 | params: {
1039 | name: 'find-documents',
1040 | args: {
1041 | collection: TEST_COLLECTION_NAME,
1042 | filter: '{"value": 888}'
1043 | }
1044 | }
1045 | })
1046 |
1047 | const responseText = findResponse.result.content[0].text
1048 | assert(responseText.includes('"name"') && responseText.includes('Updated Test'), 'Updated name not found')
1049 | assert(responseText.includes('"tags"') && responseText.includes('updated'), 'Updated tags not found')
1050 | }
1051 |
1052 | const testDeleteDocumentTool = async () => {
1053 | await useTestDatabase()
1054 |
1055 | const testDoc = {
1056 | name: 'Delete Test',
1057 | value: 777,
1058 | tags: ['delete'],
1059 | isActive: true
1060 | }
1061 |
1062 | await runLensCommand({
1063 | command: 'mcp.tool.invoke',
1064 | params: {
1065 | name: 'insert-document',
1066 | args: {
1067 | collection: TEST_COLLECTION_NAME,
1068 | document: JSON.stringify(testDoc)
1069 | }
1070 | }
1071 | })
1072 |
1073 | const countBeforeResponse = await runLensCommand({
1074 | command: 'mcp.tool.invoke',
1075 | params: {
1076 | name: 'count-documents',
1077 | args: {
1078 | collection: TEST_COLLECTION_NAME,
1079 | filter: '{"value": 777}'
1080 | }
1081 | }
1082 | })
1083 |
1084 | assert(countBeforeResponse.result.content[0].text.includes('Count: 1'), 'Test document not found before delete')
1085 |
1086 | const tokenResponse = await runLensCommand({
1087 | command: 'mcp.tool.invoke',
1088 | params: {
1089 | name: 'delete-document',
1090 | args: {
1091 | collection: TEST_COLLECTION_NAME,
1092 | filter: '{"value": 777}',
1093 | many: 'false'
1094 | }
1095 | }
1096 | })
1097 |
1098 | if (testConfig.disableTokens) {
1099 | assertToolSuccess(tokenResponse, 'Successfully deleted')
1100 | } else {
1101 | assert(tokenResponse?.result?.content[0].text.includes('Confirmation code:'), 'Confirmation message not found')
1102 |
1103 | const tokenMatch = tokenResponse.result.content[0].text.match(/Confirmation code:\s+(\d+)/)
1104 | assert(tokenMatch && tokenMatch[1], 'Confirmation code not found in text')
1105 |
1106 | const token = tokenMatch[1]
1107 |
1108 | const deleteResponse = await runLensCommand({
1109 | command: 'mcp.tool.invoke',
1110 | params: {
1111 | name: 'delete-document',
1112 | args: {
1113 | collection: TEST_COLLECTION_NAME,
1114 | filter: '{"value": 777}',
1115 | many: 'false',
1116 | token
1117 | }
1118 | }
1119 | })
1120 |
1121 | assertToolSuccess(deleteResponse, 'Successfully deleted')
1122 | }
1123 |
1124 | const countResponse = await runLensCommand({
1125 | command: 'mcp.tool.invoke',
1126 | params: {
1127 | name: 'count-documents',
1128 | args: {
1129 | collection: TEST_COLLECTION_NAME,
1130 | filter: '{"value": 777}'
1131 | }
1132 | }
1133 | })
1134 |
1135 | assert(countResponse.result.content[0].text.includes('Count: 0'), 'Document still exists after deletion')
1136 | }
1137 |
1138 | const testAggregateDataTool = async () => {
1139 | await useTestDatabase()
1140 |
1141 | const pipeline = [
1142 | { $match: { isActive: true } },
1143 | { $group: { _id: null, avgValue: { $avg: '$value' }, count: { $sum: 1 } } }
1144 | ]
1145 |
1146 | const response = await runLensCommand({
1147 | command: 'mcp.tool.invoke',
1148 | params: {
1149 | name: 'aggregate-data',
1150 | args: {
1151 | collection: TEST_COLLECTION_NAME,
1152 | pipeline: JSON.stringify(pipeline)
1153 | }
1154 | }
1155 | })
1156 |
1157 | const content = response.result.content[0].text
1158 | assert(content.includes('"avgValue":'), 'Average value not found')
1159 | assert(content.includes('"count":'), 'Count not found')
1160 | }
1161 |
1162 | const testCreateIndexTool = async () => {
1163 | await useTestDatabase()
1164 |
1165 | const indexName = `test_index_${Date.now()}`
1166 |
1167 | const response = await runLensCommand({
1168 | command: 'mcp.tool.invoke',
1169 | params: {
1170 | name: 'create-index',
1171 | args: {
1172 | collection: TEST_COLLECTION_NAME,
1173 | keys: '{"createdAt": 1}',
1174 | options: JSON.stringify({ name: indexName })
1175 | }
1176 | }
1177 | })
1178 |
1179 | assertToolSuccess(response, 'Index created')
1180 | assert(response.result.content[0].text.includes(indexName), 'Index name not found')
1181 | }
1182 |
1183 | const testDropIndexTool = async () => {
1184 | await useTestDatabase()
1185 |
1186 | const indexName = `test_drop_index_${Date.now()}`
1187 |
1188 | await runLensCommand({
1189 | command: 'mcp.tool.invoke',
1190 | params: {
1191 | name: 'create-index',
1192 | args: {
1193 | collection: TEST_COLLECTION_NAME,
1194 | keys: '{"testField": 1}',
1195 | options: JSON.stringify({ name: indexName })
1196 | }
1197 | }
1198 | })
1199 |
1200 | const tokenResponse = await runLensCommand({
1201 | command: 'mcp.tool.invoke',
1202 | params: {
1203 | name: 'drop-index',
1204 | args: {
1205 | collection: TEST_COLLECTION_NAME,
1206 | indexName: indexName
1207 | }
1208 | }
1209 | })
1210 |
1211 | let dropped = false
1212 |
1213 | if (testConfig.disableTokens) {
1214 | assertToolSuccess(tokenResponse, 'dropped from collection')
1215 | dropped = true
1216 | } else {
1217 | assert(tokenResponse?.result?.content[0].text.includes('Confirmation code:'), 'Confirmation message not found')
1218 |
1219 | const tokenMatch = tokenResponse.result.content[0].text.match(/Confirmation code:\s+(\d+)/)
1220 | assert(tokenMatch && tokenMatch[1], 'Confirmation code not found in text')
1221 |
1222 | const token = tokenMatch[1]
1223 |
1224 | const dropResponse = await runLensCommand({
1225 | command: 'mcp.tool.invoke',
1226 | params: {
1227 | name: 'drop-index',
1228 | args: {
1229 | collection: TEST_COLLECTION_NAME,
1230 | indexName: indexName,
1231 | token
1232 | }
1233 | }
1234 | })
1235 |
1236 | assertToolSuccess(dropResponse, 'dropped from collection')
1237 | dropped = true
1238 | }
1239 |
1240 | if (dropped) {
1241 | const indexes = await testDb.collection(TEST_COLLECTION_NAME).indexes()
1242 | const indexStillExists = indexes.some(idx => idx.name === indexName)
1243 | assert(!indexStillExists, `Dropped index '${indexName}' still exists`)
1244 | }
1245 | }
1246 |
1247 | const testAnalyzeSchemaTool = async () => {
1248 | await useTestDatabase()
1249 |
1250 | const response = await runLensCommand({
1251 | command: 'mcp.tool.invoke',
1252 | params: {
1253 | name: 'analyze-schema',
1254 | args: {
1255 | collection: TEST_COLLECTION_NAME,
1256 | sampleSize: 20
1257 | }
1258 | }
1259 | })
1260 |
1261 | assertToolSuccess(response, 'Schema for')
1262 | const content = response.result.content[0].text
1263 | assert(content.includes('name:'), 'Name field not found')
1264 | assert(content.includes('value:'), 'Value field not found')
1265 | assert(content.includes('tags:'), 'Tags field not found')
1266 | }
1267 |
1268 | const testGenerateSchemaValidatorTool = async () => {
1269 | await useTestDatabase()
1270 |
1271 | const response = await runLensCommand({
1272 | command: 'mcp.tool.invoke',
1273 | params: {
1274 | name: 'generate-schema-validator',
1275 | args: {
1276 | collection: TEST_COLLECTION_NAME,
1277 | strictness: 'moderate'
1278 | }
1279 | }
1280 | })
1281 |
1282 | assertToolSuccess(response, 'MongoDB JSON Schema Validator')
1283 | const content = response.result.content[0].text
1284 | assert(content.includes('$jsonSchema'), 'JSON Schema not found')
1285 | assert(content.includes('properties'), 'Properties not found')
1286 | }
1287 |
1288 | const testCompareSchemasTool = async () => {
1289 | await useTestDatabase()
1290 |
1291 | const response = await runLensCommand({
1292 | command: 'mcp.tool.invoke',
1293 | params: {
1294 | name: 'compare-schemas',
1295 | args: {
1296 | sourceCollection: TEST_COLLECTION_NAME,
1297 | targetCollection: ANOTHER_TEST_COLLECTION,
1298 | sampleSize: 10
1299 | }
1300 | }
1301 | })
1302 |
1303 | assertToolSuccess(response, 'Schema Comparison')
1304 | const content = response.result.content[0].text
1305 | assert(content.includes('Source Collection'), 'Source info not found')
1306 | assert(content.includes('Target Collection'), 'Target info not found')
1307 | }
1308 |
1309 | const testExplainQueryTool = async () => {
1310 | await useTestDatabase()
1311 |
1312 | const response = await runLensCommand({
1313 | command: 'mcp.tool.invoke',
1314 | params: {
1315 | name: 'explain-query',
1316 | args: {
1317 | collection: TEST_COLLECTION_NAME,
1318 | filter: '{"value": {"$gt": 10}}',
1319 | verbosity: 'queryPlanner'
1320 | }
1321 | }
1322 | })
1323 |
1324 | assertToolSuccess(response, 'Query Explanation')
1325 | assert(response.result.content[0].text.includes('Query Planner'), 'Query planner not found')
1326 | }
1327 |
1328 | const testAnalyzeQueryPatternsTool = async () => {
1329 | await useTestDatabase()
1330 |
1331 | const response = await runLensCommand({
1332 | command: 'mcp.tool.invoke',
1333 | params: {
1334 | name: 'analyze-query-patterns',
1335 | args: {
1336 | collection: TEST_COLLECTION_NAME,
1337 | duration: 1
1338 | }
1339 | }
1340 | })
1341 |
1342 | assertToolSuccess(response, 'Query Pattern Analysis')
1343 |
1344 | const content = response.result.content[0].text
1345 | assert(
1346 | content.includes('Analysis') ||
1347 | content.includes('Recommendations') ||
1348 | content.includes('Queries') ||
1349 | content.includes('Patterns'),
1350 | 'No analysis content found in response'
1351 | )
1352 | }
1353 |
1354 | const testGetStatsTool = async () => {
1355 | await useTestDatabase()
1356 |
1357 | const dbResponse = await runLensCommand({
1358 | command: 'mcp.tool.invoke',
1359 | params: {
1360 | name: 'get-stats',
1361 | args: {
1362 | target: 'database'
1363 | }
1364 | }
1365 | })
1366 |
1367 | assertToolSuccess(dbResponse, 'Statistics')
1368 |
1369 | const collResponse = await runLensCommand({
1370 | command: 'mcp.tool.invoke',
1371 | params: {
1372 | name: 'get-stats',
1373 | args: {
1374 | target: 'collection',
1375 | name: TEST_COLLECTION_NAME
1376 | }
1377 | }
1378 | })
1379 |
1380 | assertToolSuccess(collResponse, 'Statistics')
1381 | assert(collResponse.result.content[0].text.includes('Document Count'), 'Document count not found')
1382 | }
1383 |
1384 | const testBulkOperationsTool = async () => {
1385 | await useTestDatabase()
1386 |
1387 | const operations = [
1388 | { insertOne: { document: { name: 'Bulk Insert 1', value: 1001 } } },
1389 | { insertOne: { document: { name: 'Bulk Insert 2', value: 1002 } } },
1390 | { updateOne: { filter: { name: 'Bulk Insert 1' }, update: { $set: { updated: true } } } }
1391 | ]
1392 |
1393 | const response = await runLensCommand({
1394 | command: 'mcp.tool.invoke',
1395 | params: {
1396 | name: 'bulk-operations',
1397 | args: {
1398 | collection: TEST_COLLECTION_NAME,
1399 | operations: JSON.stringify(operations),
1400 | ordered: 'true'
1401 | }
1402 | }
1403 | })
1404 |
1405 | assertToolSuccess(response, 'Bulk Operations Results')
1406 | assert(response.result.content[0].text.includes('Inserted:'), 'Insert count not found')
1407 | }
1408 |
1409 | const testCreateTimeseriesCollectionTool = async () => {
1410 | try {
1411 | const adminDb = directMongoClient.db('admin')
1412 | const serverInfo = await adminDb.command({ buildInfo: 1 })
1413 | const version = serverInfo.version.split('.').map(Number)
1414 |
1415 | if (version[0] < 5) {
1416 | return skipTest('create-timeseries Tool', 'MongoDB version 5.0+ required for time series collections')
1417 | }
1418 | } catch (e) {
1419 | return skipTest('create-timeseries Tool', `Could not determine MongoDB version: ${e.message}`)
1420 | }
1421 |
1422 | await useTestDatabase()
1423 |
1424 | const collectionName = `timeseries_${Date.now()}`
1425 |
1426 | const response = await runLensCommand({
1427 | command: 'mcp.tool.invoke',
1428 | params: {
1429 | name: 'create-timeseries',
1430 | args: {
1431 | name: collectionName,
1432 | timeField: 'timestamp',
1433 | metaField: 'metadata',
1434 | granularity: 'seconds'
1435 | }
1436 | }
1437 | })
1438 |
1439 | assertToolSuccess(response, 'Time series collection')
1440 | assert(response.result.content[0].text.includes('created'), 'Created confirmation not found')
1441 |
1442 | const collections = await testDb.listCollections({name: collectionName}).toArray()
1443 | assert(collections.length > 0, `Timeseries collection '${collectionName}' not found`)
1444 | assert(collections[0].options?.timeseries, 'Collection is not configured as timeseries')
1445 | }
1446 |
1447 | const testCollationQueryTool = async () => {
1448 | await useTestDatabase()
1449 |
1450 | const collationTestDocs = [
1451 | { name: 'café', language: 'French', rank: 1 },
1452 | { name: 'cafe', language: 'English', rank: 2 },
1453 | { name: 'CAFE', language: 'English', rank: 3 }
1454 | ]
1455 |
1456 | await testDb.collection(TEST_COLLECTION_NAME).insertMany(collationTestDocs)
1457 |
1458 | console.log(`${COLORS.blue}Verifying collation test documents were inserted…${COLORS.reset}`)
1459 | const testDocs = await testDb.collection(TEST_COLLECTION_NAME)
1460 | .find({ name: { $in: ['café', 'cafe', 'CAFE'] } })
1461 | .toArray()
1462 | console.log(`${COLORS.blue}Found ${testDocs.length} collation test documents${COLORS.reset}`)
1463 |
1464 | const response = await runLensCommand({
1465 | command: 'mcp.tool.invoke',
1466 | params: {
1467 | name: 'collation-query',
1468 | args: {
1469 | collection: TEST_COLLECTION_NAME,
1470 | filter: '{"name": "cafe"}',
1471 | locale: 'en',
1472 | strength: 1,
1473 | caseLevel: 'false'
1474 | }
1475 | }
1476 | })
1477 |
1478 | const responseText = response.result.content[0].text
1479 | assert(
1480 | responseText.includes('café') ||
1481 | responseText.includes('cafe') ||
1482 | responseText.includes('CAFE') ||
1483 | responseText.includes('collation') ||
1484 | responseText.includes('Locale') ||
1485 | responseText.includes('Found'),
1486 | 'Collation results not found'
1487 | )
1488 | }
1489 |
1490 | const testTextSearchTool = async () => {
1491 | await useTestDatabase()
1492 |
1493 | const response = await runLensCommand({
1494 | command: 'mcp.tool.invoke',
1495 | params: {
1496 | name: 'text-search',
1497 | args: {
1498 | collection: TEST_COLLECTION_NAME,
1499 | searchText: 'test',
1500 | limit: 5
1501 | }
1502 | }
1503 | })
1504 |
1505 | const responseText = response.result.content[0].text
1506 | assert(
1507 | responseText.includes('Found') ||
1508 | responseText.includes('No text index found'),
1509 | 'Text search results or index message not found'
1510 | )
1511 | }
1512 |
1513 | const testGeoQueryTool = async () => {
1514 | await useTestDatabase()
1515 |
1516 | const geometry = {
1517 | type: 'Point',
1518 | coordinates: [-74, 40.7]
1519 | }
1520 |
1521 | const response = await runLensCommand({
1522 | command: 'mcp.tool.invoke',
1523 | params: {
1524 | name: 'geo-query',
1525 | args: {
1526 | collection: TEST_COLLECTION_NAME,
1527 | operator: 'near',
1528 | field: 'location',
1529 | geometry: JSON.stringify(geometry),
1530 | maxDistance: 2000000,
1531 | limit: 10
1532 | }
1533 | }
1534 | })
1535 |
1536 | assert(response?.result?.content, 'No content in response')
1537 | assert(Array.isArray(response.result.content), 'Content not an array')
1538 | assert(response.result.content.length > 0, 'Empty content array')
1539 |
1540 | const responseText = response.result.content[0].text
1541 | assert(
1542 | responseText.includes('coordinates') ||
1543 | responseText.includes('location') ||
1544 | responseText.includes('Geo Test'),
1545 | 'Geospatial data not found in results'
1546 | )
1547 | }
1548 |
1549 | const testTransactionTool = async () => {
1550 | if (!isReplSet) {
1551 | return skipTest('transaction Tool', 'MongoDB not in replica set mode - transactions require replica set')
1552 | }
1553 |
1554 | await useTestDatabase()
1555 |
1556 | const operations = [
1557 | {
1558 | operation: 'insert',
1559 | collection: TEST_COLLECTION_NAME,
1560 | document: { name: 'Transaction Test 1', value: 1 }
1561 | },
1562 | {
1563 | operation: 'insert',
1564 | collection: TEST_COLLECTION_NAME,
1565 | document: { name: 'Transaction Test 2', value: 2 }
1566 | },
1567 | {
1568 | operation: 'update',
1569 | collection: TEST_COLLECTION_NAME,
1570 | filter: { name: 'Transaction Test 1' },
1571 | update: { $set: { updated: true } }
1572 | }
1573 | ]
1574 |
1575 | const response = await runLensCommand({
1576 | command: 'mcp.tool.invoke',
1577 | params: {
1578 | name: 'transaction',
1579 | args: {
1580 | operations: JSON.stringify(operations)
1581 | }
1582 | }
1583 | })
1584 |
1585 | assert(response?.result?.content, 'No content in response')
1586 | assert(Array.isArray(response.result.content), 'Content not an array')
1587 | assert(response.result.content.length > 0, 'Empty content array')
1588 |
1589 | const responseText = response.result.content[0].text
1590 | assert(
1591 | responseText.includes('Transaction') &&
1592 | (responseText.includes('Step') || responseText.includes('committed')),
1593 | 'Transaction results not found'
1594 | )
1595 | }
1596 |
1597 | const testWatchChangesTool = async () => {
1598 | if (!isReplSet) {
1599 | return skipTest('watch-changes Tool', 'MongoDB not in replica set mode - change streams require replica set')
1600 | }
1601 |
1602 | await useTestDatabase()
1603 |
1604 | const response = await runLensCommand({
1605 | command: 'mcp.tool.invoke',
1606 | params: {
1607 | name: 'watch-changes',
1608 | args: {
1609 | collection: TEST_COLLECTION_NAME,
1610 | operations: JSON.stringify(['insert', 'update', 'delete']),
1611 | duration: 2,
1612 | fullDocument: 'false'
1613 | }
1614 | }
1615 | })
1616 |
1617 | assert(response?.result?.content, 'No content in response')
1618 | assert(Array.isArray(response.result.content), 'Content not an array')
1619 | assert(response.result.content.length > 0, 'Empty content array')
1620 |
1621 | const responseText = response.result.content[0].text
1622 | assert(
1623 | responseText.includes('changes detected') ||
1624 | responseText.includes('No changes detected'),
1625 | 'Change stream results not found'
1626 | )
1627 | }
1628 |
1629 | const testGridFSOperationTool = async () => {
1630 | await useTestDatabase()
1631 |
1632 | try {
1633 | const bucket = new mongodb.GridFSBucket(testDb)
1634 | const fileContent = Buffer.from('GridFS test file content')
1635 |
1636 | const uploadStream = bucket.openUploadStream('test-gridfs-file.txt')
1637 | uploadStream.write(fileContent)
1638 | uploadStream.end()
1639 |
1640 | await new Promise(resolve => uploadStream.on('finish', resolve))
1641 | } catch (e) {
1642 | console.error(`Failed to create GridFS test file: ${e.message}`)
1643 | }
1644 |
1645 | const response = await runLensCommand({
1646 | command: 'mcp.tool.invoke',
1647 | params: {
1648 | name: 'gridfs-operation',
1649 | args: {
1650 | operation: 'list',
1651 | bucket: 'fs',
1652 | limit: 10
1653 | }
1654 | }
1655 | })
1656 |
1657 | assert(response?.result?.content, 'No content in response')
1658 | assert(Array.isArray(response.result.content), 'Content not an array')
1659 | assert(response.result.content.length > 0, 'Empty content array')
1660 |
1661 | const responseText = response.result.content[0].text
1662 | assert(
1663 | responseText.includes('GridFS') ||
1664 | responseText.includes('Filename') ||
1665 | responseText.includes('Size:'),
1666 | 'GridFS data not found in response'
1667 | )
1668 | }
1669 |
1670 | const testClearCacheTool = async () => {
1671 | await useTestDatabase()
1672 |
1673 | const response = await runLensCommand({
1674 | command: 'mcp.tool.invoke',
1675 | params: {
1676 | name: 'clear-cache',
1677 | args: {
1678 | target: 'all'
1679 | }
1680 | }
1681 | })
1682 |
1683 | assertToolSuccess(response, 'cleared')
1684 | }
1685 |
1686 | const testShardStatusTool = async () => {
1687 | if (!isSharded) {
1688 | return skipTest('shard-status Tool', 'MongoDB not in sharded cluster mode - sharding features require sharded deployment')
1689 | }
1690 |
1691 | await useTestDatabase()
1692 |
1693 | const response = await runLensCommand({
1694 | command: 'mcp.tool.invoke',
1695 | params: {
1696 | name: 'shard-status',
1697 | args: {
1698 | target: 'database'
1699 | }
1700 | }
1701 | })
1702 |
1703 | assert(response?.result?.content, 'No content in response')
1704 | assert(Array.isArray(response.result.content), 'Content not an array')
1705 | assert(response.result.content.length > 0, 'Empty content array')
1706 |
1707 | const responseText = response.result.content[0].text
1708 | assert(
1709 | responseText.includes('Sharding Status') ||
1710 | responseText.includes('sharded cluster') ||
1711 | responseText.includes('Sharding is not enabled') ||
1712 | responseText.includes('not a sharded cluster') ||
1713 | responseText.includes('not running with sharding'),
1714 | 'Sharding status or message not found'
1715 | )
1716 | }
1717 |
1718 | const testExportDataTool = async () => {
1719 | await useTestDatabase()
1720 |
1721 | const jsonResponse = await runLensCommand({
1722 | command: 'mcp.tool.invoke',
1723 | params: {
1724 | name: 'export-data',
1725 | args: {
1726 | collection: TEST_COLLECTION_NAME,
1727 | filter: '{"value": {"$lt": 10}}',
1728 | format: 'json',
1729 | limit: 5
1730 | }
1731 | }
1732 | })
1733 |
1734 | assert(jsonResponse?.result?.content, 'No content in JSON export response')
1735 | assert(Array.isArray(jsonResponse.result.content), 'JSON export content not an array')
1736 | assert(jsonResponse.result.content.length > 0, 'Empty JSON export content array')
1737 |
1738 | console.log(`${COLORS.blue}JSON export response first 100 chars: ${jsonResponse.result.content[0].text.substring(0, 100)}${COLORS.reset}`)
1739 |
1740 | const jsonText = jsonResponse.result.content[0].text
1741 | assert(
1742 | jsonText.includes('{') && jsonText.includes('}'),
1743 | 'JSON content not found in export'
1744 | )
1745 |
1746 | const csvResponse = await runLensCommand({
1747 | command: 'mcp.tool.invoke',
1748 | params: {
1749 | name: 'export-data',
1750 | args: {
1751 | collection: TEST_COLLECTION_NAME,
1752 | filter: '{"value": {"$lt": 10}}',
1753 | format: 'csv',
1754 | fields: 'name,value,isActive',
1755 | limit: 5
1756 | }
1757 | }
1758 | })
1759 |
1760 | assert(csvResponse?.result?.content, 'No content in CSV export response')
1761 | assert(Array.isArray(csvResponse.result.content), 'CSV export content not an array')
1762 | assert(csvResponse.result.content.length > 0, 'Empty CSV export content array')
1763 |
1764 | console.log(`${COLORS.blue}CSV export response first 100 chars: ${csvResponse.result.content[0].text.substring(0, 100)}${COLORS.reset}`)
1765 |
1766 | assert(csvResponse.result.content[0].text.includes('name') &&
1767 | csvResponse.result.content[0].text.includes('value'),
1768 | 'CSV headers not found')
1769 | }
1770 |
1771 | const testDatabasesResource = async () => {
1772 | const response = await runLensCommand({
1773 | command: 'mcp.resource.get',
1774 | params: { uri: 'mongodb://databases' }
1775 | })
1776 |
1777 | assertResourceSuccess(response, 'Databases')
1778 | assert(response.result.contents[0].text.includes(TEST_DB_NAME), 'Test database not found')
1779 | }
1780 |
1781 | const testCollectionsResource = async () => {
1782 | await useTestDatabase()
1783 |
1784 | const response = await runLensCommand({
1785 | command: 'mcp.resource.get',
1786 | params: { uri: 'mongodb://collections' }
1787 | })
1788 |
1789 | assertResourceSuccess(response, `Collections in ${TEST_DB_NAME}`)
1790 | assert(response.result.contents[0].text.includes(TEST_COLLECTION_NAME), 'Test collection not found')
1791 | }
1792 |
1793 | const testDatabaseUsersResource = async () => {
1794 | const response = await runLensCommand({
1795 | command: 'mcp.resource.get',
1796 | params: { uri: 'mongodb://database/users' }
1797 | })
1798 |
1799 | assert(response?.result?.contents, 'No contents in response')
1800 | assert(Array.isArray(response.result.contents), 'Contents not an array')
1801 | assert(response.result.contents.length > 0, 'Empty contents array')
1802 | assert(
1803 | response.result.contents[0].text.includes('Users in database') ||
1804 | response.result.contents[0].text.includes('Could not retrieve user information'),
1805 | 'Users title or permission message not found'
1806 | )
1807 | }
1808 |
1809 | const testDatabaseTriggersResource = async () => {
1810 | const response = await runLensCommand({
1811 | command: 'mcp.resource.get',
1812 | params: { uri: 'mongodb://database/triggers' }
1813 | })
1814 |
1815 | assert(response?.result?.contents, 'No contents in response')
1816 | assert(Array.isArray(response.result.contents), 'Contents not an array')
1817 | assert(response.result.contents.length > 0, 'Empty contents array')
1818 | assert(
1819 | response.result.contents[0].text.includes('Change Stream') ||
1820 | response.result.contents[0].text.includes('Trigger'),
1821 | 'Triggers info not found'
1822 | )
1823 | }
1824 |
1825 | const testStoredFunctionsResource = async () => {
1826 | const response = await runLensCommand({
1827 | command: 'mcp.resource.get',
1828 | params: { uri: 'mongodb://database/functions' }
1829 | })
1830 |
1831 | assert(response?.result?.contents, 'No contents in response')
1832 | assert(Array.isArray(response.result.contents), 'Contents not an array')
1833 | assert(response.result.contents.length > 0, 'Empty contents array')
1834 | assert(
1835 | response.result.contents[0].text.includes('Stored Functions') ||
1836 | response.result.contents[0].text.includes('No stored JavaScript functions'),
1837 | 'Stored functions title or empty message not found'
1838 | )
1839 | }
1840 |
1841 | const testCollectionSchemaResource = async () => {
1842 | await useTestDatabase()
1843 |
1844 | const response = await runLensCommand({
1845 | command: 'mcp.resource.get',
1846 | params: { uri: `mongodb://collection/${TEST_COLLECTION_NAME}/schema` }
1847 | })
1848 |
1849 | assertResourceSuccess(response, `Schema for '${TEST_COLLECTION_NAME}'`)
1850 | const content = response.result.contents[0].text
1851 | assert(content.includes('name:'), 'Name field not found')
1852 | assert(content.includes('value:'), 'Value field not found')
1853 | assert(content.includes('tags:'), 'Tags field not found')
1854 | }
1855 |
1856 | const testCollectionIndexesResource = async () => {
1857 | await useTestDatabase()
1858 |
1859 | const response = await runLensCommand({
1860 | command: 'mcp.resource.get',
1861 | params: { uri: `mongodb://collection/${TEST_COLLECTION_NAME}/indexes` }
1862 | })
1863 |
1864 | assertResourceSuccess(response, 'Indexes')
1865 | const content = response.result.contents[0].text
1866 | assert(content.includes('name_1'), 'Name index not found')
1867 | assert(content.includes('value_-1'), 'Value index not found')
1868 | }
1869 |
1870 | const testCollectionStatsResource = async () => {
1871 | await useTestDatabase()
1872 |
1873 | const response = await runLensCommand({
1874 | command: 'mcp.resource.get',
1875 | params: { uri: `mongodb://collection/${TEST_COLLECTION_NAME}/stats` }
1876 | })
1877 |
1878 | assertResourceSuccess(response, 'Statistics')
1879 | assert(response.result.contents[0].text.includes('Document Count'), 'Document count not found')
1880 | }
1881 |
1882 | const testCollectionValidationResource = async () => {
1883 | await useTestDatabase()
1884 |
1885 | const response = await runLensCommand({
1886 | command: 'mcp.resource.get',
1887 | params: { uri: `mongodb://collection/${TEST_COLLECTION_NAME}/validation` }
1888 | })
1889 |
1890 | assert(response?.result?.contents, 'No contents in response')
1891 | assert(Array.isArray(response.result.contents), 'Contents not an array')
1892 | assert(response.result.contents.length > 0, 'Empty contents array')
1893 | assert(
1894 | response.result.contents[0].text.includes('Collection Validation Rules') ||
1895 | response.result.contents[0].text.includes('does not have any validation rules'),
1896 | 'Validation rules title or empty message not found'
1897 | )
1898 | }
1899 |
1900 | const testServerStatusResource = async () => {
1901 | const response = await runLensCommand({
1902 | command: 'mcp.resource.get',
1903 | params: { uri: 'mongodb://server/status' }
1904 | })
1905 |
1906 | assertResourceSuccess(response, 'MongoDB Server Status')
1907 | assert(response.result.contents[0].text.includes('Version'), 'Version info not found')
1908 | }
1909 |
1910 | const testReplicaStatusResource = async () => {
1911 | const response = await runLensCommand({
1912 | command: 'mcp.resource.get',
1913 | params: { uri: 'mongodb://server/replica' }
1914 | })
1915 |
1916 | assert(response?.result?.contents, 'No contents in response')
1917 | assert(Array.isArray(response.result.contents), 'Contents not an array')
1918 | assert(response.result.contents.length > 0, 'Empty contents array')
1919 | assert(
1920 | response.result.contents[0].text.includes('Replica Set') ||
1921 | response.result.contents[0].text.includes('not available'),
1922 | 'Replica set info or message not found'
1923 | )
1924 | }
1925 |
1926 | const testPerformanceMetricsResource = async () => {
1927 | try {
1928 | const originalTimeout = testConfig.requestTimeout
1929 | testConfig.requestTimeout = 30000
1930 |
1931 | const response = await runLensCommand({
1932 | command: 'mcp.resource.get',
1933 | params: { uri: 'mongodb://server/metrics' }
1934 | })
1935 |
1936 | testConfig.requestTimeout = originalTimeout
1937 |
1938 | assert(response?.result?.contents, 'No contents in response')
1939 | assert(Array.isArray(response.result.contents), 'Contents not an array')
1940 | assert(response.result.contents.length > 0, 'Empty contents array')
1941 | assert(
1942 | response.result.contents[0].text.includes('MongoDB Performance Metrics') ||
1943 | response.result.contents[0].text.includes('Error getting performance metrics'),
1944 | 'Performance metrics title or error message not found'
1945 | )
1946 | } catch (error) {
1947 | console.log(`${COLORS.yellow}Skipping performance metrics test due to complexity: ${error.message}${COLORS.reset}`)
1948 | stats.skipped++
1949 | stats.total--
1950 | }
1951 | }
1952 |
1953 | const testQueryBuilderPrompt = async () => {
1954 | await useTestDatabase()
1955 |
1956 | const response = await runLensCommand({
1957 | command: 'mcp.prompt.start',
1958 | params: {
1959 | name: 'query-builder',
1960 | args: {
1961 | collection: TEST_COLLECTION_NAME,
1962 | condition: 'active documents with value greater than 20'
1963 | }
1964 | }
1965 | })
1966 |
1967 | assertPromptSuccess(response, TEST_COLLECTION_NAME)
1968 | assert(response.result.messages[0].content.text.includes('active documents with value greater than 20'), 'Condition not found')
1969 | }
1970 |
1971 | const testAggregationBuilderPrompt = async () => {
1972 | await useTestDatabase()
1973 |
1974 | const response = await runLensCommand({
1975 | command: 'mcp.prompt.start',
1976 | params: {
1977 | name: 'aggregation-builder',
1978 | args: {
1979 | collection: TEST_COLLECTION_NAME,
1980 | goal: 'calculate average value by active status'
1981 | }
1982 | }
1983 | })
1984 |
1985 | assertPromptSuccess(response, TEST_COLLECTION_NAME)
1986 | assert(response.result.messages[0].content.text.includes('calculate average value by active status'), 'Goal not found')
1987 | }
1988 |
1989 | const testMongoShellPrompt = async () => {
1990 | const response = await runLensCommand({
1991 | command: 'mcp.prompt.start',
1992 | params: {
1993 | name: 'mongo-shell',
1994 | args: {
1995 | operation: 'find documents with specific criteria',
1996 | details: 'I want to find all documents where the value is greater than 10'
1997 | }
1998 | }
1999 | })
2000 |
2001 | assertPromptSuccess(response, 'find documents with specific criteria')
2002 | assert(response.result.messages[0].content.text.includes('value is greater than 10'), 'Details not found')
2003 | }
2004 |
2005 | const testSqlToMongodbPrompt = async () => {
2006 | const response = await runLensCommand({
2007 | command: 'mcp.prompt.start',
2008 | params: {
2009 | name: 'sql-to-mongodb',
2010 | args: {
2011 | sqlQuery: 'SELECT * FROM users WHERE age > 25 ORDER BY name ASC LIMIT 10',
2012 | targetCollection: 'users'
2013 | }
2014 | }
2015 | })
2016 |
2017 | assertPromptSuccess(response, 'SQL query')
2018 | assert(response.result.messages[0].content.text.includes('SELECT * FROM users'), 'SQL statement not found')
2019 | }
2020 |
2021 | const testSchemaAnalysisPrompt = async () => {
2022 | await useTestDatabase()
2023 |
2024 | const response = await runLensCommand({
2025 | command: 'mcp.prompt.start',
2026 | params: {
2027 | name: 'schema-analysis',
2028 | args: {
2029 | collection: TEST_COLLECTION_NAME
2030 | }
2031 | }
2032 | })
2033 |
2034 | assertPromptSuccess(response, TEST_COLLECTION_NAME)
2035 | assert(response.result.messages[0].content.text.includes('schema'), 'Schema analysis not found')
2036 | }
2037 |
2038 | const testDataModelingPrompt = async () => {
2039 | const response = await runLensCommand({
2040 | command: 'mcp.prompt.start',
2041 | params: {
2042 | name: 'data-modeling',
2043 | args: {
2044 | useCase: 'E-commerce product catalog',
2045 | requirements: 'Fast product lookup by category, efficient inventory tracking',
2046 | existingData: 'Currently using SQL with products and categories tables'
2047 | }
2048 | }
2049 | })
2050 |
2051 | assertPromptSuccess(response, 'E-commerce product catalog')
2052 | assert(response.result.messages[0].content.text.includes('Fast product lookup'), 'Requirements not found')
2053 | }
2054 |
2055 | const testSchemaVersioningPrompt = async () => {
2056 | await useTestDatabase()
2057 |
2058 | const response = await runLensCommand({
2059 | command: 'mcp.prompt.start',
2060 | params: {
2061 | name: 'schema-versioning',
2062 | args: {
2063 | collection: TEST_COLLECTION_NAME,
2064 | currentSchema: 'Documents with name, value, tags fields',
2065 | plannedChanges: 'Add a new status field, make tags required',
2066 | migrationConstraints: 'Zero downtime required'
2067 | }
2068 | }
2069 | })
2070 |
2071 | assertPromptSuccess(response, TEST_COLLECTION_NAME)
2072 | assert(response.result.messages[0].content.text.includes('schema versioning'), 'Schema versioning not found')
2073 | }
2074 |
2075 | const testMultiTenantDesignPrompt = async () => {
2076 | const response = await runLensCommand({
2077 | command: 'mcp.prompt.start',
2078 | params: {
2079 | name: 'multi-tenant-design',
2080 | args: {
2081 | tenantIsolation: 'collection',
2082 | estimatedTenants: '50',
2083 | sharedFeatures: 'User profiles, product catalog',
2084 | tenantSpecificFeatures: 'Orders, custom pricing',
2085 | scalingPriorities: 'Read-heavy, occasional bulk writes'
2086 | }
2087 | }
2088 | })
2089 |
2090 | assertPromptSuccess(response, 'tenant isolation')
2091 | assert(response.result.messages[0].content.text.includes('collection'), 'Collection isolation not found')
2092 | }
2093 |
2094 | const testIndexRecommendationPrompt = async () => {
2095 | await useTestDatabase()
2096 |
2097 | const response = await runLensCommand({
2098 | command: 'mcp.prompt.start',
2099 | params: {
2100 | name: 'index-recommendation',
2101 | args: {
2102 | collection: TEST_COLLECTION_NAME,
2103 | queryPattern: 'filtering by createdAt within a date range'
2104 | }
2105 | }
2106 | })
2107 |
2108 | assertPromptSuccess(response, TEST_COLLECTION_NAME)
2109 | assert(response.result.messages[0].content.text.includes('filtering by createdAt'), 'Query pattern not found')
2110 | }
2111 |
2112 | const testQueryOptimizerPrompt = async () => {
2113 | await useTestDatabase()
2114 |
2115 | const response = await runLensCommand({
2116 | command: 'mcp.prompt.start',
2117 | params: {
2118 | name: 'query-optimizer',
2119 | args: {
2120 | collection: TEST_COLLECTION_NAME,
2121 | query: '{"tags": "tag0", "value": {"$gt": 10}}',
2122 | performance: 'Currently taking 500ms with 10,000 documents'
2123 | }
2124 | }
2125 | })
2126 |
2127 | assertPromptSuccess(response, TEST_COLLECTION_NAME)
2128 | assert(response.result.messages[0].content.text.includes('$gt'), 'Query operator not found')
2129 | }
2130 |
2131 | const testSecurityAuditPrompt = async () => {
2132 | const response = await runLensCommand({
2133 | command: 'mcp.prompt.start',
2134 | params: {
2135 | name: 'security-audit',
2136 | args: {}
2137 | }
2138 | })
2139 |
2140 | assertPromptSuccess(response, 'security audit')
2141 | }
2142 |
2143 | const testBackupStrategyPrompt = async () => {
2144 | const response = await runLensCommand({
2145 | command: 'mcp.prompt.start',
2146 | params: {
2147 | name: 'backup-strategy',
2148 | args: {
2149 | databaseSize: '50GB',
2150 | uptime: '99.9%',
2151 | rpo: '1 hour',
2152 | rto: '4 hours'
2153 | }
2154 | }
2155 | })
2156 |
2157 | assertPromptSuccess(response, 'backup')
2158 | assert(response.result.messages[0].content.text.includes('50GB'), 'Database size not found')
2159 | }
2160 |
2161 | const testMigrationGuidePrompt = async () => {
2162 | const response = await runLensCommand({
2163 | command: 'mcp.prompt.start',
2164 | params: {
2165 | name: 'migration-guide',
2166 | args: {
2167 | sourceVersion: '4.4',
2168 | targetVersion: '5.0',
2169 | features: 'Time series collections, transactions, aggregation'
2170 | }
2171 | }
2172 | })
2173 |
2174 | assertPromptSuccess(response, 'migration')
2175 | const content = response.result.messages[0].content.text
2176 | assert(content.includes('4.4'), 'Source version not found')
2177 | assert(content.includes('5.0'), 'Target version not found')
2178 | }
2179 |
2180 | const testDatabaseHealthCheckPrompt = async () => {
2181 | try {
2182 | await useTestDatabase()
2183 |
2184 | const testDoc = {
2185 | name: 'Health Check Test',
2186 | value: 100,
2187 | tags: ['test'],
2188 | isActive: true
2189 | }
2190 |
2191 | await runLensCommand({
2192 | command: 'mcp.tool.invoke',
2193 | params: {
2194 | name: 'insert-document',
2195 | args: {
2196 | collection: TEST_COLLECTION_NAME,
2197 | document: JSON.stringify(testDoc)
2198 | }
2199 | }
2200 | })
2201 |
2202 | const originalTimeout = testConfig.requestTimeout
2203 | testConfig.requestTimeout = 45000
2204 |
2205 | console.log(`${COLORS.blue}Running database health check prompt (increased timeout to ${testConfig.requestTimeout/1000}s)${COLORS.reset}`)
2206 |
2207 | const response = await runLensCommand({
2208 | command: 'mcp.prompt.start',
2209 | params: {
2210 | name: 'database-health-check',
2211 | args: {
2212 | includePerformance: 'false',
2213 | includeSchema: 'true',
2214 | includeSecurity: 'false'
2215 | }
2216 | }
2217 | })
2218 |
2219 | testConfig.requestTimeout = originalTimeout
2220 |
2221 | assert(response?.result?.messages, 'No messages in response')
2222 | assert(Array.isArray(response.result.messages), 'Messages not an array')
2223 | assert(response.result.messages.length > 0, 'Empty messages array')
2224 |
2225 | const promptText = response.result.messages[0].content.text
2226 | assert(
2227 | promptText.includes('health') ||
2228 | promptText.includes('Health') ||
2229 | promptText.includes('assessment'),
2230 | 'Health check content not found'
2231 | )
2232 | } catch (error) {
2233 | console.log(`${COLORS.yellow}Skipping database health check prompt test due to complexity: ${error.message}${COLORS.reset}`)
2234 | stats.skipped++
2235 | stats.total--
2236 | }
2237 | }
2238 |
2239 | const logHeader = (title, margin = 'none') => {
2240 | if (margin.indexOf('top') !== -1) console.log('')
2241 | console.log(`${COLORS.cyan}${DIVIDER}${COLORS.reset}`)
2242 | console.log(`${COLORS.cyan}${title}${COLORS.reset}`)
2243 | console.log(`${COLORS.cyan}${DIVIDER}${COLORS.reset}`)
2244 | if (margin.indexOf('bottom') !== -1) console.log('')
2245 | }
2246 |
2247 | const displayTestSummary = () => {
2248 | logHeader('Test Summary', 'margin:top,bottom')
2249 | console.log(`${COLORS.white}Total Tests: ${stats.total}${COLORS.reset}`)
2250 | console.log(`${COLORS.green}Passed: ${stats.passed}${COLORS.reset}`)
2251 | console.log(`${COLORS.red}Failed: ${stats.failed}${COLORS.reset}`)
2252 | console.log(`${COLORS.yellow}Skipped: ${stats.skipped}${COLORS.reset}`)
2253 |
2254 | if (stats.failed > 0) {
2255 | console.error(`${COLORS.red}Some tests failed.${COLORS.reset}`)
2256 | process.exit(1)
2257 | } else {
2258 | console.log(`${COLORS.green}All tests passed!${COLORS.reset}`)
2259 | process.exit(0)
2260 | }
2261 | }
2262 |
2263 | const cleanup = async () => {
2264 | await cleanupTestDatabase()
2265 | await directMongoClient.close()
2266 |
2267 | if (lensProcess) {
2268 | lensProcess.kill('SIGKILL')
2269 | await new Promise(resolve => lensProcess.on('exit', resolve))
2270 | }
2271 | }
2272 |
2273 | const cleanupTestDatabase = async () => {
2274 | try {
2275 | await testDb.dropDatabase()
2276 | console.log(`${COLORS.yellow}Test database cleaned up.${COLORS.reset}`)
2277 | } catch (err) {
2278 | console.error(`${COLORS.red}Error cleaning up test database: ${err.message}${COLORS.reset}`)
2279 | }
2280 | }
2281 |
2282 | process.on('exit', () => {
2283 | console.log('Exiting test process…')
2284 | if (lensProcess) {
2285 | console.log('Shutting down MongoDB Lens server…')
2286 | lensProcess.kill()
2287 | }
2288 | })
2289 |
2290 | let testDb
2291 | let mongoUri
2292 | let testCollection
2293 | let directMongoClient
2294 | let isReplSet = false
2295 | let isSharded = false
2296 | let nextRequestId = 1
2297 | let lensProcess = null
2298 | let responseHandlers = new Map()
2299 |
2300 | const testFilters = []
2301 | const groupFilters = []
2302 | const patternFilters = []
2303 | const isDebugging = process.env.DEBUG === 'true'
2304 |
2305 | const DIVIDER = '-'.repeat(30)
2306 | const TEST_DB_NAME = 'mongodb_lens_test'
2307 | const TEST_COLLECTION_NAME = 'test_collection'
2308 | const ANOTHER_TEST_COLLECTION = 'another_collection'
2309 | const MONGODB_LENS_PATH = join(__dirname, 'mongodb-lens.js')
2310 |
2311 | const stats = {
2312 | total: 0,
2313 | passed: 0,
2314 | failed: 0,
2315 | skipped: 0
2316 | }
2317 |
2318 | const COLORS = {
2319 | red: '\x1b[31m',
2320 | reset: '\x1b[0m',
2321 | blue: '\x1b[34m',
2322 | cyan: '\x1b[36m',
2323 | gray: '\x1b[90m',
2324 | green: '\x1b[32m',
2325 | white: '\x1b[37m',
2326 | yellow: '\x1b[33m',
2327 | magenta: '\x1b[35m',
2328 | }
2329 |
2330 | const testConfig = {
2331 | requestTimeout: 15000,
2332 | serverStartupTimeout: 20000,
2333 | disableTokens: process.env.CONFIG_DISABLE_DESTRUCTIVE_OPERATION_TOKENS === 'true'
2334 | }
2335 |
2336 | const TEST_GROUPS = [
2337 | {
2338 | name: 'Connection Tools',
2339 | tests: [
2340 | { name: 'connect-mongodb Tool', fn: testConnectMongodbTool },
2341 | { name: 'connect-original Tool', fn: testConnectOriginalTool },
2342 | { name: 'add-connection-alias Tool', fn: testAddConnectionAliasTool },
2343 | { name: 'list-connections Tool', fn: testListConnectionsTool }
2344 | ]
2345 | },
2346 | {
2347 | name: 'Database Tools',
2348 | tests: [
2349 | { name: 'list-databases Tool', fn: testListDatabasesTool },
2350 | { name: 'current-database Tool', fn: testCurrentDatabaseTool },
2351 | { name: 'create-database Tool', fn: testCreateDatabaseTool },
2352 | { name: 'use-database Tool', fn: testUseDatabaseTool },
2353 | { name: 'drop-database Tool', fn: testDropDatabaseTool }
2354 | ]
2355 | },
2356 | {
2357 | name: 'User Tools',
2358 | tests: [
2359 | { name: 'create-user Tool', fn: testCreateUserTool },
2360 | { name: 'drop-user Tool', fn: testDropUserTool }
2361 | ]
2362 | },
2363 | {
2364 | name: 'Collection Tools',
2365 | tests: [
2366 | { name: 'list-collections Tool', fn: testListCollectionsTool },
2367 | { name: 'create-collection Tool', fn: testCreateCollectionTool },
2368 | { name: 'drop-collection Tool', fn: testDropCollectionTool },
2369 | { name: 'rename-collection Tool', fn: testRenameCollectionTool },
2370 | { name: 'validate-collection Tool', fn: testValidateCollectionTool }
2371 | ]
2372 | },
2373 | {
2374 | name: 'Document Tools',
2375 | tests: [
2376 | { name: 'distinct-values Tool', fn: testDistinctValuesTool },
2377 | { name: 'find-documents Tool', fn: testFindDocumentsTool },
2378 | { name: 'count-documents Tool', fn: testCountDocumentsTool },
2379 | { name: 'insert-document Tool', fn: testInsertDocumentTool },
2380 | { name: 'update-document Tool', fn: testUpdateDocumentTool },
2381 | { name: 'delete-document Tool', fn: testDeleteDocumentTool }
2382 | ]
2383 | },
2384 | {
2385 | name: 'Advanced Tools',
2386 | tests: [
2387 | { name: 'aggregate-data Tool', fn: testAggregateDataTool },
2388 | { name: 'create-index Tool', fn: testCreateIndexTool },
2389 | { name: 'drop-index Tool', fn: testDropIndexTool },
2390 | { name: 'analyze-schema Tool', fn: testAnalyzeSchemaTool },
2391 | { name: 'generate-schema-validator Tool', fn: testGenerateSchemaValidatorTool },
2392 | { name: 'compare-schemas Tool', fn: testCompareSchemasTool },
2393 | { name: 'explain-query Tool', fn: testExplainQueryTool },
2394 | { name: 'analyze-query-patterns Tool', fn: testAnalyzeQueryPatternsTool },
2395 | { name: 'get-stats Tool', fn: testGetStatsTool },
2396 | { name: 'bulk-operations Tool', fn: testBulkOperationsTool },
2397 | { name: 'create-timeseries Tool', fn: testCreateTimeseriesCollectionTool },
2398 | { name: 'collation-query Tool', fn: testCollationQueryTool },
2399 | { name: 'text-search Tool', fn: testTextSearchTool },
2400 | { name: 'geo-query Tool', fn: testGeoQueryTool },
2401 | { name: 'transaction Tool', fn: testTransactionTool },
2402 | { name: 'watch-changes Tool', fn: testWatchChangesTool },
2403 | { name: 'gridfs-operation Tool', fn: testGridFSOperationTool },
2404 | { name: 'clear-cache Tool', fn: testClearCacheTool },
2405 | { name: 'shard-status Tool', fn: testShardStatusTool },
2406 | { name: 'export-data Tool', fn: testExportDataTool }
2407 | ]
2408 | },
2409 | {
2410 | name: 'Resources',
2411 | tests: [
2412 | { name: 'databases Resource', fn: testDatabasesResource },
2413 | { name: 'collections Resource', fn: testCollectionsResource },
2414 | { name: 'database-users Resource', fn: testDatabaseUsersResource },
2415 | { name: 'database-triggers Resource', fn: testDatabaseTriggersResource },
2416 | { name: 'stored-functions Resource', fn: testStoredFunctionsResource },
2417 | { name: 'collection-schema Resource', fn: testCollectionSchemaResource },
2418 | { name: 'collection-indexes Resource', fn: testCollectionIndexesResource },
2419 | { name: 'collection-stats Resource', fn: testCollectionStatsResource },
2420 | { name: 'collection-validation Resource', fn: testCollectionValidationResource },
2421 | { name: 'server-status Resource', fn: testServerStatusResource },
2422 | { name: 'replica-status Resource', fn: testReplicaStatusResource },
2423 | { name: 'performance-metrics Resource', fn: testPerformanceMetricsResource }
2424 | ]
2425 | },
2426 | {
2427 | name: 'Prompts',
2428 | tests: [
2429 | { name: 'query-builder Prompt', fn: testQueryBuilderPrompt },
2430 | { name: 'aggregation-builder Prompt', fn: testAggregationBuilderPrompt },
2431 | { name: 'mongo-shell Prompt', fn: testMongoShellPrompt },
2432 | { name: 'sql-to-mongodb Prompt', fn: testSqlToMongodbPrompt },
2433 | { name: 'schema-analysis Prompt', fn: testSchemaAnalysisPrompt },
2434 | { name: 'data-modeling Prompt', fn: testDataModelingPrompt },
2435 | { name: 'schema-versioning Prompt', fn: testSchemaVersioningPrompt },
2436 | { name: 'multi-tenant-design Prompt', fn: testMultiTenantDesignPrompt },
2437 | { name: 'index-recommendation Prompt', fn: testIndexRecommendationPrompt },
2438 | { name: 'query-optimizer Prompt', fn: testQueryOptimizerPrompt },
2439 | { name: 'security-audit Prompt', fn: testSecurityAuditPrompt },
2440 | { name: 'backup-strategy Prompt', fn: testBackupStrategyPrompt },
2441 | { name: 'migration-guide Prompt', fn: testMigrationGuidePrompt },
2442 | { name: 'database-health-check Prompt', fn: testDatabaseHealthCheckPrompt }
2443 | ]
2444 | }
2445 | ]
2446 |
2447 | runTests().catch(err => {
2448 | console.error(`${COLORS.red}Test runner error: ${err.message}${COLORS.reset}`)
2449 | if (lensProcess) lensProcess.kill()
2450 | process.exit(1)
2451 | })
2452 |
```