#
tokens: 37118/50000 11/12 files (page 1/2)
lines: off (toggle) GitHub
raw markdown copy
This is page 1 of 2. Use http://codebase.md/furey/mongodb-lens?lines=false&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

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
.DS_Store
node_modules/
repomix*
.mongodb-lens*.json*

```

--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------

```
.DS_Store
node_modules/
.dockerhub/
.github/
.git/
.gitignore
repomix*

```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
# MongoDB Lens

[![License](https://img.shields.io/github/license/furey/mongodb-lens)](./LICENSE)
[![Docker Hub Version](https://img.shields.io/docker/v/furey/mongodb-lens)](https://hub.docker.com/r/furey/mongodb-lens)
[![NPM Version](https://img.shields.io/npm/v/mongodb-lens)](https://www.npmjs.com/package/mongodb-lens)
[![Buy Me a Coffee](https://img.shields.io/badge/Buy%20Me%20a%20Coffee-donate-orange.svg)](https://www.buymeacoffee.com/furey)

**MongoDB Lens** is a local Model Context Protocol (MCP) server with full featured access to MongoDB databases using natural language via LLMs to perform queries, run aggregations, optimize performance, and more.

## Contents

- [Quick Start](#quick-start)
- [Features](#features)
- [Installation](#installation)
- [Configuration](#configuration)
- [Client Setup](#client-setup)
- [Data Protection](#data-protection)
- [Tutorial](#tutorial)
- [Test Suite](#test-suite)
- [Disclaimer](#disclaimer)
- [Support](#support)

## Quick Start

- [Install](#installation) MongoDB Lens
- [Configure](#configuration) MongoDB Lens
- [Set up](#client-setup) your MCP Client (e.g. [Claude Desktop](#client-setup-claude-desktop), [Cursor](https://docs.cursor.com/context/model-context-protocol#configuring-mcp-servers), etc)
- Explore your MongoDB databases with [natural language queries](#tutorial-4-example-queries)

## Features

- [Tools](#tools)
- [Resources](#resources)
- [Prompts](#prompts)
- [Other](#other-features)

### Tools

- [`add-connection-alias`](https://github.com/search?type=code&q=repo%3Afurey%2Fmongodb-lens+%2Fserver%5C.tool%5C%28%5Cs*%27add-connection-alias%27%2C%2F): Add a new MongoDB connection alias
- [`aggregate-data`](https://github.com/search?type=code&q=repo%3Afurey%2Fmongodb-lens+%2Fserver%5C.tool%5C%28%5Cs*%27aggregate-data%27%2C%2F): Execute aggregation pipelines
- [`analyze-query-patterns`](https://github.com/search?type=code&q=repo%3Afurey%2Fmongodb-lens+%2Fserver%5C.tool%5C%28%5Cs*%27analyze-query-patterns%27%2C%2F): Analyze live queries and suggest optimizations
- [`analyze-schema`](https://github.com/search?type=code&q=repo%3Afurey%2Fmongodb-lens+%2Fserver%5C.tool%5C%28%5Cs*%27analyze-schema%27%2C%2F): Automatically infer collection schemas
- [`bulk-operations`](https://github.com/search?type=code&q=repo%3Afurey%2Fmongodb-lens+%2Fserver%5C.tool%5C%28%5Cs*%27bulk-operations%27%2C%2F): Perform multiple operations efficiently ([requires confirmation](#data-protection-confirmation-for-destructive-operations) for destructive operations)
- [`clear-cache`](https://github.com/search?type=code&q=repo%3Afurey%2Fmongodb-lens+%2Fserver%5C.tool%5C%28%5Cs*%27clear-cache%27%2C%2F): Clear memory caches to ensure fresh data
- [`collation-query`](https://github.com/search?type=code&q=repo%3Afurey%2Fmongodb-lens+%2Fserver%5C.tool%5C%28%5Cs*%27collation-query%27%2C%2F): Find documents with language-specific collation rules
- [`compare-schemas`](https://github.com/search?type=code&q=repo%3Afurey%2Fmongodb-lens+%2Fserver%5C.tool%5C%28%5Cs*%27compare-schemas%27%2C%2F): Compare schemas between two collections
- [`connect-mongodb`](https://github.com/search?type=code&q=repo%3Afurey%2Fmongodb-lens+%2Fserver%5C.tool%5C%28%5Cs*%27connect-mongodb%27%2C%2F): Connect to a different MongoDB URI
- [`connect-original`](https://github.com/search?type=code&q=repo%3Afurey%2Fmongodb-lens+%2Fserver%5C.tool%5C%28%5Cs*%27connect-original%27%2C%2F): Connect back to the original MongoDB URI used at startup
- [`count-documents`](https://github.com/search?type=code&q=repo%3Afurey%2Fmongodb-lens+%2Fserver%5C.tool%5C%28%5Cs*%27count-documents%27%2C%2F): Count documents matching specified criteria
- [`create-collection`](https://github.com/search?type=code&q=repo%3Afurey%2Fmongodb-lens+%2Fserver%5C.tool%5C%28%5Cs*%27create-collection%27%2C%2F): Create new collections with custom options
- [`create-database`](https://github.com/search?type=code&q=repo%3Afurey%2Fmongodb-lens+%2Fserver%5C.tool%5C%28%5Cs*%27create-database%27%2C%2F): Create a new database with option to switch to it
- [`create-index`](https://github.com/search?type=code&q=repo%3Afurey%2Fmongodb-lens+%2Fserver%5C.tool%5C%28%5Cs*%27create-index%27%2C%2F): Create new indexes for performance optimization
- [`create-timeseries`](https://github.com/search?type=code&q=repo%3Afurey%2Fmongodb-lens+%2Fserver%5C.tool%5C%28%5Cs*%27create-timeseries%27%2C%2F): Create time series collections for temporal data
- [`create-user`](https://github.com/search?type=code&q=repo%3Afurey%2Fmongodb-lens+%2Fserver%5C.tool%5C%28%5Cs*%27create-user%27%2C%2F): Create new database users with specific roles
- [`current-database`](https://github.com/search?type=code&q=repo%3Afurey%2Fmongodb-lens+%2Fserver%5C.tool%5C%28%5Cs*%27current-database%27%2C%2F): Show the current database context
- [`delete-document`](https://github.com/search?type=code&q=repo%3Afurey%2Fmongodb-lens+%2Fserver%5C.tool%5C%28%5Cs*%27delete-document%27%2C%2F): Delete documents matching specified criteria ([requires confirmation](#data-protection-confirmation-for-destructive-operations))
- [`distinct-values`](https://github.com/search?type=code&q=repo%3Afurey%2Fmongodb-lens+%2Fserver%5C.tool%5C%28%5Cs*%27distinct-values%27%2C%2F): Extract unique values for any field
- [`drop-collection`](https://github.com/search?type=code&q=repo%3Afurey%2Fmongodb-lens+%2Fserver%5C.tool%5C%28%5Cs*%27drop-collection%27%2C%2F): Remove collections from the database ([requires confirmation](#data-protection-confirmation-for-destructive-operations))
- [`drop-database`](https://github.com/search?type=code&q=repo%3Afurey%2Fmongodb-lens+%2Fserver%5C.tool%5C%28%5Cs*%27drop-database%27%2C%2F): Drop a database ([requires confirmation](#data-protection-confirmation-for-destructive-operations))
- [`drop-index`](https://github.com/search?type=code&q=repo%3Afurey%2Fmongodb-lens+%2Fserver%5C.tool%5C%28%5Cs*%27drop-index%27%2C%2F): Remove indexes from collections ([requires confirmation](#data-protection-confirmation-for-destructive-operations))
- [`drop-user`](https://github.com/search?type=code&q=repo%3Afurey%2Fmongodb-lens+%2Fserver%5C.tool%5C%28%5Cs*%27drop-user%27%2C%2F): Remove database users ([requires confirmation](#data-protection-confirmation-for-destructive-operations))
- [`explain-query`](https://github.com/search?type=code&q=repo%3Afurey%2Fmongodb-lens+%2Fserver%5C.tool%5C%28%5Cs*%27explain-query%27%2C%2F): Analyze query execution plans
- [`export-data`](https://github.com/search?type=code&q=repo%3Afurey%2Fmongodb-lens+%2Fserver%5C.tool%5C%28%5Cs*%27export-data%27%2C%2F): Export query results in JSON or CSV format
- [`find-documents`](https://github.com/search?type=code&q=repo%3Afurey%2Fmongodb-lens+%2Fserver%5C.tool%5C%28%5Cs*%27find-documents%27%2C%2F): Run queries with filters, projections, and sorting
- [`generate-schema-validator`](https://github.com/search?type=code&q=repo%3Afurey%2Fmongodb-lens+%2Fserver%5C.tool%5C%28%5Cs*%27generate-schema-validator%27%2C%2F): Generate JSON Schema validators
- [`geo-query`](https://github.com/search?type=code&q=repo%3Afurey%2Fmongodb-lens+%2Fserver%5C.tool%5C%28%5Cs*%27geo-query%27%2C%2F): Perform geospatial queries with various operators
- [`get-stats`](https://github.com/search?type=code&q=repo%3Afurey%2Fmongodb-lens+%2Fserver%5C.tool%5C%28%5Cs*%27get-stats%27%2C%2F): Retrieve database or collection statistics
- [`gridfs-operation`](https://github.com/search?type=code&q=repo%3Afurey%2Fmongodb-lens+%2Fserver%5C.tool%5C%28%5Cs*%27gridfs-operation%27%2C%2F): Manage large files with GridFS buckets
- [`insert-document`](https://github.com/search?type=code&q=repo%3Afurey%2Fmongodb-lens+%2Fserver%5C.tool%5C%28%5Cs*%27insert-document%27%2C%2F): Insert one or more documents into collections
- [`list-collections`](https://github.com/search?type=code&q=repo%3Afurey%2Fmongodb-lens+%2Fserver%5C.tool%5C%28%5Cs*%27list-collections%27%2C%2F): Explore collections in the current database
- [`list-connections`](https://github.com/search?type=code&q=repo%3Afurey%2Fmongodb-lens+%2Fserver%5C.tool%5C%28%5Cs*%27list-connections%27%2C%2F): View all available MongoDB connection aliases
- [`list-databases`](https://github.com/search?type=code&q=repo%3Afurey%2Fmongodb-lens+%2Fserver%5C.tool%5C%28%5Cs*%27list-databases%27%2C%2F): View all accessible databases
- [`rename-collection`](https://github.com/search?type=code&q=repo%3Afurey%2Fmongodb-lens+%2Fserver%5C.tool%5C%28%5Cs*%27rename-collection%27%2C%2F): Rename existing collections ([requires confirmation](#data-protection-confirmation-for-destructive-operations) when dropping targets)
- [`shard-status`](https://github.com/search?type=code&q=repo%3Afurey%2Fmongodb-lens+%2Fserver%5C.tool%5C%28%5Cs*%27shard-status%27%2C%2F): View sharding configuration for databases and collections
- [`text-search`](https://github.com/search?type=code&q=repo%3Afurey%2Fmongodb-lens+%2Fserver%5C.tool%5C%28%5Cs*%27text-search%27%2C%2F): Perform full-text search across text-indexed fields
- [`transaction`](https://github.com/search?type=code&q=repo%3Afurey%2Fmongodb-lens+%2Fserver%5C.tool%5C%28%5Cs*%27transaction%27%2C%2F): Execute multiple operations in a single ACID transaction
- [`update-document`](https://github.com/search?type=code&q=repo%3Afurey%2Fmongodb-lens+%2Fserver%5C.tool%5C%28%5Cs*%27update-document%27%2C%2F): Update documents matching specified criteria
- [`use-database`](https://github.com/search?type=code&q=repo%3Afurey%2Fmongodb-lens+%2Fserver%5C.tool%5C%28%5Cs*%27use-database%27%2C%2F): Switch to a specific database context
- [`validate-collection`](https://github.com/search?type=code&q=repo%3Afurey%2Fmongodb-lens+%2Fserver%5C.tool%5C%28%5Cs*%27validate-collection%27%2C%2F): Check for data inconsistencies
- [`watch-changes`](https://github.com/search?type=code&q=repo%3Afurey%2Fmongodb-lens+%2Fserver%5C.tool%5C%28%5Cs*%27watch-changes%27%2C%2F): Monitor real-time changes to collections

### Resources

- [`collection-indexes`](https://github.com/search?type=code&q=repo%3Afurey%2Fmongodb-lens+%2Fserver%5C.resource%5C%28%5Cs*%27collection-indexes%27%2C%2F): Index information for a collection
- [`collection-schema`](https://github.com/search?type=code&q=repo%3Afurey%2Fmongodb-lens+%2Fserver%5C.resource%5C%28%5Cs*%27collection-schema%27%2C%2F): Schema information for a collection
- [`collection-stats`](https://github.com/search?type=code&q=repo%3Afurey%2Fmongodb-lens+%2Fserver%5C.resource%5C%28%5Cs*%27collection-stats%27%2C%2F): Performance statistics for a collection
- [`collection-validation`](https://github.com/search?type=code&q=repo%3Afurey%2Fmongodb-lens+%2Fserver%5C.resource%5C%28%5Cs*%27collection-validation%27%2C%2F): Validation rules for a collection
- [`collections`](https://github.com/search?type=code&q=repo%3Afurey%2Fmongodb-lens+%2Fserver%5C.resource%5C%28%5Cs*%27collections%27%2C%2F): List of collections in the current database
- [`database-triggers`](https://github.com/search?type=code&q=repo%3Afurey%2Fmongodb-lens+%2Fserver%5C.resource%5C%28%5Cs*%27database-triggers%27%2C%2F): Database change streams and event triggers configuration
- [`database-users`](https://github.com/search?type=code&q=repo%3Afurey%2Fmongodb-lens+%2Fserver%5C.resource%5C%28%5Cs*%27database-users%27%2C%2F): Database users and roles in the current database
- [`databases`](https://github.com/search?type=code&q=repo%3Afurey%2Fmongodb-lens+%2Fserver%5C.resource%5C%28%5Cs*%27databases%27%2C%2F): List of all accessible databases
- [`performance-metrics`](https://github.com/search?type=code&q=repo%3Afurey%2Fmongodb-lens+%2Fserver%5C.resource%5C%28%5Cs*%27performance-metrics%27%2C%2F): Real-time performance metrics and profiling data
- [`replica-status`](https://github.com/search?type=code&q=repo%3Afurey%2Fmongodb-lens+%2Fserver%5C.resource%5C%28%5Cs*%27replica-status%27%2C%2F): Replica set status and configuration
- [`server-status`](https://github.com/search?type=code&q=repo%3Afurey%2Fmongodb-lens+%2Fserver%5C.resource%5C%28%5Cs*%27server-status%27%2C%2F): Server status information
- [`stored-functions`](https://github.com/search?type=code&q=repo%3Afurey%2Fmongodb-lens+%2Fserver%5C.resource%5C%28%5Cs*%27stored-functions%27%2C%2F): Stored JavaScript functions in the current database

### Prompts

- [`aggregation-builder`](https://github.com/search?type=code&q=repo%3Afurey%2Fmongodb-lens+%2Fserver%5C.prompt%5C%28%5Cs*%27aggregation-builder%27%2C%2F): Step-by-step creation of aggregation pipelines
- [`backup-strategy`](https://github.com/search?type=code&q=repo%3Afurey%2Fmongodb-lens+%2Fserver%5C.prompt%5C%28%5Cs*%27backup-strategy%27%2C%2F): Customized backup and recovery recommendations
- [`data-modeling`](https://github.com/search?type=code&q=repo%3Afurey%2Fmongodb-lens+%2Fserver%5C.prompt%5C%28%5Cs*%27data-modeling%27%2C%2F): Expert advice on MongoDB schema design for specific use cases
- [`database-health-check`](https://github.com/search?type=code&q=repo%3Afurey%2Fmongodb-lens+%2Fserver%5C.prompt%5C%28%5Cs*%27database-health-check%27%2C%2F): Comprehensive database health assessment and recommendations
- [`index-recommendation`](https://github.com/search?type=code&q=repo%3Afurey%2Fmongodb-lens+%2Fserver%5C.prompt%5C%28%5Cs*%27index-recommendation%27%2C%2F): Get personalized index suggestions based on query patterns
- [`migration-guide`](https://github.com/search?type=code&q=repo%3Afurey%2Fmongodb-lens+%2Fserver%5C.prompt%5C%28%5Cs*%27migration-guide%27%2C%2F): Step-by-step MongoDB version migration plans
- [`mongo-shell`](https://github.com/search?type=code&q=repo%3Afurey%2Fmongodb-lens+%2Fserver%5C.prompt%5C%28%5Cs*%27mongo-shell%27%2C%2F): Generate MongoDB shell commands with explanations
- [`multi-tenant-design`](https://github.com/search?type=code&q=repo%3Afurey%2Fmongodb-lens+%2Fserver%5C.prompt%5C%28%5Cs*%27multi-tenant-design%27%2C%2F): Design MongoDB multi-tenant database architecture
- [`query-builder`](https://github.com/search?type=code&q=repo%3Afurey%2Fmongodb-lens+%2Fserver%5C.prompt%5C%28%5Cs*%27query-builder%27%2C%2F): Interactive guidance for constructing MongoDB queries
- [`query-optimizer`](https://github.com/search?type=code&q=repo%3Afurey%2Fmongodb-lens+%2Fserver%5C.prompt%5C%28%5Cs*%27query-optimizer%27%2C%2F): Optimization recommendations for slow queries
- [`schema-analysis`](https://github.com/search?type=code&q=repo%3Afurey%2Fmongodb-lens+%2Fserver%5C.prompt%5C%28%5Cs*%27schema-analysis%27%2C%2F): Detailed collection schema analysis with recommendations
- [`schema-versioning`](https://github.com/search?type=code&q=repo%3Afurey%2Fmongodb-lens+%2Fserver%5C.prompt%5C%28%5Cs*%27schema-versioning%27%2C%2F): Manage schema evolution in MongoDB applications
- [`security-audit`](https://github.com/search?type=code&q=repo%3Afurey%2Fmongodb-lens+%2Fserver%5C.prompt%5C%28%5Cs*%27security-audit%27%2C%2F): Database security analysis and improvement recommendations
- [`sql-to-mongodb`](https://github.com/search?type=code&q=repo%3Afurey%2Fmongodb-lens+%2Fserver%5C.prompt%5C%28%5Cs*%27sql-to-mongodb%27%2C%2F): Convert SQL queries to MongoDB aggregation pipelines

### Other Features

- [Overview](#other-features-overview)
- [New Database Metadata](#other-features-new-database-metadata)

#### Other Features: Overview

MongoDB Lens includes numerous other features:

- **[Config File](#configuration-config-file)**: Custom configuration via `~/.mongodb-lens.[jsonc|json]`
- **[Env Var Overrides](#configuration-environment-variable-overrides)**: Override config settings via `process.env.CONFIG_*`
- **[Confirmation System](#data-protection-confirmation-for-destructive-operations)**: Two-step verification for destructive operations
- **[Multiple Connections](#configuration-multiple-mongodb-connections)**: Define and switch between named URI aliases
- **[Component Disabling](#disabling-tools)**: Selectively disable tools, prompts or resources
- **Connection Resilience**: Auto-reconnection with exponential backoff
- **Query Safeguards**: Configurable limits and performance protections
- **Error Handling**: Comprehensive JSONRPC error codes and messages
- **Schema Inference**: Efficient schema analysis with intelligent sampling
- **Credential Protection**: Connection string password obfuscation in logs
- **Memory Management**: Auto-monitoring and cleanup for large operations
- **Smart Caching**: Optimized caching for schema, indexes, fields and collections
- **Backwards Compatible**: Support both modern and legacy MongoDB versions

#### Other Features: New Database Metadata

MongoDB Lens inserts a `metadata` collection into each database it creates.

This `metadata` collection stores a single document containing contextual information serving as a permanent record of the database's origin while ensuring the new and otherwise empty database persists in MongoDB's storage system.

<details>
  <summary><strong>Example metadata document</strong></summary>

```js
{
    "_id" : ObjectId("67d5284463788ec38aecee14"),
    "created" : {
        "timestamp" : ISODate("2025-03-15T07:12:04.705Z"),
        "tool" : "MongoDB Lens v5.0.7",
        "user" : "anonymous"
    },
    "mongodb" : {
        "version" : "3.6.23",
        "connectionInfo" : {
            "host" : "unknown",
            "readPreference" : "primary"
        }
    },
    "database" : {
        "name" : "example_database",
        "description" : "Created via MongoDB Lens"
    },
    "system" : {
        "hostname" : "unknown",
        "platform" : "darwin",
        "nodeVersion" : "v22.14.0"
    },
    "lens" : {
        "version" : "5.0.7",
        "startTimestamp" : ISODate("2025-03-15T07:10:06.084Z")
    }
}
```

</details>

Once you've added your own collections to your new database, you can safely remove the `metadata` collection via the `drop-collection` tool:

- _"Drop the new database's metadata collection"_<br>
  <sup>➥ Uses `drop-collection` tool (with confirmation)</sup>

## Installation

MongoDB Lens can be installed and run in several ways:

- [NPX](#installation-npx) (Easiest)
- [Docker Hub](#installation-docker-hub)
- [Node.js from Source](#installation-nodejs-from-source)
- [Docker from Source](#installation-docker-from-source)
- [Installation Verification](#installation-verification)
- [Older MongoDB Versions](#installation-older-mongodb-versions)

### Installation: NPX

> [!NOTE]<br>
> NPX requires [Node.js](https://nodejs.org/en/download) installed and running on your system (suggestion: use [Volta](https://volta.sh)).

The easiest way to run MongoDB Lens is using NPX.

First, ensure Node.js is installed:

```console
node --version # Ideally >= v22.x but MongoDB Lens is >= v18.x compatible
```

Then, run MongoDB Lens via NPX:

```console
# Using default connection string mongodb://localhost:27017
npx -y mongodb-lens

# Using custom connection string
npx -y mongodb-lens mongodb://your-connection-string

# Using "@latest" to keep the package up-to-date
npx -y mongodb-lens@latest
```

> [!TIP]<br>
> If you encounter permissions errors with `npx` try running `npx clear-npx-cache` prior to running `npx -y mongodb-lens` (this clears the cache and re-downloads the package).

### Installation: Docker Hub

> [!NOTE]<br>
> Docker Hub requires [Docker](https://docs.docker.com/get-started/get-docker) installed and running on your system.

First, ensure Docker is installed:

```console
docker --version # Ideally >= v27.x
```

Then, run MongoDB Lens via Docker Hub:

```console
# Using default connection string mongodb://localhost:27017
docker run --rm -i --network=host furey/mongodb-lens

# Using custom connection string
docker run --rm -i --network=host furey/mongodb-lens mongodb://your-connection-string

# Using "--pull" to keep the Docker image up-to-date
docker run --rm -i --network=host --pull=always furey/mongodb-lens
```

### Installation: Node.js from Source

> [!NOTE]<br>
> Node.js from source requires [Node.js](https://nodejs.org/en/download) installed and running on your system (suggestion: use [Volta](https://volta.sh)).

1. Clone the MongoDB Lens repository:<br>
    ```console
    git clone https://github.com/furey/mongodb-lens.git
    ```
1. Navigate to the cloned repository directory:<br>
    ```console
    cd /path/to/mongodb-lens
    ```
1. Ensure Node.js is installed:<br>
    ```console
    node --version # Ideally >= v22.x but MongoDB Lens is >= v18.x compatible
    ```
1. Install Node.js dependencies:<br>
    ```console
    npm ci
    ```
1. Start the server:<br>
    ```console
    # Using default connection string mongodb://localhost:27017
    node mongodb-lens.js

    # Using custom connection string
    node mongodb-lens.js mongodb://your-connection-string
    ```

### Installation: Docker from Source

> [!NOTE]<br>
> Docker from source requires [Docker](https://docs.docker.com/get-started/get-docker) installed and running on your system.

1. Clone the MongoDB Lens repository:<br>
    ```console
    git clone https://github.com/furey/mongodb-lens.git
    ```
1. Navigate to the cloned repository directory:<br>
    ```console
    cd /path/to/mongodb-lens
    ```
1. Ensure Docker is installed:<br>
    ```console
    docker --version # Ideally >= v27.x
    ```
1. Build the Docker image:<br>
    ```console
    docker build -t mongodb-lens .
    ```
1. Run the container:<br>
    ```console
    # Using default connection string mongodb://localhost:27017
    docker run --rm -i --network=host mongodb-lens

    # Using custom connection string
    docker run --rm -i --network=host mongodb-lens mongodb://your-connection-string
    ```

### Installation Verification

To verify the installation, paste and run the following JSONRPC message into the server's stdio:

```json
{"method":"resources/read","params":{"uri":"mongodb://databases"},"jsonrpc":"2.0","id":1}
```

The server should respond with a list of databases in your MongoDB instance, for example:

```json
{"result":{"contents":[{"uri":"mongodb://databases","text":"Databases (12):\n- admin (180.00 KB)\n- config (108.00 KB)\n- local (40.00 KB)\n- sample_airbnb (51.88 MB)\n- sample_analytics (9.46 MB)\n- sample_geospatial (980.00 KB)\n- sample_guides (40.00 KB)\n- sample_mflix (108.90 MB)\n- sample_restaurants (7.73 MB)\n- sample_supplies (968.00 KB)\n- sample_training (40.85 MB)\n- sample_weatherdata (2.69 MB)"}]},"jsonrpc":"2.0","id":1}
```

MongoDB Lens is now installed and ready to accept MCP requests.

### Installation: Older MongoDB Versions

If connecting to a MongoDB instance with a version `< 4.0`, the MongoDB Node.js driver used by the latest version of MongoDB Lens will not be compatible. Specifically, MongoDB Node.js driver versions `4.0.0` and above require MongoDB version `4.0` or higher.

To use MongoDB Lens with older MongoDB instances, you need to use a MongoDB Node.js driver version from the `3.x` series (e.g. `3.7.4` which is compatible with MongoDB `3.6`).

#### Older MongoDB Versions: Running from Source

1. Clone the MongoDB Lens repository:<br>
    ```console
    git clone https://github.com/furey/mongodb-lens.git
    ```
1. Navigate to the cloned repository directory:<br>
    ```console
    cd /path/to/mongodb-lens
    ```
1. Modify `package.json`:<br>
    ```diff
    "dependencies": {
      ...
    -  "mongodb": "^6.15.0",  // Or whatever newer version is listed
    +  "mongodb": "^3.7.4",   // Or whatever 3.x version is compatible with your older MongoDB instance
      ...
    }
    ```
1. Install Node.js dependencies:<br>
    ```console
    npm install
    ```
1. Start MongoDB Lens:<br>
    ```console
    node mongodb-lens.js mongodb://older-mongodb-instance
    ```

This will use the older driver version compatible with your MongoDB instance.

> [!NOTE]<br>
> You may also need to revert [this commit](https://github.com/furey/mongodb-lens/commit/603b28cbde72fcd62a15cd324afc93028380a054) to add back `useNewUrlParser` and `useUnifiedTopology` MongoDB configuration options.

#### Older MongoDB Versions: Using NPX or Docker

If you prefer to use NPX or Docker, you'll need to use an older version of MongoDB Lens that was published with a compatible driver.

For example, MongoDB Lens `8.3.0` uses MongoDB Node.js driver `3.7.4` (see: [`package-lock.json`](https://github.com/furey/mongodb-lens/blob/8.3.0/package-lock.json#L944-L945)).

To run an older version of MongoDB Lens using NPX, specify the version tag:

```console
npx -y [email protected]
```

Similarly for Docker:

```console
docker run --rm -i --network=host furey/mongodb-lens:8.3.0
```

## Configuration

- [MongoDB Connection String](#configuration-mongodb-connection-string)
- [Config File](#configuration-config-file)
- [Config File Generation](#configuration-config-file-generation)
- [Multiple MongoDB Connections](#configuration-multiple-mongodb-connections)
- [Environment Variable Overrides](#configuration-environment-variable-overrides)
- [Cross-Platform Environment Variables](#configuration-cross-platform-environment-variables)

### Configuration: MongoDB Connection String

The server accepts a MongoDB connection string as its only argument.

Example NPX usage:

```console
npx -y mongodb-lens@latest mongodb://your-connection-string
```

MongoDB connection strings have the following format:

```txt
mongodb://[username:password@]host[:port][/database][?options]
```

Example connection strings:

- Local connection:<br>
  `mongodb://localhost:27017`
- Connection to `mydatabase` with credentials from `admin` database:<br>
  `mongodb://username:password@hostname:27017/mydatabase?authSource=admin`
- Connection to `mydatabase` with various other options:<br>
  `mongodb://hostname:27017/mydatabase?retryWrites=true&w=majority`

If no connection string is provided, the server will attempt to connect via local connection.

### Configuration: Config File

MongoDB Lens supports extensive customization via JSON configuration file.

> [!NOTE]<br>
> The config file is optional. MongoDB Lens will run with default settings if no config file is provided.

> [!TIP]<br>
> You only need to include the settings you want to customize in the config file. MongoDB Lens will use default settings for any omitted values.

> [!TIP]<br>
> MongoDB Lens supports both `.json` and `.jsonc` (JSON with comments) config file formats.

<details>
  <summary><strong>Example configuration file</strong></summary>

```jsonc
{
  "mongoUri": "mongodb://localhost:27017",         // Default MongoDB connection string or object of alias-URI pairs
  "connectionOptions": {
    "maxPoolSize": 20,                             // Maximum number of connections in the pool
    "retryWrites": false,                          // Whether to retry write operations
    "connectTimeoutMS": 30000,                     // Connection timeout in milliseconds
    "socketTimeoutMS": 360000,                     // Socket timeout in milliseconds
    "heartbeatFrequencyMS": 10000,                 // How often to ping servers for status
    "serverSelectionTimeoutMS": 30000              // Timeout for server selection
  },
  "defaultDbName": "admin",                        // Default database if not specified in URI
  "connection": {
    "maxRetries": 5,                               // Maximum number of initial connection attempts
    "maxRetryDelayMs": 30000,                      // Maximum delay between retries
    "reconnectionRetries": 10,                     // Maximum reconnection attempts if connection lost
    "initialRetryDelayMs": 1000                    // Initial delay between retries
  },
  "disabled": {
    "tools": [],                                   // Array of tools to disable or true to disable all
    "prompts": [],                                 // Array of prompts to disable or true to disable all
    "resources": []                                // Array of resources to disable or true to disable all
  },
  "enabled": {
    "tools": true,                                 // Array of tools to enable or true to enable all
    "prompts": true,                               // Array of prompts to enable or true to enable all
    "resources": true                              // Array of resources to enable or true to enable all
  },
  "cacheTTL": {
    "stats": 15000,                                // Stats cache lifetime in milliseconds
    "fields": 30000,                               // Fields cache lifetime in milliseconds
    "schemas": 60000,                              // Schema cache lifetime in milliseconds
    "indexes": 120000,                             // Index cache lifetime in milliseconds
    "collections": 30000,                          // Collections list cache lifetime in milliseconds
    "serverStatus": 20000                          // Server status cache lifetime in milliseconds
  },
  "enabledCaches": [                               // List of caches to enable
    "stats",                                       // Statistics cache
    "fields",                                      // Collection fields cache
    "schemas",                                     // Collection schemas cache
    "indexes",                                     // Collection indexes cache
    "collections",                                 // Database collections cache
    "serverStatus"                                 // MongoDB server status cache
  ],
  "memory": {
    "enableGC": true,                              // Whether to enable garbage collection
    "warningThresholdMB": 1500,                    // Memory threshold for warnings
    "criticalThresholdMB": 2000                    // Memory threshold for cache clearing
  },
  "logLevel": "info",                              // Log level (info or verbose)
  "disableDestructiveOperationTokens": false,      // Whether to skip confirmation for destructive ops
  "watchdogIntervalMs": 30000,                     // Interval for connection monitoring
  "defaults": {
    "slowMs": 100,                                 // Threshold for slow query detection
    "queryLimit": 10,                              // Default limit for query results
    "allowDiskUse": true,                          // Allow operations to use disk for large datasets
    "schemaSampleSize": 100,                       // Sample size for schema inference
    "aggregationBatchSize": 50                     // Batch size for aggregation operations
  },
  "security": {
    "tokenLength": 4,                              // Length of confirmation tokens
    "tokenExpirationMinutes": 5,                   // Expiration time for tokens
    "strictDatabaseNameValidation": true           // Enforce strict database name validation
  },
  "tools": {
    "transaction": {
      "readConcern": "snapshot",                   // Read concern level for transactions
      "writeConcern": {
        "w": "majority"                            // Write concern for transactions
      }
    },
    "bulkOperations": {
      "ordered": true                              // Whether bulk operations execute in order
    },
    "export": {
      "defaultLimit": -1,                          // Default limit for exports (-1 = no limit)
      "defaultFormat": "json"                      // Default export format (json or csv)
    },
    "watchChanges": {
      "maxDurationSeconds": 60,                    // Maximum duration for change streams
      "defaultDurationSeconds": 10                 // Default duration for change streams
    },
    "queryAnalysis": {
      "defaultDurationSeconds": 10                 // Default duration for query analysis
    }
  }
}
```

</details>

By default, MongoDB Lens looks for the config file at:

- `~/.mongodb-lens.jsonc` first, then falls back to
- `~/.mongodb-lens.json` if the former doesn't exist

To customize the config file path, set the environment variable `CONFIG_PATH` to the desired file path.

Example NPX usage:

```console
CONFIG_PATH='/path/to/config.json' npx -y mongodb-lens@latest
```

Example Docker Hub usage:

```console
docker run --rm -i --network=host --pull=always -v /path/to/config.json:/root/.mongodb-lens.json furey/mongodb-lens
```

### Configuration: Config File Generation

You can generate a configuration file automatically using the `config:create` script:

```console
# NPX Usage (recommended)
npx -y mongodb-lens@latest config:create

# Node.js Usage
npm run config:create

# Force overwrite existing files
npx -y mongodb-lens@latest config:create -- --force
npm run config:create -- --force
```

This script extracts the [example configuration file](#configuration-config-file) above and saves it to: `~/.mongodb-lens.jsonc`

#### Config File Generation: Custom Path

You can specify a custom output location using the `CONFIG_PATH` environment variable.

- If `CONFIG_PATH` has no file extension, it's treated as a directory and `.mongodb-lens.jsonc` is appended
- If `CONFIG_PATH` ends with `.json` (not `.jsonc`) comments are removed from the generated file

Example NPX usage:

```console
# With custom path
CONFIG_PATH=/path/to/config.jsonc npx -y mongodb-lens@latest config:create

# Save to directory (will append .mongodb-lens.jsonc to the path)
CONFIG_PATH=/path/to/directory npx -y mongodb-lens@latest config:create

# Save as JSON instead of JSONC
CONFIG_PATH=/path/to/config.json npx -y mongodb-lens@latest config:create
```

Example Node.js usage:

```console
# With custom path
CONFIG_PATH=/path/to/config.jsonc node mongodb-lens.js config:create

# Save to directory (will append .mongodb-lens.jsonc to the path)
CONFIG_PATH=/path/to/directory node mongodb-lens.js config:create

# Save as JSON instead of JSONC
CONFIG_PATH=/path/to/config.json node mongodb-lens.js config:create
```

### Configuration: Multiple MongoDB Connections

MongoDB Lens supports multiple MongoDB URIs with aliases in your [config file](#configuration-config-file), allowing you to easily switch between different MongoDB instances using simple names.

To configure multiple connections, set the `mongoUri` config setting to an object with alias-URI pairs:

```json
{
  "mongoUri": {
    "main": "mongodb://localhost:27017",
    "backup": "mongodb://localhost:27018",
    "atlas": "mongodb+srv://username:[email protected]/mydb"
  }
}
```

With this configuration:

- The first URI in the list (e.g. `main`) becomes the default connection at startup
- You can switch connections using natural language: `"Connect to backup"` or `"Connect to atlas"`
- The original syntax still works: `"Connect to mongodb://localhost:27018"`
- The `list-connections` tool shows all available connection aliases

> [!NOTE]<br>
> When using the command-line argument to specify a connection, you can use either a full MongoDB URI or an alias defined in your configuration file.

> [!TIP]<br>
> To add connection aliases at runtime, use the `add-connection-alias` tool.

### Configuration: Environment Variable Overrides

MongoDB Lens supports environment variable overrides for configuration settings.

Environment variables take precedence over [config file](#configuration-config-file) settings.

Config environment variables follow the naming pattern:

```txt
CONFIG_[SETTING PATH, SNAKE CASED, UPPERCASED]
```

Example overrides:

| Config Setting                   | Environment Variable Override             |
| -------------------------------- | ----------------------------------------- |
| `mongoUri`                       | `CONFIG_MONGO_URI`                        |
| `logLevel`                       | `CONFIG_LOG_LEVEL`                        |
| `defaultDbName`                  | `CONFIG_DEFAULT_DB_NAME`                  |
| `defaults.queryLimit`            | `CONFIG_DEFAULTS_QUERY_LIMIT`             |
| `tools.export.defaultFormat`     | `CONFIG_TOOLS_EXPORT_DEFAULT_FORMAT`      |
| `connectionOptions.maxPoolSize`  | `CONFIG_CONNECTION_OPTIONS_MAX_POOL_SIZE` |
| `connection.reconnectionRetries` | `CONFIG_CONNECTION_RECONNECTION_RETRIES`  |

For environment variable values:

- For boolean settings, use string values `'true'` or `'false'`.
- For numeric settings, use string representations.
- For nested objects or arrays, use JSON strings.

Example NPX usage:

```console
CONFIG_DEFAULTS_QUERY_LIMIT='25' npx -y mongodb-lens@latest
```

Example Docker Hub usage:

```console
docker run --rm -i --network=host --pull=always -e CONFIG_DEFAULTS_QUERY_LIMIT='25' furey/mongodb-lens
```

### Configuration: Cross-Platform Environment Variables

For consistent environment variable usage across Windows, macOS, and Linux, consider using `cross-env`:

1. Install cross-env globally:<br>
   ```console
   # Using NPM
   npm install -g cross-env

   # Using Volta (see: https://volta.sh)
   volta install cross-env
   ```
1. Prefix any NPX or Node.js environment variables in this document's examples:<br>
   ```console
   # Example NPX usage with cross-env
   cross-env CONFIG_DEFAULTS_QUERY_LIMIT='25' npx -y mongodb-lens@latest

   # Example Node.js usage with cross-env
   cross-env CONFIG_DEFAULTS_QUERY_LIMIT='25' node mongodb-lens.js
   ```

## Client Setup

- [Claude Desktop](#client-setup-claude-desktop)
- [MCP Inspector](#client-setup-mcp-inspector)
- [Other MCP Clients](#client-setup-other-mcp-clients)

### Client Setup: Claude Desktop

To use MongoDB Lens with Claude Desktop:

1. Install [Claude Desktop](https://claude.ai/download)
1. Open `claude_desktop_config.json` (create if it doesn't exist):
    - macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
    - Windows: `%APPDATA%\Claude\claude_desktop_config.json`
1. Add the MongoDB Lens server configuration as per [configuration options](#claude-desktop-configuration-options)
1. Restart Claude Desktop
1. Start a conversation with Claude about your MongoDB data

#### Claude Desktop Configuration Options

- [Option 1: NPX (Recommended)](#option-1-npx-recommended)
- [Option 2: Docker Hub Image](#option-2-docker-hub-image)
- [Option 3: Local Node.js Installation](#option-3-local-nodejs-installation)
- [Option 4: Local Docker Image](#option-4-local-docker-image)

For each option:

- Replace `mongodb://your-connection-string` with your MongoDB connection string or omit it to use the default `mongodb://localhost:27017`.
- To use a custom config file, set [`CONFIG_PATH`](#configuration-config-file) environment variable.
- To include environment variables:
  - For NPX or Node.js add `"env": {}` with key-value pairs, for example:<br>
    ```json
    "command": "/path/to/npx",
    "args": [
      "-y",
      "mongodb-lens@latest",
      "mongodb://your-connection-string"
    ],
    "env": {
      "CONFIG_LOG_LEVEL": "verbose"
    }
    ```
  - For Docker add `-e` flags, for example:<br>
    ```json
    "command": "docker",
    "args": [
      "run", "--rm", "-i",
      "--network=host",
      "--pull=always",
      "-e", "CONFIG_LOG_LEVEL=verbose",
      "furey/mongodb-lens",
      "mongodb://your-connection-string"
    ]
    ```

##### Option 1: NPX (Recommended)

```json
{
  "mcpServers": {
    "mongodb-lens": {
      "command": "/path/to/npx",
      "args": [
        "-y",
        "mongodb-lens@latest",
        "mongodb://your-connection-string"
      ]
    }
  }
}
```

##### Option 2: Docker Hub Image

```json
{
  "mcpServers": {
    "mongodb-lens": {
      "command": "docker",
      "args": [
        "run", "--rm", "-i",
        "--network=host",
        "--pull=always",
        "furey/mongodb-lens",
        "mongodb://your-connection-string"
      ]
    }
  }
}
```

##### Option 3: Local Node.js Installation

```json
{
  "mcpServers": {
    "mongodb-lens": {
      "command": "/path/to/node",
      "args": [
        "/path/to/mongodb-lens.js",
        "mongodb://your-connection-string"
      ]
    }
  }
}
```

##### Option 4: Local Docker Image

```json
{
  "mcpServers": {
    "mongodb-lens": {
      "command": "docker",
      "args": [
        "run", "--rm", "-i",
        "--network=host",
        "mongodb-lens",
        "mongodb://your-connection-string"
      ]
    }
  }
}
```

### Client Setup: MCP Inspector

[MCP Inspector](https://github.com/modelcontextprotocol/inspector) is a tool designed for testing and debugging MCP servers.

> [!NOTE]<br>
> MCP Inspector starts a proxy server on port 3000 and web client on port 5173.

Example NPX usage:

1. Run MCP Inspector:<br>
    ```console
    # Using default connection string mongodb://localhost:27017
    npx -y @modelcontextprotocol/inspector npx -y mongodb-lens@latest

    # Using custom connection string
    npx -y @modelcontextprotocol/inspector npx -y mongodb-lens@latest mongodb://your-connection-string

    # Using custom ports
    SERVER_PORT=1234 CLIENT_PORT=5678 npx -y @modelcontextprotocol/inspector npx -y mongodb-lens@latest
    ```
1. Open MCP Inspector: http://localhost:5173

MCP Inspector should support the full range of MongoDB Lens capabilities, including autocompletion for collection names and query fields.

For more, see: [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector)

### Client Setup: Other MCP Clients

MongoDB Lens should be usable with any MCP-compatible client.

For more, see: [MCP Documentation: Example Clients](https://modelcontextprotocol.io/clients)

## Data Protection

To protect your data while using MongoDB Lens, consider the following:

- [Read-Only User Accounts](#data-protection-read-only-user-accounts)
- [Working with Database Backups](#data-protection-working-with-database-backups)
- [Data Flow Considerations](#data-protection-data-flow-considerations)
- [Confirmation for Destructive Operations](#data-protection-confirmation-for-destructive-operations)
- [Disabling Destructive Operations](#data-protection-disabling-destructive-operations)

### Data Protection: Read-Only User Accounts

When connecting MongoDB Lens to your database, the permissions granted to the user in the MongoDB connection string dictate what actions can be performed. When the use case fits, a read-only user can prevent unintended writes or deletes, ensuring MongoDB Lens can query data but not alter it.

To set this up, create a user with the `read` role scoped to the database(s) you're targeting. In MongoDB shell, you'd run something like:

```js
use admin

db.createUser({
  user: 'readonly',
  pwd: 'eXaMpLePaSsWoRd',
  roles: [{ role: 'read', db: 'mydatabase' }]
})
```

Then, apply those credentials to your MongoDB connection string:

```txt
mongodb://readonly:eXaMpLePaSsWoRd@localhost:27017/mydatabase
```

Using read-only credentials is a simple yet effective way to enforce security boundaries, especially when you're poking around schemas or running ad-hoc queries.

### Data Protection: Working with Database Backups

When working with MongoDB Lens, consider connecting to a backup copy of your data hosted on a separate MongoDB instance.

Start by generating the backup with `mongodump`. Next, spin up a fresh MongoDB instance (e.g. on a different port like `27018`) and restore the backup there using `mongorestore`. Once it's running, point MongoDB Lens to the backup instance's connection string (e.g. `mongodb://localhost:27018/mydatabase`).

This approach gives you a sandbox to test complex or destructive operations against without risking accidental corruption of your live data.

### Data Protection: Data Flow Considerations

- [How Your Data Flows Through the System](#data-flow-considerations-how-your-data-flows-through-the-system)
- [Protecting Sensitive Data with Projection](#data-flow-considerations-protecting-sensitive-data-with-projection)
- [Connection Aliases and Passwords](#data-flow-considerations-connection-aliases-and-passwords)
- [Local Setup for Maximum Safety](#data-flow-considerations-local-setup-for-maximum-safety)

#### Data Flow Considerations: How Your Data Flows Through the System

When using an MCP Server with a remote LLM provider (such as Anthropic via Claude Desktop) understanding how your data flows through the system is key to protecting sensitive information from unintended exposure.

When you send a MongoDB related query through your MCP client, here’s what happens:

> [!NOTE]<br>
> While this example uses a local MongoDB instance, the same principles apply to remote MongoDB instances.

```mermaid
sequenceDiagram
    actor User
    box Local Machine #d4f1f9
        participant Client as MCP Client
        participant Lens as MongoDB Lens
        participant MongoDB as MongoDB Instance
    end
    box Remote Server #ffe6cc
        participant LLM as Remote LLM Provider
    end

    User->>Client: 1. Submit request<br>"Show me all users older than 30"
    Client->>LLM: 2. User request + available tools
    Note over LLM: Interprets request<br>Chooses appropriate tool
    LLM->>Client: 3. Tool selection (find-documents)
    Client->>Lens: 4. Tool run with parameters
    Lens->>MongoDB: 5. Database query
    MongoDB-->>Lens: 6. Database results
    Lens-->>Client: 7. Tool results (formatted data)
    Client->>LLM: 8. Tool results
    Note over LLM: Processes results<br>Formats response
    LLM-->>Client: 9. Processed response
    Client-->>User: 10. Final answer
```

1. You submit a request<br><sup>➥ e.g. "Show me all users older than 30"</sup>
1. Your client sends the request to the remote LLM<br><sup>➥ The LLM provider receives your exact words along with a list of available MCP tools and their parameters.</sup>
1. The remote LLM interprets your request<br><sup>➥ It determines your intent and instructs the client to use a specific MCP tool with appropriate parameters.</sup>
1. The client asks MongoDB Lens to run the tool<br><sup>➥ This occurs locally on your machine via stdio.</sup>
1. MongoDB Lens queries your MongoDB database
1. MongoDB Lens retrieves your MongoDB query results
1. MongoDB Lens sends the data back to the client<br><sup>➥ The client receives results formatted by MongoDB Lens.</sup>
1. The client forwards the data to the remote LLM<br><sup>➥ The LLM provider sees the exact data returned by MongoDB Lens.</sup>
1. The remote LLM processes the data<br><sup>➥ It may summarize or format the results further.</sup>
1. The remote LLM sends the final response to the client<br><sup>➥ The client displays the answer to you.</sup>

The remote LLM provider sees both your original request and the full response from MongoDB Lens. If your database includes sensitive fields (e.g. passwords, personal details, etc) this data could be unintentionally transmitted to the remote provider unless you take precautions.

#### Data Flow Considerations: Protecting Sensitive Data with Projection

To prevent sensitive data from being sent to the remote LLM provider, use the concept of projection when using tools like `find-documents`, `aggregate-data`, or `export-data`. Projection allows you to specify which fields to include or exclude in query results, ensuring sensitive information stays local.

Example projection usage:

- _"Show me all users older than 30, but use projection to hide their passwords."_<br>
  <sup>➥ Uses `find-documents` tool with projection</sup>

#### Data Flow Considerations: Connection Aliases and Passwords

When adding new connection aliases using the `add-connection-alias` tool, avoid added aliases to URIs that contain passwords if you're using a remote LLM provider. Since your request is sent to the LLM, any passwords in the URI could be exposed. Instead, define aliases with passwords in the MongoDB Lens [config file](#configuration-multiple-mongodb-connections), where they remain local and are not transmitted to the LLM.

#### Data Flow Considerations: Local Setup for Maximum Safety

While outside the scope of this document, for the highest level of data privacy, consider using a local MCP client paired with a locally hosted LLM model. This approach keeps all requests and data within your local environment, eliminating the risk of sensitive information being sent to a remote provider.

### Data Protection: Confirmation for Destructive Operations

MongoDB Lens implements a token-based confirmation system for potentially destructive operations, requiring a two-step process to execute tools that may otherwise result in unchecked data loss:

1. First tool invocation: Returns a 4-digit confirmation token that expires after 5 minutes
1. Second tool invocation: Executes the operation if provided with the valid token

For an example of the confirmation process, see: [Working with Confirmation Protection](#tutorial-5-working-with-confirmation-protection)

Tools that require confirmation include:

- `drop-user`: Remove a database user
- `drop-index`: Remove an index (potential performance impact)
- `drop-database`: Permanently delete a database
- `drop-collection`: Delete a collection and all its documents
- `delete-document`: Delete one or multiple documents
- `bulk-operations`: When including delete operations
- `rename-collection`: When the target collection exists and will be dropped

This protection mechanism aims to prevent accidental data loss from typos and unintended commands. It's a safety net ensuring you're aware of the consequences before proceeding with potentially harmful actions.

> [!NOTE]<br>
> If you're working in a controlled environment where data loss is acceptable, you can configure MongoDB Lens to [bypass confirmation](#bypassing-confirmation-for-destructive-operations) and perform destructive operations immediately.

#### Bypassing Confirmation for Destructive Operations

You might want to bypass the token confirmation system.

Set the environment variable `CONFIG_DISABLE_DESTRUCTIVE_OPERATION_TOKENS` to `true` to execute destructive operations immediately without confirmation:

```console
# Using NPX
CONFIG_DISABLE_DESTRUCTIVE_OPERATION_TOKENS=true npx -y mongodb-lens@latest

# Using Docker
docker run --rm -i --network=host --pull=always -e CONFIG_DISABLE_DESTRUCTIVE_OPERATION_TOKENS='true' furey/mongodb-lens
```

> [!WARNING]<br>
> Disabling confirmation tokens removes an important safety mechanism. It's strongly recommended to only use this option in controlled environments where data loss is acceptable, such as development or testing. Disable at your own risk.

### Data Protection: Disabling Destructive Operations

- [Disabling Tools](#disabling-tools)
- [High-Risk Tools](#high-risk-tools)
- [Medium-Risk Tools](#medium-risk-tools)
- [Read-Only Configuration](#read-only-configuration)
- [Selective Component Enabling](#selective-component-enabling)

#### Disabling Tools

MongoDB Lens includes several tools that can modify or delete data. To disable specific tools, add them to the `disabled.tools` array in your [configuration file](#configuration-config-file):

```json
{
  "disabled": {
    "tools": [
      "drop-user",
      "drop-index",
      "drop-database",
      "drop-collection",
      "delete-document",
      "bulk-operations",
      "rename-collection"
    ]
  }
}
```

> [!NOTE]<br>
> Resources and prompts can also be disabled via `disabled.resources` and `disabled.prompts` settings.

#### High-Risk Tools

These tools can cause immediate data loss and should be considered for disabling in sensitive environments:

- `drop-user`: Removes database users and their access permissions
- `drop-index`: Removes indexes (can impact query performance)
- `drop-database`: Permanently deletes entire databases
- `drop-collection`: Permanently deletes collections and all their documents
- `delete-document`: Removes documents matching specified criteria
- `bulk-operations`: Can perform batch deletions when configured to do so
- `rename-collection`: Can overwrite existing collections when using the drop target option

#### Medium-Risk Tools

These tools can modify data but typically don't cause immediate data loss:

- `create-user`: Creates users with permissions that could enable further changes
- `transaction`: Executes multiple operations in a transaction (potential for complex changes)
- `update-document`: Updates documents which could overwrite existing data

#### Read-Only Configuration

For a complete read-only configuration, disable all potentially destructive tools:

```json
{
  "disabled": {
    "tools": [
      "drop-user",
      "drop-index",
      "create-user",
      "transaction",
      "create-index",
      "drop-database",
      "drop-collection",
      "insert-document",
      "update-document",
      "delete-document",
      "bulk-operations",
      "create-database",
      "gridfs-operation",
      "create-collection",
      "rename-collection",
      "create-timeseries"
    ]
  }
}
```

This configuration allows MongoDB Lens to query and analyze data while preventing any modifications, providing multiple layers of protection against accidental data loss.

#### Selective Component Enabling

In addition to [disabling components](#disabling-tools), specify exactly which components should be enabled (implicitly disabling all others) using the `enabled` settings in your [configuration file](#configuration-config-file):

```json
{
  "enabled": {
    "tools": [
      "use-database",
      "find-documents",
      "count-documents",
      "aggregate-data"
    ]
  },
  "disabled": {
    "resources": true,
    "prompts": true
  }
}
```

> [!IMPORTANT]<br>
> If a component appears in both `enabled` and `disabled` lists, the `enabled` setting takes precedence.

## Tutorial

This following tutorial guides you through setting up a MongoDB container with sample data, then using MongoDB Lens to interact with it through natural language queries:

1. [Start Sample Data Container](#tutorial-1-start-sample-data-container)
1. [Import Sample Data](#tutorial-2-import-sample-data)
1. [Connect MongoDB Lens](#tutorial-3-connect-mongodb-lens)
1. [Example Queries](#tutorial-4-example-queries)
1. [Working With Confirmation Protection](#tutorial-5-working-with-confirmation-protection)

### Tutorial: 1. Start Sample Data Container

> [!NOTE]<br>
> This tutorial assumes you have [Docker](https://docs.docker.com/get-started/get-docker/) installed and running on your system.

> [!IMPORTANT]<br>
> If Docker is already running a container on port 27017, stop it before proceeding.

1. Initialise the sample data container:<br>
    ```console
    docker run --name mongodb-sampledata -d -p 27017:27017 mongo:6
    ```
1. Verify the container is running without issue:<br>
    ```console
    docker ps | grep mongodb-sampledata
    ```

### Tutorial: 2. Import Sample Data

MongoDB provides several [sample datasets](https://www.mongodb.com/docs/atlas/sample-data/#available-sample-datasets) which we'll use to explore MongoDB Lens.

1. Download the sample datasets:
    ```console<br>
    curl -LO https://atlas-education.s3.amazonaws.com/sampledata.archive
    ```
1. Copy the sample datasets into your sample data container:<br>
    ```console
    docker cp sampledata.archive mongodb-sampledata:/tmp/
    ```
1. Import the sample datasets into MongoDB:<br>
    ```console
    docker exec -it mongodb-sampledata mongorestore --archive=/tmp/sampledata.archive
    ```

This will import several databases:

- `sample_airbnb`: Airbnb listings and reviews
- `sample_analytics`: Customer and account data
- `sample_geospatial`: Geographic data
- `sample_mflix`: Movie data
- `sample_restaurants`: Restaurant data
- `sample_supplies`: Supply chain data
- `sample_training`: Training data for various applications
- `sample_weatherdata`: Weather measurements

### Tutorial: 3. Connect MongoDB Lens

[Install](#installation) MongoDB Lens as per the [Quick Start](#quick-start) instructions.

Set your [MCP Client](#client-setup) to connect to MongoDB Lens via: `mongodb://localhost:27017`

> [!TIP]<br>
> Omitting the connection string from your MCP Client configuration will default the connection string to `mongodb://localhost:27017`.

Example [Claude Desktop configuration](#client-setup-claude-desktop):

```json
{
  "mcpServers": {
    "mongodb-lens": {
      "command": "/path/to/npx",
      "args": [
        "-y",
        "mongodb-lens@latest"
      ]
    }
  }
}
```

### Tutorial: 4. Example Queries

With your MCP Client running and connected to MongoDB Lens, try the following example queries:

- [Example Queries: Basic Database Operations](#example-queries-basic-database-operations)
- [Example Queries: Collection Management](#example-queries-collection-management)
- [Example Queries: User Management](#example-queries-user-management)
- [Example Queries: Querying Data](#example-queries-querying-data)
- [Example Queries: Schema Analysis](#example-queries-schema-analysis)
- [Example Queries: Data Modification](#example-queries-data-modification)
- [Example Queries: Performance & Index Management](#example-queries-performance--index-management)
- [Example Queries: Geospatial & Special Operations](#example-queries-geospatial--special-operations)
- [Example Queries: Export, Administrative & Other Features](#example-queries-export-administrative--other-features)
- [Example Queries: Connection Management](#example-queries-connection-management)

#### Example Queries: Basic Database Operations

- _"List all databases"_<br>
  <sup>➥ Uses `list-databases` tool</sup>
- _"What db am I currently using?"_<br>
  <sup>➥ Uses `current-database` tool</sup>
- _"Switch to the sample_mflix database"_<br>
  <sup>➥ Uses `use-database` tool</sup>
- _"Create a new db called test_db"_<br>
  <sup>➥ Uses `create-database` tool</sup>
- _"Create another db called analytics_db and switch to it"_<br>
  <sup>➥ Uses `create-database` tool with switch=true</sup>
- _"Drop test_db"_<br>
  <sup>➥ Uses `drop-database` tool (with confirmation)</sup>

#### Example Queries: Collection Management

- _"What collections are in the current database?"_<br>
  <sup>➥ Uses `list-collections` tool</sup>
- _"Create user_logs collection"_<br>
  <sup>➥ Uses `create-collection` tool</sup>
- _"Rename user_logs to system_logs"_<br>
  <sup>➥ Uses `rename-collection` tool</sup>
- _"Drop system_logs"_<br>
  <sup>➥ Uses `drop-collection` tool (with confirmation)</sup>
- _"Check the data consistency in the movies collection"_<br>
  <sup>➥ Uses `validate-collection` tool</sup>

#### Example Queries: User Management

- _"Create a read-only user for analytics"_<br>
  <sup>➥ Uses `create-user` tool</sup>
- _"Drop the inactive_user account"_<br>
  <sup>➥ Uses `drop-user` tool (with confirmation)</sup>

#### Example Queries: Querying Data

- _"Count all docs in the movies collection"_<br>
  <sup>➥ Uses `count-documents` tool</sup>
- _"Find the top 5 movies with the highest IMDB rating"_<br>
  <sup>➥ Uses `find-documents` tool</sup>
- _"Show me aggregate data for movies grouped by decade"_<br>
  <sup>➥ Uses `aggregate-data` tool</sup>
- _"List all unique countries where movies were produced"_<br>
  <sup>➥ Uses `distinct-values` tool</sup>
- _"Search for movies containing godfather in their title"_<br>
  <sup>➥ Uses `text-search` tool</sup>
- _"Find German users with last name müller using proper collation"_<br>
  <sup>➥ Uses `collation-query` tool</sup>

#### Example Queries: Schema Analysis

- _"What's the schema structure of the movies collection?"_<br>
  <sup>➥ Uses `analyze-schema` tool</sup>
- _"Compare users and comments schemas"_<br>
  <sup>➥ Uses `compare-schemas` tool</sup>
- _"Generate a schema validator for the movies collection"_<br>
  <sup>➥ Uses `generate-schema-validator` tool</sup>
- _"Analyze common query patterns for the movies collection"_<br>
  <sup>➥ Uses `analyze-query-patterns` tool</sup>

#### Example Queries: Data Modification

- _"Insert new movie document: \<your field data\>"_<br>
  <sup>➥ Uses `insert-document` tool</sup>
- _"Update all movies from 1994 to add a 'classic' flag"_<br>
  <sup>➥ Uses `update-document` tool</sup>
- _"Delete all movies with zero ratings"_<br>
  <sup>➥ Uses `delete-document` tool (with confirmation)</sup>
- _"Run these bulk operations on the movies collection: \<your JSON data\>"_<br>
  <sup>➥ Uses `bulk-operations` tool</sup>

> [!TIP]<br>
> For specialized MongoDB operations (like array operations, bitwise operations, or other complex updates), use MongoDB's native operators via the `update-document` tool's `update` and `options` parameters.

#### Example Queries: Performance & Index Management

- _"Create an index on the title field in the movies collection"_<br>
  <sup>➥ Uses `create-index` tool</sup>
- _"Drop the ratings_idx index"_<br>
  <sup>➥ Uses `drop-index` tool (with confirmation)</sup>
- _"Explain the execution plan for finding movies from 1995"_<br>
  <sup>➥ Uses `explain-query` tool</sup>
- _"Get statistics for the current db"_<br>
  <sup>➥ Uses `get-stats` tool with target=database</sup>
- _"Show collection stats for the movies collection"_<br>
  <sup>➥ Uses `get-stats` tool with target=collection</sup>

#### Example Queries: Geospatial & Special Operations

- _"Switch to sample_geospatial db, then find all shipwrecks within 10km of coordinates [-80.12, 26.46]"_<br>
  <sup>➥ Uses `geo-query` tool</sup>
- _"Switch to sample_analytics db, then execute a transaction to move funds between accounts: \<account ids\>"_<br>
  <sup>➥ Uses `transaction` tool</sup>
- _"Create a time series collection for sensor readings"_<br>
  <sup>➥ Uses `create-timeseries` tool</sup>
- _"Watch for changes in the users collection for 30 seconds"_<br>
  <sup>➥ Uses `watch-changes` tool</sup>
- _"List all files in the images GridFS bucket"_<br>
  <sup>➥ Uses `gridfs-operation` tool with operation=list</sup>

#### Example Queries: Export, Administrative & Other Features

- _"Switch to sample_mflix db, then export the top 20 movies based on 'tomatoes.critic.rating' as a CSV with title, year and rating fields (output in a single code block)"_<br>
  <sup>➥ Uses `export-data` tool</sup>
- _"Switch to sample_analytics db, then check its sharding status"_<br>
  <sup>➥ Uses `shard-status` tool</sup>
- _"Clear the collections cache"_<br>
  <sup>➥ Uses `clear-cache` tool with target=collections</sup>
- _"Clear all caches"_<br>
  <sup>➥ Uses `clear-cache` tool</sup>
- _"Switch to sample_weatherdata db then generate an interactive report on its current state"_<br>
  <sup>➥ Uses numerous tools</sup>

#### Example Queries: Connection Management

- _"Connect to mongodb://localhost:27018"_<br>
  <sup>➥ Uses `connect-mongodb` tool</sup>
- _"Connect to mongodb+srv://username:[email protected]/mydb"_<br>
  <sup>➥ Uses `connect-mongodb` tool</sup>
- _"Connect back to the original mongodb instance"_<br>
  <sup>➥ Uses `connect-original` tool</sup>
- _"Connect to replica set without validating the connection: \<replica set details\>"_<br>
  <sup>➥ Uses `connect-mongodb` tool with validateConnection=false</sup>
- _"Add connection alias 'prod' for mongodb://username:password@prod-server:27017/mydb"_<br>
<sup>➥ Uses `add-connection-alias` tool</sup>

### Tutorial: 5. Working With Confirmation Protection

MongoDB Lens includes a safety mechanism for potentially destructive operations. Here's how it works in practice:

1. Request to drop a collection:<br>
    ```
    "Drop the collection named test_collection"
    ```
1. MongoDB Lens responds with a warning and confirmation token:<br>
    ```
    ⚠️ DESTRUCTIVE OPERATION WARNING ⚠️

    You've requested to drop the collection 'test_collection'.

    This operation is irreversible and will permanently delete all data in this collection.

    To confirm, you must type the 4-digit confirmation code EXACTLY as shown below:

    Confirmation code: 9876

    This code will expire in 5 minutes for security purposes.
    ```
1. Confirm the operation by submitting the confirmation token:<br>
    ```
    "9876"
    ```
1. MongoDB Lens executes the operation:<br>
    ```
    Collection 'test_collection' has been permanently deleted.
    ```

This two-step process prevents accidental data loss by requiring explicit confirmation.

> [!NOTE]<br>
> If you're working in a controlled environment where data loss is acceptable, you can configure MongoDB Lens to [bypass confirmation](#bypassing-confirmation-for-destructive-operations) and perform destructive operations immediately.

## Test Suite

MongoDB Lens includes a [test suite](./mongodb-lens.test.js) to verify functionality across tools, resources, and prompts.

- [Running Tests](#test-suite-running-tests)
- [Command Line Options](#test-suite-command-line-options)
- [Examples](#test-suite-examples)

### Test Suite: Running Tests

The test suite requires a `CONFIG_MONGO_URI` environment variable which can be set to:

- a MongoDB connection string (e.g. `mongodb://localhost:27017`)
- `mongodb-memory-server` (for in-memory testing)

```console
# Run Tests with MongoDB Connection String
CONFIG_MONGO_URI=mongodb://localhost:27017 node mongodb-lens.test.js

# Run Tests with In-Memory MongoDB (requires mongodb-memory-server)
CONFIG_MONGO_URI=mongodb-memory-server node mongodb-lens.test.js
```

For convenience, the following scripts are available for running tests:

```console
npm test                        # Fails if no CONFIG_MONGO_URI provided
npm run test:localhost          # Uses mongodb://localhost:27017
npm run test:localhost:verbose  # Runs with DEBUG=true for verbose output
npm run test:in-memory          # Uses mongodb-memory-server
npm run test:in-memory:verbose  # Runs with DEBUG=true for verbose output
```

> [!NOTE]<br>
> The test suite creates temporary databases and collections that are cleaned up after test completion.

### Test Suite: Command Line Options

| Option             | Description                                          |
| ------------------ | ---------------------------------------------------- |
| `--list`           | List all available tests without running them        |
| `--test=<n>`       | Run specific test(s) by name (comma-separated)       |
| `--group=<n>`      | Run all tests in specific group(s) (comma-separated) |
| `--pattern=<glob>` | Run tests matching pattern(s) (comma-separated)      |

### Test Suite: Examples

```console
# List All Available Tests
npm test -- --list

# Run Only Connection-Related Tests (:27017)
npm run test:localhost -- --group=Connection\ Tools

# Test Specific Database Operations (In-Memory)
npm run test:in-memory -- --test=create-database\ Tool,drop-database\ Tool

# Test All Document-Related Tools (:27017)
npm run test:localhost -- --pattern=document

# Run Resource Tests Only (In-Memory)
npm run test:in-memory -- --group=Resources

# Run Specific Tests Only (In-Memory)
npm run test:in-memory -- --test=aggregate-data\ Tool,find-documents\ Tool
```

## Disclaimer

MongoDB Lens:

- is licensed under the [MIT License](./LICENSE).
- is not affiliated with or endorsed by MongoDB, Inc.
- is written with the assistance of AI and may contain errors.
- is intended for educational and experimental purposes only.
- is provided as-is with no warranty—please use at your own risk.

## Support

If you've found MongoDB Lens helpful consider supporting my work through:

[Buy Me a Coffee](https://www.buymeacoffee.com/furey) | [GitHub Sponsorship](https://github.com/sponsors/furey)

Contributions help me continue developing and improving this tool, allowing me to dedicate more time to add new features and ensuring it remains a valuable resource for the community.

```

--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------

```yaml
# These are supported funding model platforms

github: furey
buy_me_a_coffee: furey

```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
FROM node:22-alpine

WORKDIR /app

COPY package*.json ./

RUN npm ci --production --no-fund --no-audit

COPY . .

ENTRYPOINT ["node", "mongodb-lens.js"]

```

--------------------------------------------------------------------------------
/.github/workflows/publish-npm.yml:
--------------------------------------------------------------------------------

```yaml
name: Publish to NPM

on:
  release:
    types: [created]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          registry-url: https://registry.npmjs.org/
      - run: npm ci
      - run: npm publish
        env:
          NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}

```

--------------------------------------------------------------------------------
/.github/workflows/publish-docker.yml:
--------------------------------------------------------------------------------

```yaml
name: Publish to Docker Hub

on:
  release:
    types: [created]

jobs:
  docker:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      
      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3
      
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
      
      - name: Login to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ vars.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}
      
      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: furey/mongodb-lens
          tags: |
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}
            latest

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          platforms: linux/amd64,linux/arm64
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}

```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
{
  "name": "mongodb-lens",
  "version": "9.1.4",
  "author": "James Furey (https://about.me/jamesfurey)",
  "description": "MongoDB Lens: Full Featured MCP Server for MongoDB Databases",
  "license": "MIT",
  "type": "module",
  "main": "mongodb-lens.js",
  "bin": {
    "mongodb-lens": "mongodb-lens.js"
  },
  "files": [
    "mongodb-lens.js",
    "config-create.js",
    "package.json",
    "README.md",
    "LICENSE"
  ],
  "scripts": {
    "start": "node mongodb-lens.js",
    "start:gc": "node --expose-gc mongodb-lens.js",
    "start:debug": "node --inspect mongodb-lens.js",
    "start:verbose": "cross-env CONFIG_LOG_LEVEL=verbose node mongodb-lens.js",
    "start:inspector": "npx -y @modelcontextprotocol/inspector node mongodb-lens",
    "docker:hub:run": "docker run --rm -i --network=host --pull always furey/mongodb-lens",
    "docker:local:build": "docker build -t mongodb-lens .",
    "docker:local:run": "docker run --rm -i --network=host mongodb-lens",
    "config:create": "node config-create.js",
    "test": "node mongodb-lens.test.js",
    "test:localhost": "cross-env CONFIG_MONGO_URI=mongodb://localhost:27017 node mongodb-lens.test.js",
    "test:localhost:verbose": "cross-env DEBUG=true CONFIG_MONGO_URI=mongodb://localhost:27017 node mongodb-lens.test.js",
    "test:in-memory": "cross-env CONFIG_MONGO_URI=mongodb-memory-server node mongodb-lens.test.js",
    "test:in-memory:verbose": "cross-env DEBUG=true CONFIG_MONGO_URI=mongodb-memory-server node mongodb-lens.test.js"
  },
  "keywords": [
    "ai",
    "llm",
    "mcp",
    "claude",
    "mongodb",
    "database",
    "model-context-protocol"
  ],
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.8.0",
    "cross-env": "^7.0.3",
    "lodash": "^4.17.21",
    "mongodb": "^6.15.0",
    "strip-json-comments": "^5.0.1",
    "zod": "^3.24.2"
  },
  "devDependencies": {
    "mongodb-memory-server": "^10.1.4"
  },
  "volta": {
    "node": "22.14.0"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/furey/mongodb-lens.git"
  },
  "engines": {
    "node": ">=18.0.0"
  }
}

```

--------------------------------------------------------------------------------
/.dockerhub/overview.md:
--------------------------------------------------------------------------------

```markdown
# MongoDB Lens

[![Docker Pulls](https://img.shields.io/docker/pulls/furey/mongodb-lens)](https://hub.docker.com/r/furey/mongodb-lens)<br>
[![Docker Image Size (latest by date)](https://img.shields.io/docker/image-size/furey/mongodb-lens)](https://hub.docker.com/r/furey/mongodb-lens)<br>
[![Buy Me a Coffee](https://img.shields.io/badge/Buy%20Me%20a%20Coffee-donate-orange.svg)](https://www.buymeacoffee.com/furey)

- [Overview](#overview)
- [Features](#features)
- [Usage](#usage)
- [Links](#links)
- [Disclaimer](#disclaimer)
- [Support](#support)

## Overview

<strong>MongoDB Lens</strong> is a Model Context Protocol (MCP) server providing full-featured MongoDB database access to LLMs like Claude. It enables natural language interaction with MongoDB databases for querying, aggregation, performance optimization, schema analysis, and more.

## Features

See: [GitHub Documentation → Features](https://github.com/furey/mongodb-lens?tab=readme-ov-file#features)

## Usage

```bash
# Basic usage with local MongoDB
docker run --rm -i --network=host furey/mongodb-lens

# With custom connection string
docker run --rm -i --network=host furey/mongodb-lens mongodb://username:password@hostname:27017/database

# With verbose logging
docker run --rm -i --network=host -e CONFIG_LOG_LEVEL='verbose' furey/mongodb-lens
```

## Links

- [NPM Package](https://www.npmjs.com/package/mongodb-lens)
- [GitHub Repository](https://github.com/furey/mongodb-lens)
- [GitHub Documentation](https://github.com/furey/mongodb-lens/blob/main/README.md)

## Disclaimer

MongoDB Lens:

- is licensed under the [MIT License](./LICENSE).
- is not affiliated with or endorsed by MongoDB, Inc.
- is written with the assistance of AI and may contain errors.
- is intended for educational and experimental purposes only.
- is provided as-is with no warranty or support—use at your own risk.

## Support

If you've found MongoDB Lens helpful consider supporting my work through:

[Buy Me a Coffee](https://www.buymeacoffee.com/furey) | [GitHub Sponsorship](https://github.com/sponsors/furey)

Contributions help me continue developing and improving this tool, allowing me to dedicate more time to add new features and ensuring it remains a valuable resource for the community.

```

--------------------------------------------------------------------------------
/config-create.js:
--------------------------------------------------------------------------------

```javascript
#!/usr/bin/env node

import { join, dirname, extname, resolve } from 'path'
import stripJsonComments from 'strip-json-comments'
import { readFile, writeFile } from 'fs/promises'
import { fileURLToPath } from 'url'
import { existsSync } from 'fs'

const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)

const run = async () => {
  const homeDir = process.env.HOME || process.env.USERPROFILE

  let configPath = process.env.CONFIG_PATH || join(homeDir, '.mongodb-lens.jsonc')

  if (!extname(configPath)) configPath = join(configPath, '.mongodb-lens.jsonc')

  if (!configPath.endsWith('.json') && !configPath.endsWith('.jsonc')) {
    console.error('Error: Configuration file must have .json or .jsonc extension')
    process.exit(1)
  }

  configPath = resolve(configPath)

  try {
    const configContent = await extractConfigFromReadme()

    if (existsSync(configPath)) {
      const overwrite = process.argv.includes('--force')
      if (!overwrite) {
        console.log(`Configuration file already exists at: ${configPath}`)
        console.log('Use --force to overwrite it.')
        return
      }
      console.log(`Overwriting existing configuration file at: ${configPath}`)
    }

    let finalContent = configContent
    if (configPath.endsWith('.json')) finalContent = stripJsonComments(configContent)

    finalContent = finalContent
      .split('\n')
      .map(line => line.trimEnd())
      .join('\n')

    await writeFile(configPath, finalContent, 'utf8')
    console.log(`Configuration file created successfully at: ${configPath}`)
  } catch (error) {
    console.error('Error creating configuration file:', error.message)
    process.exit(1)
  }
}

const extractConfigFromReadme = async () => {
  try {
    const possiblePaths = [
      join(__dirname, 'README.md'),
      join(__dirname, '..', 'README.md'),
      join(process.cwd(), 'README.md')
    ]

    let readmeContent = null

    for (const path of possiblePaths) {
      if (existsSync(path)) {
        readmeContent = await readFile(path, 'utf8')
        console.log(`Found README at: ${path}`)
        break
      }
    }

    if (!readmeContent) throw new Error('README.md not found in expected locations')

    const configRegex = /Example configuration file[\s\S]*?```jsonc\s*([\s\S]*?)```/
    const match = readmeContent.match(configRegex)

    if (!match || !match[1]) throw new Error('Could not find example configuration in README.md')

    return match[1].trim()
  } catch (error) {
    console.error('Error extracting configuration from README:', error.message)
    throw error
  }
}

run()

```

--------------------------------------------------------------------------------
/mongodb-lens.test.js:
--------------------------------------------------------------------------------

```javascript
#!/usr/bin/env node

import { spawn } from 'child_process'
import { dirname, join } from 'path'
import { fileURLToPath } from 'url'
import { existsSync } from 'fs'
import mongodb from 'mongodb'

const { MongoClient, ObjectId } = mongodb

const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)

const runTests = async () => {
  parseCommandLineArgs()

  if (process.argv.includes('--list')) return listAllTests()

  try {
    logHeader('MongoDB Lens Test Suite', 'margin:bottom')

    await initialize()

    logHeader('Testing Tools', 'margin:top,bottom')

    for (const group of TEST_GROUPS) {
      if (group.name !== 'Resources' && group.name !== 'Prompts') {
        await runTestGroup(group.name, group.tests)
      }
    }

    logHeader('Testing Resources', 'margin:top,bottom')

    const resourcesGroup = TEST_GROUPS.find(g => g.name === 'Resources')
    if (resourcesGroup) await runTestGroup(resourcesGroup.name, resourcesGroup.tests)

    logHeader('Testing Prompts', 'margin:top,bottom')

    const promptsGroup = TEST_GROUPS.find(g => g.name === 'Prompts')
    if (promptsGroup) await runTestGroup(promptsGroup.name, promptsGroup.tests)
  } finally {
    await cleanup()
    displayTestSummary()
  }
}

const runTestGroup = async (groupName, tests) => {
  let anyTestsInGroup = false

  for (const test of tests) {
    if (shouldRunTest(test.name, groupName)) {
      anyTestsInGroup = true
      break
    }
  }

  if (!anyTestsInGroup) return console.log(`${COLORS.yellow}Skipping group: ${groupName} - No matching tests${COLORS.reset}`)

  console.log(`${COLORS.blue}Running tests in group: ${groupName}${COLORS.reset}`)

  for (const test of tests) {
    if (shouldRunTest(test.name, groupName)) {
      await runTest(test.name, test.fn)
    } else {
      skipTest(test.name, 'Not selected for execution')
    }
  }
}

const shouldRunTest = (testName, groupName) => {
  if (!testFilters.length && !groupFilters.length && !patternFilters.length) return true

  if (testFilters.includes(testName)) return true

  if (groupFilters.includes(groupName)) return true

  for (const pattern of patternFilters) {
    const regexPattern = pattern.replace(/\*/g, '.*')
    const regex = new RegExp(regexPattern, 'i')
    if (regex.test(testName)) return true
  }

  return false
}

const listAllTests = () => {
  logHeader('Available Test Groups & Tests', 'margin:bottom')

  TEST_GROUPS.forEach(group => {
    console.log(`${COLORS.yellow}${group.name}:${COLORS.reset}`)
    group.tests.forEach(test => {
      console.log(`- ${test.name}`)
    })
    console.log('')
  })

  console.log(`${COLORS.yellow}Usage:${COLORS.reset}`)
  console.log('  --test=<test-name>     Run specific test(s), comma separated')
  console.log('  --group=<group-name>   Run all tests in specific group(s), comma separated')
  console.log('  --pattern=<pattern>    Run tests matching pattern (glob style, e.g. *collection*)')
  console.log('  --list                 List all available tests without running them')

  process.exit(0)
}

const parseCommandLineArgs = () => {
  for (let i = 2; i < process.argv.length; i++) {
    const arg = process.argv[i]

    if (arg.startsWith('--test=')) {
      const tests = arg.replace('--test=', '').split(',')
      testFilters.push(...tests.map(t => t.trim()))
    } else if (arg.startsWith('--group=')) {
      const groups = arg.replace('--group=', '').split(',')
      groupFilters.push(...groups.map(g => g.trim()))
    } else if (arg.startsWith('--pattern=')) {
      const patterns = arg.replace('--pattern=', '').split(',')
      patternFilters.push(...patterns.map(p => p.trim()))
    }
  }

  if (testFilters.length || groupFilters.length || patternFilters.length) {
    console.log(`${COLORS.blue}Running with filters:${COLORS.reset}`)
    if (testFilters.length) console.log(`${COLORS.blue}Tests: ${testFilters.join(', ')}${COLORS.reset}`)
    if (groupFilters.length) console.log(`${COLORS.blue}Groups: ${groupFilters.join(', ')}${COLORS.reset}`)
    if (patternFilters.length) console.log(`${COLORS.blue}Patterns: ${patternFilters.join(', ')}${COLORS.reset}`)
  }
}

const initialize = async () => {
  mongoUri = await setupMongoUri()
  await connectToMongo()
  await setupTestEnvironment()
}

const setupMongoUri = async () => {
  const uri = process.env.CONFIG_MONGO_URI

  if (uri === 'mongodb-memory-server') return await startInMemoryMongo()

  if (!uri) {
    console.error(`${COLORS.red}No MongoDB URI provided. Please set CONFIG_MONGO_URI environment variable.${COLORS.reset}`)
    console.error(`${COLORS.yellow}Example: CONFIG_MONGO_URI=mongodb://localhost:27017 node mongodb-lens.test.js${COLORS.reset}`)
    console.error(`${COLORS.yellow}Example: CONFIG_MONGO_URI=mongodb-memory-server node mongodb-lens.test.js${COLORS.reset}`)
    process.exit(1)
  }

  return uri
}

const startInMemoryMongo = async () => {
  console.log(`${COLORS.yellow}In-memory MongoDB requested. Checking if mongodb-memory-server is available…${COLORS.reset}`)
  try {
    const { MongoMemoryServer } = await import('mongodb-memory-server')
    const mongod = await MongoMemoryServer.create()
    const uri = mongod.getUri()
    console.log(`${COLORS.green}In-memory MongoDB instance started at: ${uri}${COLORS.reset}`)

    process.on('exit', async () => {
      console.log(`${COLORS.yellow}Stopping in-memory MongoDB server…${COLORS.reset}`)
      await mongod.stop()
    })

    return uri
  } catch (err) {
    console.error(`${COLORS.red}Failed to start in-memory MongoDB: ${err.message}${COLORS.reset}`)
    console.error(`${COLORS.yellow}Install with: npm install mongodb-memory-server --save-dev${COLORS.reset}`)
    process.exit(1)
  }
}

const connectToMongo = async () => {
  console.log(`${COLORS.blue}Connecting to MongoDB: ${obfuscateMongoUri(mongoUri)}${COLORS.reset}`)

  if (!existsSync(MONGODB_LENS_PATH)) {
    console.error(`${COLORS.red}MongoDB Lens script not found at: ${MONGODB_LENS_PATH}${COLORS.reset}`)
    process.exit(1)
  }

  directMongoClient = new MongoClient(mongoUri, {
    retryWrites: true
  })

  await directMongoClient.connect()
  console.log(`${COLORS.green}Connected to MongoDB successfully.${COLORS.reset}`)
}

const obfuscateMongoUri = uri => {
  if (!uri || typeof uri !== 'string') return uri
  try {
    if (uri.includes('@') && uri.includes('://')) {
      const parts = uri.split('@')
      const authPart = parts[0]
      const restPart = parts.slice(1).join('@')
      const authIndex = authPart.lastIndexOf('://')
      if (authIndex !== -1) {
        const protocol = authPart.substring(0, authIndex + 3)
        const credentials = authPart.substring(authIndex + 3)
        if (credentials.includes(':')) {
          const [username] = credentials.split(':')
          return `${protocol}${username}:********@${restPart}`
        }
      }
    }
    return uri
  } catch (error) {
    return uri
  }
}

const setupTestEnvironment = async () => {
  await checkServerCapabilities()
  testDb = directMongoClient.db(TEST_DB_NAME)
  await cleanupTestDatabase()
  await setupTestData()
  console.log(`${COLORS.blue}Running tests against MongoDB Lens…${COLORS.reset}`)
}

const checkServerCapabilities = async () => {
  try {
    const adminDb = directMongoClient.db('admin')

    const replSetStatus = await adminDb.command({ replSetGetStatus: 1 }).catch(() => null)
    isReplSet = !!replSetStatus
    console.log(`${COLORS.blue}MongoDB instance ${isReplSet ? 'is' : 'is not'} a replica set.${COLORS.reset}`)

    const listShards = await adminDb.command({ listShards: 1 }).catch(() => null)
    isSharded = !!listShards
    console.log(`${COLORS.blue}MongoDB instance ${isSharded ? 'is' : 'is not'} a sharded cluster.${COLORS.reset}`)
  } catch (error) {
    console.log(`${COLORS.yellow}Not a replica set: ${error.message}${COLORS.reset}`)
  }
}

const setupTestData = async () => {
  try {
    testCollection = testDb.collection(TEST_COLLECTION_NAME)
    await createTestDocuments()
    await createTestIndexes()
    await createTestGeospatialData()
    await createTestUser()
    console.log(`${COLORS.green}Test data setup complete.${COLORS.reset}`)
  } catch (err) {
    console.error(`${COLORS.red}Error setting up test data: ${err.message}${COLORS.reset}`)
  }
}

const createTestDocuments = async () => {
  const testDocuments = Array(50).fill(0).map((_, i) => ({
    _id: new ObjectId(),
    name: `Test Document ${i}`,
    value: i,
    tags: [`tag${i % 5}`, `category${i % 3}`],
    isActive: i % 2 === 0,
    createdAt: new Date(Date.now() - i * 86400000)
  }))

  await testCollection.insertMany(testDocuments)

  const anotherCollection = testDb.collection(ANOTHER_TEST_COLLECTION)
  await anotherCollection.insertMany([
    { name: 'Test 1', value: 10 },
    { name: 'Test 2', value: 20 }
  ])

  await testCollection.insertOne({
    name: 'Date Test',
    startDate: new Date('2023-01-01'),
    endDate: new Date('2023-12-31')
  })
}

const createTestIndexes = async () => {
  await testCollection.createIndex({ name: 1 })
  await testCollection.createIndex({ value: -1 })
  await testCollection.createIndex({ tags: 1 })
  await testCollection.createIndex({ name: 'text', tags: 'text' })
  await testCollection.createIndex({ location: '2dsphere' })
}

const createTestGeospatialData = async () => {
  await testCollection.insertMany([
    {
      name: 'Geo Test 1',
      location: { type: 'Point', coordinates: [-73.9857, 40.7484] }
    },
    {
      name: 'Geo Test 2',
      location: { type: 'Point', coordinates: [-118.2437, 34.0522] }
    }
  ])
}

const createTestUser = async () => {
  try {
    await testDb.command({
      createUser: 'testuser',
      pwd: 'testpassword',
      roles: [{ role: 'read', db: TEST_DB_NAME }]
    })
  } catch (e) {
    console.log(`${COLORS.yellow}Could not create test user: ${e.message}${COLORS.reset}`)
  }
}

const runTest = async (name, testFn) => {
  stats.total++
  console.log(`${COLORS.blue}Running test: ${name}${COLORS.reset}`)
  try {
    const startTime = Date.now()
    await testFn()
    const duration = Date.now() - startTime
    console.log(`${COLORS.green}✓ PASS: ${name} (${duration}ms)${COLORS.reset}`)
    stats.passed++
  } catch (err) {
    console.error(`${COLORS.red}✗ FAIL: ${name} - ${err.message}${COLORS.reset}`)
    console.error(`${COLORS.red}Stack: ${err.stack}${COLORS.reset}`)
    stats.failed++
  }
}

const skipTest = (name, reason) => {
  stats.total++
  stats.skipped++
  console.log(`${COLORS.yellow}⚠ SKIP: ${name} - ${reason}${COLORS.reset}`)
}

const startLensServer = async () => {
  console.log('Starting MongoDB Lens server…')

  const env = {
    ...process.env,
    CONFIG_MONGO_URI: mongoUri,
    CONFIG_LOG_LEVEL: isDebugging ? 'verbose' : 'info',
  }

  if (testConfig.disableTokens) {
    env.CONFIG_DISABLE_DESTRUCTIVE_OPERATION_TOKENS = 'true'
  }

  lensProcess = spawn('node', [MONGODB_LENS_PATH], {
    env: env,
    stdio: ['pipe', 'pipe', 'pipe']
  })

  setupResponseHandling()
  return waitForServerStart()
}

const setupResponseHandling = () => {
  lensProcess.stdout.on('data', data => {
    const response = data.toString().trim()
    try {
      const parsed = JSON.parse(response)
      if (parsed.id && responseHandlers.has(parsed.id)) {
        const handler = responseHandlers.get(parsed.id)
        responseHandlers.delete(parsed.id)
        handler.resolve(parsed)
      }
    } catch (e) {
      console.error(`Parse error: ${e.message}`)
      console.error(`Response was: ${response.substring(0, 200)}${response.length > 200 ? '…' : ''}`)
    }
  })

  lensProcess.stderr.on('data', data => {
    const output = data.toString().trim()
    if (isDebugging)
      console.log(`${COLORS.gray}[SERVER] ${output.split('\n').join(`\n[SERVER] `)}${COLORS.reset}`)
  })
}

const waitForServerStart = () => {
  return new Promise((resolve, reject) => {
    const handler = data => {
      if (data.toString().includes('MongoDB Lens server running.')) {
        lensProcess.stderr.removeListener('data', handler)
        console.log('MongoDB Lens server started successfully.')
        resolve()
      }
    }

    lensProcess.stderr.on('data', handler)
    setTimeout(() => reject(new Error('Server startup timed out')), testConfig.serverStartupTimeout)
  })
}

const runLensCommand = async ({ command, params = {} }) => {
  if (!lensProcess) await startLensServer()

  const requestId = nextRequestId++
  const { method, methodParams } = mapToLensMethod(command, params)

  return new Promise((resolve, reject) => {
    const request = {
      jsonrpc: '2.0',
      id: requestId,
      method: method,
      params: methodParams
    }

    responseHandlers.set(requestId, { resolve, reject })

    if (isDebugging) console.log(`Sending request #${requestId}:`, JSON.stringify(request))

    lensProcess.stdin.write(JSON.stringify(request) + '\n')

    setTimeout(() => {
      if (responseHandlers.has(requestId)) {
        responseHandlers.delete(requestId)
        reject(new Error(`Request ${method} timed out after ${testConfig.requestTimeout/1000} seconds`))
      }
    }, testConfig.requestTimeout)
  })
}

const mapToLensMethod = (command, params) => {
  let method, methodParams

  switch(command) {
    case 'mcp.resource.get':
      method = 'resources/read'
      methodParams = { uri: params.uri }
      break
    case 'mcp.tool.invoke':
      method = 'tools/call'
      methodParams = {
        name: params.name,
        arguments: params.args || {}
      }
      break
    case 'mcp.prompt.start':
      method = 'prompts/get'
      methodParams = {
        name: params.name,
        arguments: params.args || {}
      }
      break
    case 'initialize':
      method = 'initialize'
      methodParams = params
      break
    default:
      method = command
      methodParams = params
  }

  return { method, methodParams }
}

const useTestDatabase = async () => {
  await runLensCommand({
    command: 'mcp.tool.invoke',
    params: {
      name: 'use-database',
      args: {
        database: TEST_DB_NAME
      }
    }
  })
}

const handleDestructiveOperationToken = async (tokenResponse, toolName, params) => {
  if (testConfig.disableTokens) {
    return assertToolSuccess(tokenResponse, 'has been permanently deleted')
  }

  assert(tokenResponse?.result?.content[0].text.includes('Confirmation code:'), 'Confirmation message not found')
  const tokenMatch = tokenResponse.result.content[0].text.match(/Confirmation code:\s+(\d+)/)
  assert(tokenMatch && tokenMatch[1], 'Confirmation code not found in text')

  const token = tokenMatch[1]
  const completeResponse = await runLensCommand({
    command: 'mcp.tool.invoke',
    params: {
      name: toolName,
      args: {
        ...params,
        token
      }
    }
  })

  return assertToolSuccess(completeResponse, 'has been permanently deleted')
}

const assertToolSuccess = (response, successIndicator) => {
  assert(response?.result?.content, 'No content in response')
  assert(Array.isArray(response.result.content), 'Content not an array')
  assert(response.result.content.length > 0, 'Empty content array')
  assert(response.result.content[0].text.includes(successIndicator), `Success message not found: "${successIndicator}"`)
  return response
}

const assertResourceSuccess = (response, successIndicator) => {
  assert(response?.result?.contents, 'No contents in response')
  assert(Array.isArray(response.result.contents), 'Contents not an array')
  assert(response.result.contents.length > 0, 'Empty contents array')
  assert(response.result.contents[0].text.includes(successIndicator), `Success message not found: "${successIndicator}"`)
  return response
}

const assertPromptSuccess = (response, successIndicator) => {
  assert(response?.result?.messages, 'No messages in response')
  assert(Array.isArray(response.result.messages), 'Messages not an array')
  assert(response.result.messages.length > 0, 'Empty messages array')
  assert(response.result.messages[0].content.text.includes(successIndicator), `Success message not found: "${successIndicator}"`)
  return response
}

const assert = (condition, message, context = null) => {
  if (condition) return
  if (context) console.error('CONTEXT:', JSON.stringify(context, null, 2))
  throw new Error(message || 'Assertion failed')
}

const testConnectMongodbTool = async () => {
  const response = await runLensCommand({
    command: 'mcp.tool.invoke',
    params: {
      name: 'connect-mongodb',
      args: {
        uri: mongoUri,
        validateConnection: 'true'
      }
    }
  })

  assertToolSuccess(response, 'Successfully connected')
}

const testConnectOriginalTool = async () => {
  await runLensCommand({
    command: 'mcp.tool.invoke',
    params: {
      name: 'connect-mongodb',
      args: {
        uri: mongoUri,
        validateConnection: 'true'
      }
    }
  })

  const response = await runLensCommand({
    command: 'mcp.tool.invoke',
    params: {
      name: 'connect-original',
      args: {
        validateConnection: 'true'
      }
    }
  })

  assertToolSuccess(response, 'Successfully connected')
}

const testAddConnectionAliasTool = async () => {
  const aliasName = 'test_alias'
  const response = await runLensCommand({
    command: 'mcp.tool.invoke',
    params: {
      name: 'add-connection-alias',
      args: {
        alias: aliasName,
        uri: mongoUri
      }
    }
  })

  assertToolSuccess(response, `Successfully added connection alias '${aliasName}'`)
}

const testListConnectionsTool = async () => {
  const aliasName = 'test_alias_for_list'

  await runLensCommand({
    command: 'mcp.tool.invoke',
    params: {
      name: 'add-connection-alias',
      args: {
        alias: aliasName,
        uri: mongoUri
      }
    }
  })

  const response = await runLensCommand({
    command: 'mcp.tool.invoke',
    params: {
      name: 'list-connections'
    }
  })

  assertToolSuccess(response, aliasName)
}

const testListDatabasesTool = async () => {
  const response = await runLensCommand({
    command: 'mcp.tool.invoke',
    params: {
      name: 'list-databases'
    }
  })

  assertToolSuccess(response, TEST_DB_NAME)
}

const testCurrentDatabaseTool = async () => {
  await useTestDatabase()

  const response = await runLensCommand({
    command: 'mcp.tool.invoke',
    params: {
      name: 'current-database'
    }
  })

  assertToolSuccess(response, `Current database: ${TEST_DB_NAME}`)
}

const testCreateDatabaseTool = async () => {
  const testDbName = `test_create_db_${Date.now()}`

  const response = await runLensCommand({
    command: 'mcp.tool.invoke',
    params: {
      name: 'create-database',
      args: {
        name: testDbName,
        switch: 'true',
        validateName: 'true'
      }
    }
  })

  assertToolSuccess(response, `Database '${testDbName}' created`)

  const dbs = await directMongoClient.db('admin').admin().listDatabases()
  const dbExists = dbs.databases.some(db => db.name === testDbName)
  assert(dbExists, `Created database '${testDbName}' not found in database list`)

  await directMongoClient.db(testDbName).dropDatabase()
}

const testUseDatabaseTool = async () => {
  const response = await runLensCommand({
    command: 'mcp.tool.invoke',
    params: {
      name: 'use-database',
      args: {
        database: TEST_DB_NAME
      }
    }
  })

  assertToolSuccess(response, `Switched to database: ${TEST_DB_NAME}`)
}

const testDropDatabaseTool = async () => {
  const testDbToDrop = `test_drop_db_${Date.now()}`

  await directMongoClient.db(testDbToDrop).collection('test').insertOne({ test: 1 })

  await runLensCommand({
    command: 'mcp.tool.invoke',
    params: {
      name: 'use-database',
      args: {
        database: testDbToDrop
      }
    }
  })

  const tokenResponse = await runLensCommand({
    command: 'mcp.tool.invoke',
    params: {
      name: 'drop-database',
      args: {
        name: testDbToDrop
      }
    }
  })

  await handleDestructiveOperationToken(tokenResponse, 'drop-database', { name: testDbToDrop })

  const dbs = await directMongoClient.db('admin').admin().listDatabases()
  const dbExists = dbs.databases.some(db => db.name === testDbToDrop)
  assert(!dbExists, `Dropped database '${testDbToDrop}' still exists`)
}

const testCreateUserTool = async () => {
  const username = `test_user_${Date.now()}`

  await useTestDatabase()

  const response = await runLensCommand({
    command: 'mcp.tool.invoke',
    params: {
      name: 'create-user',
      args: {
        username: username,
        password: 'test_password',
        roles: JSON.stringify([{ role: 'read', db: TEST_DB_NAME }])
      }
    }
  })

  assertToolSuccess(response, `User '${username}' created`)

  const usersInfo = await testDb.command({ usersInfo: username })
  assert(usersInfo.users.length > 0, `Created user '${username}' not found`)
}

const testDropUserTool = async () => {
  const username = `test_drop_user_${Date.now()}`

  await useTestDatabase()

  await runLensCommand({
    command: 'mcp.tool.invoke',
    params: {
      name: 'create-user',
      args: {
        username: username,
        password: 'test_password',
        roles: JSON.stringify([{ role: 'read', db: TEST_DB_NAME }])
      }
    }
  })

  const tokenResponse = await runLensCommand({
    command: 'mcp.tool.invoke',
    params: {
      name: 'drop-user',
      args: {
        username: username
      }
    }
  })

  let dropped = false

  if (testConfig.disableTokens) {
    assertToolSuccess(tokenResponse, 'dropped successfully')
    dropped = true
  } else {
    assert(tokenResponse?.result?.content[0].text.includes('Confirmation code:'), 'Confirmation message not found')

    const tokenMatch = tokenResponse.result.content[0].text.match(/Confirmation code:\s+(\d+)/)
    assert(tokenMatch && tokenMatch[1], 'Confirmation code not found in text')

    const token = tokenMatch[1]

    const dropResponse = await runLensCommand({
      command: 'mcp.tool.invoke',
      params: {
        name: 'drop-user',
        args: {
          username: username,
          token
        }
      }
    })

    assertToolSuccess(dropResponse, 'dropped successfully')
    dropped = true
  }

  if (dropped) {
    const usersInfo = await testDb.command({ usersInfo: username })
    assert(usersInfo.users.length === 0, `Dropped user '${username}' still exists`)
  }
}

const testListCollectionsTool = async () => {
  await useTestDatabase()

  const response = await runLensCommand({
    command: 'mcp.tool.invoke',
    params: {
      name: 'list-collections'
    }
  })

  assertToolSuccess(response, TEST_COLLECTION_NAME)
}

const testCreateCollectionTool = async () => {
  const collectionName = `test_create_coll_${Date.now()}`

  await useTestDatabase()

  const response = await runLensCommand({
    command: 'mcp.tool.invoke',
    params: {
      name: 'create-collection',
      args: {
        name: collectionName,
        options: JSON.stringify({})
      }
    }
  })

  assertToolSuccess(response, `Collection '${collectionName}' created`)

  const collections = await testDb.listCollections().toArray()
  const collExists = collections.some(coll => coll.name === collectionName)
  assert(collExists, `Created collection '${collectionName}' not found`)
}

const testDropCollectionTool = async () => {
  const collectionName = `test_drop_coll_${Date.now()}`

  await useTestDatabase()

  await runLensCommand({
    command: 'mcp.tool.invoke',
    params: {
      name: 'create-collection',
      args: {
        name: collectionName,
        options: JSON.stringify({})
      }
    }
  })

  const tokenResponse = await runLensCommand({
    command: 'mcp.tool.invoke',
    params: {
      name: 'drop-collection',
      args: {
        name: collectionName
      }
    }
  })

  await handleDestructiveOperationToken(tokenResponse, 'drop-collection', { name: collectionName })

  const collections = await testDb.listCollections().toArray()
  const collExists = collections.some(coll => coll.name === collectionName)
  assert(!collExists, `Dropped collection '${collectionName}' still exists`)
}

const testRenameCollectionTool = async () => {
  const oldName = `test_rename_old_${Date.now()}`
  const newName = `test_rename_new_${Date.now()}`

  await useTestDatabase()

  await runLensCommand({
    command: 'mcp.tool.invoke',
    params: {
      name: 'create-collection',
      args: {
        name: oldName,
        options: JSON.stringify({})
      }
    }
  })

  const response = await runLensCommand({
    command: 'mcp.tool.invoke',
    params: {
      name: 'rename-collection',
      args: {
        oldName: oldName,
        newName: newName,
        dropTarget: 'false'
      }
    }
  })

  assertToolSuccess(response, `renamed to '${newName}'`)

  const collections = await testDb.listCollections().toArray()
  const oldExists = collections.some(coll => coll.name === oldName)
  const newExists = collections.some(coll => coll.name === newName)
  assert(!oldExists, `Old collection '${oldName}' still exists`)
  assert(newExists, `New collection '${newName}' not found`)
}

const testValidateCollectionTool = async () => {
  await useTestDatabase()

  const response = await runLensCommand({
    command: 'mcp.tool.invoke',
    params: {
      name: 'validate-collection',
      args: {
        collection: TEST_COLLECTION_NAME,
        full: 'false'
      }
    }
  })

  assertToolSuccess(response, 'Validation Results')

  const content = response.result.content[0].text
  assert(content.includes('Collection'), 'Collection name not found')
  assert(content.includes('Valid:') || content.includes('Records Validated:'), 'Validation results not found')
}

const testDistinctValuesTool = async () => {
  await useTestDatabase()

  const response = await runLensCommand({
    command: 'mcp.tool.invoke',
    params: {
      name: 'distinct-values',
      args: {
        collection: TEST_COLLECTION_NAME,
        field: 'tags',
        filter: '{}'
      }
    }
  })

  assertToolSuccess(response, 'Distinct values for field')

  const content = response.result.content[0].text
  assert(content.includes('tag') || content.includes('category'), 'Expected distinct tag values not found')
}

const testFindDocumentsTool = async () => {
  await useTestDatabase()

  const response = await runLensCommand({
    command: 'mcp.tool.invoke',
    params: {
      name: 'find-documents',
      args: {
        collection: TEST_COLLECTION_NAME,
        filter: '{"value": {"$lt": 10}}',
        limit: 5
      }
    }
  })

  const content = response.result.content[0].text
  assert(content.includes('"value":'), 'Value field not found')
  assert(content.includes('"name":'), 'Name field not found')
}

const testCountDocumentsTool = async () => {
  await useTestDatabase()

  const response = await runLensCommand({
    command: 'mcp.tool.invoke',
    params: {
      name: 'count-documents',
      args: {
        collection: TEST_COLLECTION_NAME,
        filter: '{"isActive": true}'
      }
    }
  })

  assertToolSuccess(response, 'Count:')
  assert(/Count: \d+ document/.test(response.result.content[0].text), 'Count number not found')
}

const testInsertDocumentTool = async () => {
  await useTestDatabase()

  const testDoc = {
    name: 'Test Insert',
    value: 999,
    tags: ['test', 'insert'],
    isActive: true,
    createdAt: new Date().toISOString()
  }

  const response = await runLensCommand({
    command: 'mcp.tool.invoke',
    params: {
      name: 'insert-document',
      args: {
        collection: TEST_COLLECTION_NAME,
        document: JSON.stringify(testDoc)
      }
    }
  })

  assertToolSuccess(response, 'inserted successfully')

  const countResponse = await runLensCommand({
    command: 'mcp.tool.invoke',
    params: {
      name: 'count-documents',
      args: {
        collection: TEST_COLLECTION_NAME,
        filter: '{"value": 999}'
      }
    }
  })

  assert(countResponse.result.content[0].text.includes('Count: 1'), 'Inserted document not found')
}

const testUpdateDocumentTool = async () => {
  await useTestDatabase()

  const testDoc = {
    name: 'Update Test',
    value: 888,
    tags: ['update'],
    isActive: true
  }

  await runLensCommand({
    command: 'mcp.tool.invoke',
    params: {
      name: 'insert-document',
      args: {
        collection: TEST_COLLECTION_NAME,
        document: JSON.stringify(testDoc)
      }
    }
  })

  const response = await runLensCommand({
    command: 'mcp.tool.invoke',
    params: {
      name: 'update-document',
      args: {
        collection: TEST_COLLECTION_NAME,
        filter: '{"value": 888}',
        update: '{"$set": {"name": "Updated Test", "tags": ["updated"]}}'
      }
    }
  })

  assertToolSuccess(response, 'Matched:')
  assert(response.result.content[0].text.includes('Modified:'), 'Modified count not found')

  const findResponse = await runLensCommand({
    command: 'mcp.tool.invoke',
    params: {
      name: 'find-documents',
      args: {
        collection: TEST_COLLECTION_NAME,
        filter: '{"value": 888}'
      }
    }
  })

  const responseText = findResponse.result.content[0].text
  assert(responseText.includes('"name"') && responseText.includes('Updated Test'), 'Updated name not found')
  assert(responseText.includes('"tags"') && responseText.includes('updated'), 'Updated tags not found')
}

const testDeleteDocumentTool = async () => {
  await useTestDatabase()

  const testDoc = {
    name: 'Delete Test',
    value: 777,
    tags: ['delete'],
    isActive: true
  }

  await runLensCommand({
    command: 'mcp.tool.invoke',
    params: {
      name: 'insert-document',
      args: {
        collection: TEST_COLLECTION_NAME,
        document: JSON.stringify(testDoc)
      }
    }
  })

  const countBeforeResponse = await runLensCommand({
    command: 'mcp.tool.invoke',
    params: {
      name: 'count-documents',
      args: {
        collection: TEST_COLLECTION_NAME,
        filter: '{"value": 777}'
      }
    }
  })

  assert(countBeforeResponse.result.content[0].text.includes('Count: 1'), 'Test document not found before delete')

  const tokenResponse = await runLensCommand({
    command: 'mcp.tool.invoke',
    params: {
      name: 'delete-document',
      args: {
        collection: TEST_COLLECTION_NAME,
        filter: '{"value": 777}',
        many: 'false'
      }
    }
  })

  if (testConfig.disableTokens) {
    assertToolSuccess(tokenResponse, 'Successfully deleted')
  } else {
    assert(tokenResponse?.result?.content[0].text.includes('Confirmation code:'), 'Confirmation message not found')

    const tokenMatch = tokenResponse.result.content[0].text.match(/Confirmation code:\s+(\d+)/)
    assert(tokenMatch && tokenMatch[1], 'Confirmation code not found in text')

    const token = tokenMatch[1]

    const deleteResponse = await runLensCommand({
      command: 'mcp.tool.invoke',
      params: {
        name: 'delete-document',
        args: {
          collection: TEST_COLLECTION_NAME,
          filter: '{"value": 777}',
          many: 'false',
          token
        }
      }
    })

    assertToolSuccess(deleteResponse, 'Successfully deleted')
  }

  const countResponse = await runLensCommand({
    command: 'mcp.tool.invoke',
    params: {
      name: 'count-documents',
      args: {
        collection: TEST_COLLECTION_NAME,
        filter: '{"value": 777}'
      }
    }
  })

  assert(countResponse.result.content[0].text.includes('Count: 0'), 'Document still exists after deletion')
}

const testAggregateDataTool = async () => {
  await useTestDatabase()

  const pipeline = [
    { $match: { isActive: true } },
    { $group: { _id: null, avgValue: { $avg: '$value' }, count: { $sum: 1 } } }
  ]

  const response = await runLensCommand({
    command: 'mcp.tool.invoke',
    params: {
      name: 'aggregate-data',
      args: {
        collection: TEST_COLLECTION_NAME,
        pipeline: JSON.stringify(pipeline)
      }
    }
  })

  const content = response.result.content[0].text
  assert(content.includes('"avgValue":'), 'Average value not found')
  assert(content.includes('"count":'), 'Count not found')
}

const testCreateIndexTool = async () => {
  await useTestDatabase()

  const indexName = `test_index_${Date.now()}`

  const response = await runLensCommand({
    command: 'mcp.tool.invoke',
    params: {
      name: 'create-index',
      args: {
        collection: TEST_COLLECTION_NAME,
        keys: '{"createdAt": 1}',
        options: JSON.stringify({ name: indexName })
      }
    }
  })

  assertToolSuccess(response, 'Index created')
  assert(response.result.content[0].text.includes(indexName), 'Index name not found')
}

const testDropIndexTool = async () => {
  await useTestDatabase()

  const indexName = `test_drop_index_${Date.now()}`

  await runLensCommand({
    command: 'mcp.tool.invoke',
    params: {
      name: 'create-index',
      args: {
        collection: TEST_COLLECTION_NAME,
        keys: '{"testField": 1}',
        options: JSON.stringify({ name: indexName })
      }
    }
  })

  const tokenResponse = await runLensCommand({
    command: 'mcp.tool.invoke',
    params: {
      name: 'drop-index',
      args: {
        collection: TEST_COLLECTION_NAME,
        indexName: indexName
      }
    }
  })

  let dropped = false

  if (testConfig.disableTokens) {
    assertToolSuccess(tokenResponse, 'dropped from collection')
    dropped = true
  } else {
    assert(tokenResponse?.result?.content[0].text.includes('Confirmation code:'), 'Confirmation message not found')

    const tokenMatch = tokenResponse.result.content[0].text.match(/Confirmation code:\s+(\d+)/)
    assert(tokenMatch && tokenMatch[1], 'Confirmation code not found in text')

    const token = tokenMatch[1]

    const dropResponse = await runLensCommand({
      command: 'mcp.tool.invoke',
      params: {
        name: 'drop-index',
        args: {
          collection: TEST_COLLECTION_NAME,
          indexName: indexName,
          token
        }
      }
    })

    assertToolSuccess(dropResponse, 'dropped from collection')
    dropped = true
  }

  if (dropped) {
    const indexes = await testDb.collection(TEST_COLLECTION_NAME).indexes()
    const indexStillExists = indexes.some(idx => idx.name === indexName)
    assert(!indexStillExists, `Dropped index '${indexName}' still exists`)
  }
}

const testAnalyzeSchemaTool = async () => {
  await useTestDatabase()

  const response = await runLensCommand({
    command: 'mcp.tool.invoke',
    params: {
      name: 'analyze-schema',
      args: {
        collection: TEST_COLLECTION_NAME,
        sampleSize: 20
      }
    }
  })

  assertToolSuccess(response, 'Schema for')
  const content = response.result.content[0].text
  assert(content.includes('name:'), 'Name field not found')
  assert(content.includes('value:'), 'Value field not found')
  assert(content.includes('tags:'), 'Tags field not found')
}

const testGenerateSchemaValidatorTool = async () => {
  await useTestDatabase()

  const response = await runLensCommand({
    command: 'mcp.tool.invoke',
    params: {
      name: 'generate-schema-validator',
      args: {
        collection: TEST_COLLECTION_NAME,
        strictness: 'moderate'
      }
    }
  })

  assertToolSuccess(response, 'MongoDB JSON Schema Validator')
  const content = response.result.content[0].text
  assert(content.includes('$jsonSchema'), 'JSON Schema not found')
  assert(content.includes('properties'), 'Properties not found')
}

const testCompareSchemasTool = async () => {
  await useTestDatabase()

  const response = await runLensCommand({
    command: 'mcp.tool.invoke',
    params: {
      name: 'compare-schemas',
      args: {
        sourceCollection: TEST_COLLECTION_NAME,
        targetCollection: ANOTHER_TEST_COLLECTION,
        sampleSize: 10
      }
    }
  })

  assertToolSuccess(response, 'Schema Comparison')
  const content = response.result.content[0].text
  assert(content.includes('Source Collection'), 'Source info not found')
  assert(content.includes('Target Collection'), 'Target info not found')
}

const testExplainQueryTool = async () => {
  await useTestDatabase()

  const response = await runLensCommand({
    command: 'mcp.tool.invoke',
    params: {
      name: 'explain-query',
      args: {
        collection: TEST_COLLECTION_NAME,
        filter: '{"value": {"$gt": 10}}',
        verbosity: 'queryPlanner'
      }
    }
  })

  assertToolSuccess(response, 'Query Explanation')
  assert(response.result.content[0].text.includes('Query Planner'), 'Query planner not found')
}

const testAnalyzeQueryPatternsTool = async () => {
  await useTestDatabase()

  const response = await runLensCommand({
    command: 'mcp.tool.invoke',
    params: {
      name: 'analyze-query-patterns',
      args: {
        collection: TEST_COLLECTION_NAME,
        duration: 1
      }
    }
  })

  assertToolSuccess(response, 'Query Pattern Analysis')

  const content = response.result.content[0].text
  assert(
    content.includes('Analysis') ||
    content.includes('Recommendations') ||
    content.includes('Queries') ||
    content.includes('Patterns'),
    'No analysis content found in response'
  )
}

const testGetStatsTool = async () => {
  await useTestDatabase()

  const dbResponse = await runLensCommand({
    command: 'mcp.tool.invoke',
    params: {
      name: 'get-stats',
      args: {
        target: 'database'
      }
    }
  })

  assertToolSuccess(dbResponse, 'Statistics')

  const collResponse = await runLensCommand({
    command: 'mcp.tool.invoke',
    params: {
      name: 'get-stats',
      args: {
        target: 'collection',
        name: TEST_COLLECTION_NAME
      }
    }
  })

  assertToolSuccess(collResponse, 'Statistics')
  assert(collResponse.result.content[0].text.includes('Document Count'), 'Document count not found')
}

const testBulkOperationsTool = async () => {
  await useTestDatabase()

  const operations = [
    { insertOne: { document: { name: 'Bulk Insert 1', value: 1001 } } },
    { insertOne: { document: { name: 'Bulk Insert 2', value: 1002 } } },
    { updateOne: { filter: { name: 'Bulk Insert 1' }, update: { $set: { updated: true } } } }
  ]

  const response = await runLensCommand({
    command: 'mcp.tool.invoke',
    params: {
      name: 'bulk-operations',
      args: {
        collection: TEST_COLLECTION_NAME,
        operations: JSON.stringify(operations),
        ordered: 'true'
      }
    }
  })

  assertToolSuccess(response, 'Bulk Operations Results')
  assert(response.result.content[0].text.includes('Inserted:'), 'Insert count not found')
}

const testCreateTimeseriesCollectionTool = async () => {
  try {
    const adminDb = directMongoClient.db('admin')
    const serverInfo = await adminDb.command({ buildInfo: 1 })
    const version = serverInfo.version.split('.').map(Number)

    if (version[0] < 5) {
      return skipTest('create-timeseries Tool', 'MongoDB version 5.0+ required for time series collections')
    }
  } catch (e) {
    return skipTest('create-timeseries Tool', `Could not determine MongoDB version: ${e.message}`)
  }

  await useTestDatabase()

  const collectionName = `timeseries_${Date.now()}`

  const response = await runLensCommand({
    command: 'mcp.tool.invoke',
    params: {
      name: 'create-timeseries',
      args: {
        name: collectionName,
        timeField: 'timestamp',
        metaField: 'metadata',
        granularity: 'seconds'
      }
    }
  })

  assertToolSuccess(response, 'Time series collection')
  assert(response.result.content[0].text.includes('created'), 'Created confirmation not found')

  const collections = await testDb.listCollections({name: collectionName}).toArray()
  assert(collections.length > 0, `Timeseries collection '${collectionName}' not found`)
  assert(collections[0].options?.timeseries, 'Collection is not configured as timeseries')
}

const testCollationQueryTool = async () => {
  await useTestDatabase()

  const collationTestDocs = [
    { name: 'café', language: 'French', rank: 1 },
    { name: 'cafe', language: 'English', rank: 2 },
    { name: 'CAFE', language: 'English', rank: 3 }
  ]

  await testDb.collection(TEST_COLLECTION_NAME).insertMany(collationTestDocs)

  console.log(`${COLORS.blue}Verifying collation test documents were inserted…${COLORS.reset}`)
  const testDocs = await testDb.collection(TEST_COLLECTION_NAME)
    .find({ name: { $in: ['café', 'cafe', 'CAFE'] } })
    .toArray()
  console.log(`${COLORS.blue}Found ${testDocs.length} collation test documents${COLORS.reset}`)

  const response = await runLensCommand({
    command: 'mcp.tool.invoke',
    params: {
      name: 'collation-query',
      args: {
        collection: TEST_COLLECTION_NAME,
        filter: '{"name": "cafe"}',
        locale: 'en',
        strength: 1,
        caseLevel: 'false'
      }
    }
  })

  const responseText = response.result.content[0].text
  assert(
    responseText.includes('café') ||
    responseText.includes('cafe') ||
    responseText.includes('CAFE') ||
    responseText.includes('collation') ||
    responseText.includes('Locale') ||
    responseText.includes('Found'),
    'Collation results not found'
  )
}

const testTextSearchTool = async () => {
  await useTestDatabase()

  const response = await runLensCommand({
    command: 'mcp.tool.invoke',
    params: {
      name: 'text-search',
      args: {
        collection: TEST_COLLECTION_NAME,
        searchText: 'test',
        limit: 5
      }
    }
  })

  const responseText = response.result.content[0].text
  assert(
    responseText.includes('Found') ||
    responseText.includes('No text index found'),
    'Text search results or index message not found'
  )
}

const testGeoQueryTool = async () => {
  await useTestDatabase()

  const geometry = {
    type: 'Point',
    coordinates: [-74, 40.7]
  }

  const response = await runLensCommand({
    command: 'mcp.tool.invoke',
    params: {
      name: 'geo-query',
      args: {
        collection: TEST_COLLECTION_NAME,
        operator: 'near',
        field: 'location',
        geometry: JSON.stringify(geometry),
        maxDistance: 2000000,
        limit: 10
      }
    }
  })

  assert(response?.result?.content, 'No content in response')
  assert(Array.isArray(response.result.content), 'Content not an array')
  assert(response.result.content.length > 0, 'Empty content array')

  const responseText = response.result.content[0].text
  assert(
    responseText.includes('coordinates') ||
    responseText.includes('location') ||
    responseText.includes('Geo Test'),
    'Geospatial data not found in results'
  )
}

const testTransactionTool = async () => {
  if (!isReplSet) {
    return skipTest('transaction Tool', 'MongoDB not in replica set mode - transactions require replica set')
  }

  await useTestDatabase()

  const operations = [
    {
      operation: 'insert',
      collection: TEST_COLLECTION_NAME,
      document: { name: 'Transaction Test 1', value: 1 }
    },
    {
      operation: 'insert',
      collection: TEST_COLLECTION_NAME,
      document: { name: 'Transaction Test 2', value: 2 }
    },
    {
      operation: 'update',
      collection: TEST_COLLECTION_NAME,
      filter: { name: 'Transaction Test 1' },
      update: { $set: { updated: true } }
    }
  ]

  const response = await runLensCommand({
    command: 'mcp.tool.invoke',
    params: {
      name: 'transaction',
      args: {
        operations: JSON.stringify(operations)
      }
    }
  })

  assert(response?.result?.content, 'No content in response')
  assert(Array.isArray(response.result.content), 'Content not an array')
  assert(response.result.content.length > 0, 'Empty content array')

  const responseText = response.result.content[0].text
  assert(
    responseText.includes('Transaction') &&
    (responseText.includes('Step') || responseText.includes('committed')),
    'Transaction results not found'
  )
}

const testWatchChangesTool = async () => {
  if (!isReplSet) {
    return skipTest('watch-changes Tool', 'MongoDB not in replica set mode - change streams require replica set')
  }

  await useTestDatabase()

  const response = await runLensCommand({
    command: 'mcp.tool.invoke',
    params: {
      name: 'watch-changes',
      args: {
        collection: TEST_COLLECTION_NAME,
        operations: JSON.stringify(['insert', 'update', 'delete']),
        duration: 2,
        fullDocument: 'false'
      }
    }
  })

  assert(response?.result?.content, 'No content in response')
  assert(Array.isArray(response.result.content), 'Content not an array')
  assert(response.result.content.length > 0, 'Empty content array')

  const responseText = response.result.content[0].text
  assert(
    responseText.includes('changes detected') ||
    responseText.includes('No changes detected'),
    'Change stream results not found'
  )
}

const testGridFSOperationTool = async () => {
  await useTestDatabase()

  try {
    const bucket = new mongodb.GridFSBucket(testDb)
    const fileContent = Buffer.from('GridFS test file content')

    const uploadStream = bucket.openUploadStream('test-gridfs-file.txt')
    uploadStream.write(fileContent)
    uploadStream.end()

    await new Promise(resolve => uploadStream.on('finish', resolve))
  } catch (e) {
    console.error(`Failed to create GridFS test file: ${e.message}`)
  }

  const response = await runLensCommand({
    command: 'mcp.tool.invoke',
    params: {
      name: 'gridfs-operation',
      args: {
        operation: 'list',
        bucket: 'fs',
        limit: 10
      }
    }
  })

  assert(response?.result?.content, 'No content in response')
  assert(Array.isArray(response.result.content), 'Content not an array')
  assert(response.result.content.length > 0, 'Empty content array')

  const responseText = response.result.content[0].text
  assert(
    responseText.includes('GridFS') ||
    responseText.includes('Filename') ||
    responseText.includes('Size:'),
    'GridFS data not found in response'
  )
}

const testClearCacheTool = async () => {
  await useTestDatabase()

  const response = await runLensCommand({
    command: 'mcp.tool.invoke',
    params: {
      name: 'clear-cache',
      args: {
        target: 'all'
      }
    }
  })

  assertToolSuccess(response, 'cleared')
}

const testShardStatusTool = async () => {
  if (!isSharded) {
    return skipTest('shard-status Tool', 'MongoDB not in sharded cluster mode - sharding features require sharded deployment')
  }

  await useTestDatabase()

  const response = await runLensCommand({
    command: 'mcp.tool.invoke',
    params: {
      name: 'shard-status',
      args: {
        target: 'database'
      }
    }
  })

  assert(response?.result?.content, 'No content in response')
  assert(Array.isArray(response.result.content), 'Content not an array')
  assert(response.result.content.length > 0, 'Empty content array')

  const responseText = response.result.content[0].text
  assert(
    responseText.includes('Sharding Status') ||
    responseText.includes('sharded cluster') ||
    responseText.includes('Sharding is not enabled') ||
    responseText.includes('not a sharded cluster') ||
    responseText.includes('not running with sharding'),
    'Sharding status or message not found'
  )
}

const testExportDataTool = async () => {
  await useTestDatabase()

  const jsonResponse = await runLensCommand({
    command: 'mcp.tool.invoke',
    params: {
      name: 'export-data',
      args: {
        collection: TEST_COLLECTION_NAME,
        filter: '{"value": {"$lt": 10}}',
        format: 'json',
        limit: 5
      }
    }
  })

  assert(jsonResponse?.result?.content, 'No content in JSON export response')
  assert(Array.isArray(jsonResponse.result.content), 'JSON export content not an array')
  assert(jsonResponse.result.content.length > 0, 'Empty JSON export content array')

  console.log(`${COLORS.blue}JSON export response first 100 chars: ${jsonResponse.result.content[0].text.substring(0, 100)}${COLORS.reset}`)

  const jsonText = jsonResponse.result.content[0].text
  assert(
    jsonText.includes('{') && jsonText.includes('}'),
    'JSON content not found in export'
  )

  const csvResponse = await runLensCommand({
    command: 'mcp.tool.invoke',
    params: {
      name: 'export-data',
      args: {
        collection: TEST_COLLECTION_NAME,
        filter: '{"value": {"$lt": 10}}',
        format: 'csv',
        fields: 'name,value,isActive',
        limit: 5
      }
    }
  })

  assert(csvResponse?.result?.content, 'No content in CSV export response')
  assert(Array.isArray(csvResponse.result.content), 'CSV export content not an array')
  assert(csvResponse.result.content.length > 0, 'Empty CSV export content array')

  console.log(`${COLORS.blue}CSV export response first 100 chars: ${csvResponse.result.content[0].text.substring(0, 100)}${COLORS.reset}`)

  assert(csvResponse.result.content[0].text.includes('name') &&
         csvResponse.result.content[0].text.includes('value'),
         'CSV headers not found')
}

const testDatabasesResource = async () => {
  const response = await runLensCommand({
    command: 'mcp.resource.get',
    params: { uri: 'mongodb://databases' }
  })

  assertResourceSuccess(response, 'Databases')
  assert(response.result.contents[0].text.includes(TEST_DB_NAME), 'Test database not found')
}

const testCollectionsResource = async () => {
  await useTestDatabase()

  const response = await runLensCommand({
    command: 'mcp.resource.get',
    params: { uri: 'mongodb://collections' }
  })

  assertResourceSuccess(response, `Collections in ${TEST_DB_NAME}`)
  assert(response.result.contents[0].text.includes(TEST_COLLECTION_NAME), 'Test collection not found')
}

const testDatabaseUsersResource = async () => {
  const response = await runLensCommand({
    command: 'mcp.resource.get',
    params: { uri: 'mongodb://database/users' }
  })

  assert(response?.result?.contents, 'No contents in response')
  assert(Array.isArray(response.result.contents), 'Contents not an array')
  assert(response.result.contents.length > 0, 'Empty contents array')
  assert(
    response.result.contents[0].text.includes('Users in database') ||
    response.result.contents[0].text.includes('Could not retrieve user information'),
    'Users title or permission message not found'
  )
}

const testDatabaseTriggersResource = async () => {
  const response = await runLensCommand({
    command: 'mcp.resource.get',
    params: { uri: 'mongodb://database/triggers' }
  })

  assert(response?.result?.contents, 'No contents in response')
  assert(Array.isArray(response.result.contents), 'Contents not an array')
  assert(response.result.contents.length > 0, 'Empty contents array')
  assert(
    response.result.contents[0].text.includes('Change Stream') ||
    response.result.contents[0].text.includes('Trigger'),
    'Triggers info not found'
  )
}

const testStoredFunctionsResource = async () => {
  const response = await runLensCommand({
    command: 'mcp.resource.get',
    params: { uri: 'mongodb://database/functions' }
  })

  assert(response?.result?.contents, 'No contents in response')
  assert(Array.isArray(response.result.contents), 'Contents not an array')
  assert(response.result.contents.length > 0, 'Empty contents array')
  assert(
    response.result.contents[0].text.includes('Stored Functions') ||
    response.result.contents[0].text.includes('No stored JavaScript functions'),
    'Stored functions title or empty message not found'
  )
}

const testCollectionSchemaResource = async () => {
  await useTestDatabase()

  const response = await runLensCommand({
    command: 'mcp.resource.get',
    params: { uri: `mongodb://collection/${TEST_COLLECTION_NAME}/schema` }
  })

  assertResourceSuccess(response, `Schema for '${TEST_COLLECTION_NAME}'`)
  const content = response.result.contents[0].text
  assert(content.includes('name:'), 'Name field not found')
  assert(content.includes('value:'), 'Value field not found')
  assert(content.includes('tags:'), 'Tags field not found')
}

const testCollectionIndexesResource = async () => {
  await useTestDatabase()

  const response = await runLensCommand({
    command: 'mcp.resource.get',
    params: { uri: `mongodb://collection/${TEST_COLLECTION_NAME}/indexes` }
  })

  assertResourceSuccess(response, 'Indexes')
  const content = response.result.contents[0].text
  assert(content.includes('name_1'), 'Name index not found')
  assert(content.includes('value_-1'), 'Value index not found')
}

const testCollectionStatsResource = async () => {
  await useTestDatabase()

  const response = await runLensCommand({
    command: 'mcp.resource.get',
    params: { uri: `mongodb://collection/${TEST_COLLECTION_NAME}/stats` }
  })

  assertResourceSuccess(response, 'Statistics')
  assert(response.result.contents[0].text.includes('Document Count'), 'Document count not found')
}

const testCollectionValidationResource = async () => {
  await useTestDatabase()

  const response = await runLensCommand({
    command: 'mcp.resource.get',
    params: { uri: `mongodb://collection/${TEST_COLLECTION_NAME}/validation` }
  })

  assert(response?.result?.contents, 'No contents in response')
  assert(Array.isArray(response.result.contents), 'Contents not an array')
  assert(response.result.contents.length > 0, 'Empty contents array')
  assert(
    response.result.contents[0].text.includes('Collection Validation Rules') ||
    response.result.contents[0].text.includes('does not have any validation rules'),
    'Validation rules title or empty message not found'
  )
}

const testServerStatusResource = async () => {
  const response = await runLensCommand({
    command: 'mcp.resource.get',
    params: { uri: 'mongodb://server/status' }
  })

  assertResourceSuccess(response, 'MongoDB Server Status')
  assert(response.result.contents[0].text.includes('Version'), 'Version info not found')
}

const testReplicaStatusResource = async () => {
  const response = await runLensCommand({
    command: 'mcp.resource.get',
    params: { uri: 'mongodb://server/replica' }
  })

  assert(response?.result?.contents, 'No contents in response')
  assert(Array.isArray(response.result.contents), 'Contents not an array')
  assert(response.result.contents.length > 0, 'Empty contents array')
  assert(
    response.result.contents[0].text.includes('Replica Set') ||
    response.result.contents[0].text.includes('not available'),
    'Replica set info or message not found'
  )
}

const testPerformanceMetricsResource = async () => {
  try {
    const originalTimeout = testConfig.requestTimeout
    testConfig.requestTimeout = 30000

    const response = await runLensCommand({
      command: 'mcp.resource.get',
      params: { uri: 'mongodb://server/metrics' }
    })

    testConfig.requestTimeout = originalTimeout

    assert(response?.result?.contents, 'No contents in response')
    assert(Array.isArray(response.result.contents), 'Contents not an array')
    assert(response.result.contents.length > 0, 'Empty contents array')
    assert(
      response.result.contents[0].text.includes('MongoDB Performance Metrics') ||
      response.result.contents[0].text.includes('Error getting performance metrics'),
      'Performance metrics title or error message not found'
    )
  } catch (error) {
    console.log(`${COLORS.yellow}Skipping performance metrics test due to complexity: ${error.message}${COLORS.reset}`)
    stats.skipped++
    stats.total--
  }
}

const testQueryBuilderPrompt = async () => {
  await useTestDatabase()

  const response = await runLensCommand({
    command: 'mcp.prompt.start',
    params: {
      name: 'query-builder',
      args: {
        collection: TEST_COLLECTION_NAME,
        condition: 'active documents with value greater than 20'
      }
    }
  })

  assertPromptSuccess(response, TEST_COLLECTION_NAME)
  assert(response.result.messages[0].content.text.includes('active documents with value greater than 20'), 'Condition not found')
}

const testAggregationBuilderPrompt = async () => {
  await useTestDatabase()

  const response = await runLensCommand({
    command: 'mcp.prompt.start',
    params: {
      name: 'aggregation-builder',
      args: {
        collection: TEST_COLLECTION_NAME,
        goal: 'calculate average value by active status'
      }
    }
  })

  assertPromptSuccess(response, TEST_COLLECTION_NAME)
  assert(response.result.messages[0].content.text.includes('calculate average value by active status'), 'Goal not found')
}

const testMongoShellPrompt = async () => {
  const response = await runLensCommand({
    command: 'mcp.prompt.start',
    params: {
      name: 'mongo-shell',
      args: {
        operation: 'find documents with specific criteria',
        details: 'I want to find all documents where the value is greater than 10'
      }
    }
  })

  assertPromptSuccess(response, 'find documents with specific criteria')
  assert(response.result.messages[0].content.text.includes('value is greater than 10'), 'Details not found')
}

const testSqlToMongodbPrompt = async () => {
  const response = await runLensCommand({
    command: 'mcp.prompt.start',
    params: {
      name: 'sql-to-mongodb',
      args: {
        sqlQuery: 'SELECT * FROM users WHERE age > 25 ORDER BY name ASC LIMIT 10',
        targetCollection: 'users'
      }
    }
  })

  assertPromptSuccess(response, 'SQL query')
  assert(response.result.messages[0].content.text.includes('SELECT * FROM users'), 'SQL statement not found')
}

const testSchemaAnalysisPrompt = async () => {
  await useTestDatabase()

  const response = await runLensCommand({
    command: 'mcp.prompt.start',
    params: {
      name: 'schema-analysis',
      args: {
        collection: TEST_COLLECTION_NAME
      }
    }
  })

  assertPromptSuccess(response, TEST_COLLECTION_NAME)
  assert(response.result.messages[0].content.text.includes('schema'), 'Schema analysis not found')
}

const testDataModelingPrompt = async () => {
  const response = await runLensCommand({
    command: 'mcp.prompt.start',
    params: {
      name: 'data-modeling',
      args: {
        useCase: 'E-commerce product catalog',
        requirements: 'Fast product lookup by category, efficient inventory tracking',
        existingData: 'Currently using SQL with products and categories tables'
      }
    }
  })

  assertPromptSuccess(response, 'E-commerce product catalog')
  assert(response.result.messages[0].content.text.includes('Fast product lookup'), 'Requirements not found')
}

const testSchemaVersioningPrompt = async () => {
  await useTestDatabase()

  const response = await runLensCommand({
    command: 'mcp.prompt.start',
    params: {
      name: 'schema-versioning',
      args: {
        collection: TEST_COLLECTION_NAME,
        currentSchema: 'Documents with name, value, tags fields',
        plannedChanges: 'Add a new status field, make tags required',
        migrationConstraints: 'Zero downtime required'
      }
    }
  })

  assertPromptSuccess(response, TEST_COLLECTION_NAME)
  assert(response.result.messages[0].content.text.includes('schema versioning'), 'Schema versioning not found')
}

const testMultiTenantDesignPrompt = async () => {
  const response = await runLensCommand({
    command: 'mcp.prompt.start',
    params: {
      name: 'multi-tenant-design',
      args: {
        tenantIsolation: 'collection',
        estimatedTenants: '50',
        sharedFeatures: 'User profiles, product catalog',
        tenantSpecificFeatures: 'Orders, custom pricing',
        scalingPriorities: 'Read-heavy, occasional bulk writes'
      }
    }
  })

  assertPromptSuccess(response, 'tenant isolation')
  assert(response.result.messages[0].content.text.includes('collection'), 'Collection isolation not found')
}

const testIndexRecommendationPrompt = async () => {
  await useTestDatabase()

  const response = await runLensCommand({
    command: 'mcp.prompt.start',
    params: {
      name: 'index-recommendation',
      args: {
        collection: TEST_COLLECTION_NAME,
        queryPattern: 'filtering by createdAt within a date range'
      }
    }
  })

  assertPromptSuccess(response, TEST_COLLECTION_NAME)
  assert(response.result.messages[0].content.text.includes('filtering by createdAt'), 'Query pattern not found')
}

const testQueryOptimizerPrompt = async () => {
  await useTestDatabase()

  const response = await runLensCommand({
    command: 'mcp.prompt.start',
    params: {
      name: 'query-optimizer',
      args: {
        collection: TEST_COLLECTION_NAME,
        query: '{"tags": "tag0", "value": {"$gt": 10}}',
        performance: 'Currently taking 500ms with 10,000 documents'
      }
    }
  })

  assertPromptSuccess(response, TEST_COLLECTION_NAME)
  assert(response.result.messages[0].content.text.includes('$gt'), 'Query operator not found')
}

const testSecurityAuditPrompt = async () => {
  const response = await runLensCommand({
    command: 'mcp.prompt.start',
    params: {
      name: 'security-audit',
      args: {}
    }
  })

  assertPromptSuccess(response, 'security audit')
}

const testBackupStrategyPrompt = async () => {
  const response = await runLensCommand({
    command: 'mcp.prompt.start',
    params: {
      name: 'backup-strategy',
      args: {
        databaseSize: '50GB',
        uptime: '99.9%',
        rpo: '1 hour',
        rto: '4 hours'
      }
    }
  })

  assertPromptSuccess(response, 'backup')
  assert(response.result.messages[0].content.text.includes('50GB'), 'Database size not found')
}

const testMigrationGuidePrompt = async () => {
  const response = await runLensCommand({
    command: 'mcp.prompt.start',
    params: {
      name: 'migration-guide',
      args: {
        sourceVersion: '4.4',
        targetVersion: '5.0',
        features: 'Time series collections, transactions, aggregation'
      }
    }
  })

  assertPromptSuccess(response, 'migration')
  const content = response.result.messages[0].content.text
  assert(content.includes('4.4'), 'Source version not found')
  assert(content.includes('5.0'), 'Target version not found')
}

const testDatabaseHealthCheckPrompt = async () => {
  try {
    await useTestDatabase()

    const testDoc = {
      name: 'Health Check Test',
      value: 100,
      tags: ['test'],
      isActive: true
    }

    await runLensCommand({
      command: 'mcp.tool.invoke',
      params: {
        name: 'insert-document',
        args: {
          collection: TEST_COLLECTION_NAME,
          document: JSON.stringify(testDoc)
        }
      }
    })

    const originalTimeout = testConfig.requestTimeout
    testConfig.requestTimeout = 45000

    console.log(`${COLORS.blue}Running database health check prompt (increased timeout to ${testConfig.requestTimeout/1000}s)${COLORS.reset}`)

    const response = await runLensCommand({
      command: 'mcp.prompt.start',
      params: {
        name: 'database-health-check',
        args: {
          includePerformance: 'false',
          includeSchema: 'true',
          includeSecurity: 'false'
        }
      }
    })

    testConfig.requestTimeout = originalTimeout

    assert(response?.result?.messages, 'No messages in response')
    assert(Array.isArray(response.result.messages), 'Messages not an array')
    assert(response.result.messages.length > 0, 'Empty messages array')

    const promptText = response.result.messages[0].content.text
    assert(
      promptText.includes('health') ||
      promptText.includes('Health') ||
      promptText.includes('assessment'),
      'Health check content not found'
    )
  } catch (error) {
    console.log(`${COLORS.yellow}Skipping database health check prompt test due to complexity: ${error.message}${COLORS.reset}`)
    stats.skipped++
    stats.total--
  }
}

const logHeader = (title, margin = 'none') => {
  if (margin.indexOf('top') !== -1) console.log('')
  console.log(`${COLORS.cyan}${DIVIDER}${COLORS.reset}`)
  console.log(`${COLORS.cyan}${title}${COLORS.reset}`)
  console.log(`${COLORS.cyan}${DIVIDER}${COLORS.reset}`)
  if (margin.indexOf('bottom') !== -1) console.log('')
}

const displayTestSummary = () => {
  logHeader('Test Summary', 'margin:top,bottom')
  console.log(`${COLORS.white}Total Tests: ${stats.total}${COLORS.reset}`)
  console.log(`${COLORS.green}Passed: ${stats.passed}${COLORS.reset}`)
  console.log(`${COLORS.red}Failed: ${stats.failed}${COLORS.reset}`)
  console.log(`${COLORS.yellow}Skipped: ${stats.skipped}${COLORS.reset}`)

  if (stats.failed > 0) {
    console.error(`${COLORS.red}Some tests failed.${COLORS.reset}`)
    process.exit(1)
  } else {
    console.log(`${COLORS.green}All tests passed!${COLORS.reset}`)
    process.exit(0)
  }
}

const cleanup = async () => {
  await cleanupTestDatabase()
  await directMongoClient.close()

  if (lensProcess) {
    lensProcess.kill('SIGKILL')
    await new Promise(resolve => lensProcess.on('exit', resolve))
  }
}

const cleanupTestDatabase = async () => {
  try {
    await testDb.dropDatabase()
    console.log(`${COLORS.yellow}Test database cleaned up.${COLORS.reset}`)
  } catch (err) {
    console.error(`${COLORS.red}Error cleaning up test database: ${err.message}${COLORS.reset}`)
  }
}

process.on('exit', () => {
  console.log('Exiting test process…')
  if (lensProcess) {
    console.log('Shutting down MongoDB Lens server…')
    lensProcess.kill()
  }
})

let testDb
let mongoUri
let testCollection
let directMongoClient
let isReplSet = false
let isSharded = false
let nextRequestId = 1
let lensProcess = null
let responseHandlers = new Map()

const testFilters = []
const groupFilters = []
const patternFilters = []
const isDebugging = process.env.DEBUG === 'true'

const DIVIDER = '-'.repeat(30)
const TEST_DB_NAME = 'mongodb_lens_test'
const TEST_COLLECTION_NAME = 'test_collection'
const ANOTHER_TEST_COLLECTION = 'another_collection'
const MONGODB_LENS_PATH = join(__dirname, 'mongodb-lens.js')

const stats = {
  total: 0,
  passed: 0,
  failed: 0,
  skipped: 0
}

const COLORS = {
  red: '\x1b[31m',
  reset: '\x1b[0m',
  blue: '\x1b[34m',
  cyan: '\x1b[36m',
  gray: '\x1b[90m',
  green: '\x1b[32m',
  white: '\x1b[37m',
  yellow: '\x1b[33m',
  magenta: '\x1b[35m',
}

const testConfig = {
  requestTimeout: 15000,
  serverStartupTimeout: 20000,
  disableTokens: process.env.CONFIG_DISABLE_DESTRUCTIVE_OPERATION_TOKENS === 'true'
}

const TEST_GROUPS = [
  {
    name: 'Connection Tools',
    tests: [
      { name: 'connect-mongodb Tool', fn: testConnectMongodbTool },
      { name: 'connect-original Tool', fn: testConnectOriginalTool },
      { name: 'add-connection-alias Tool', fn: testAddConnectionAliasTool },
      { name: 'list-connections Tool', fn: testListConnectionsTool }
    ]
  },
  {
    name: 'Database Tools',
    tests: [
      { name: 'list-databases Tool', fn: testListDatabasesTool },
      { name: 'current-database Tool', fn: testCurrentDatabaseTool },
      { name: 'create-database Tool', fn: testCreateDatabaseTool },
      { name: 'use-database Tool', fn: testUseDatabaseTool },
      { name: 'drop-database Tool', fn: testDropDatabaseTool }
    ]
  },
  {
    name: 'User Tools',
    tests: [
      { name: 'create-user Tool', fn: testCreateUserTool },
      { name: 'drop-user Tool', fn: testDropUserTool }
    ]
  },
  {
    name: 'Collection Tools',
    tests: [
      { name: 'list-collections Tool', fn: testListCollectionsTool },
      { name: 'create-collection Tool', fn: testCreateCollectionTool },
      { name: 'drop-collection Tool', fn: testDropCollectionTool },
      { name: 'rename-collection Tool', fn: testRenameCollectionTool },
      { name: 'validate-collection Tool', fn: testValidateCollectionTool }
    ]
  },
  {
    name: 'Document Tools',
    tests: [
      { name: 'distinct-values Tool', fn: testDistinctValuesTool },
      { name: 'find-documents Tool', fn: testFindDocumentsTool },
      { name: 'count-documents Tool', fn: testCountDocumentsTool },
      { name: 'insert-document Tool', fn: testInsertDocumentTool },
      { name: 'update-document Tool', fn: testUpdateDocumentTool },
      { name: 'delete-document Tool', fn: testDeleteDocumentTool }
    ]
  },
  {
    name: 'Advanced Tools',
    tests: [
      { name: 'aggregate-data Tool', fn: testAggregateDataTool },
      { name: 'create-index Tool', fn: testCreateIndexTool },
      { name: 'drop-index Tool', fn: testDropIndexTool },
      { name: 'analyze-schema Tool', fn: testAnalyzeSchemaTool },
      { name: 'generate-schema-validator Tool', fn: testGenerateSchemaValidatorTool },
      { name: 'compare-schemas Tool', fn: testCompareSchemasTool },
      { name: 'explain-query Tool', fn: testExplainQueryTool },
      { name: 'analyze-query-patterns Tool', fn: testAnalyzeQueryPatternsTool },
      { name: 'get-stats Tool', fn: testGetStatsTool },
      { name: 'bulk-operations Tool', fn: testBulkOperationsTool },
      { name: 'create-timeseries Tool', fn: testCreateTimeseriesCollectionTool },
      { name: 'collation-query Tool', fn: testCollationQueryTool },
      { name: 'text-search Tool', fn: testTextSearchTool },
      { name: 'geo-query Tool', fn: testGeoQueryTool },
      { name: 'transaction Tool', fn: testTransactionTool },
      { name: 'watch-changes Tool', fn: testWatchChangesTool },
      { name: 'gridfs-operation Tool', fn: testGridFSOperationTool },
      { name: 'clear-cache Tool', fn: testClearCacheTool },
      { name: 'shard-status Tool', fn: testShardStatusTool },
      { name: 'export-data Tool', fn: testExportDataTool }
    ]
  },
  {
    name: 'Resources',
    tests: [
      { name: 'databases Resource', fn: testDatabasesResource },
      { name: 'collections Resource', fn: testCollectionsResource },
      { name: 'database-users Resource', fn: testDatabaseUsersResource },
      { name: 'database-triggers Resource', fn: testDatabaseTriggersResource },
      { name: 'stored-functions Resource', fn: testStoredFunctionsResource },
      { name: 'collection-schema Resource', fn: testCollectionSchemaResource },
      { name: 'collection-indexes Resource', fn: testCollectionIndexesResource },
      { name: 'collection-stats Resource', fn: testCollectionStatsResource },
      { name: 'collection-validation Resource', fn: testCollectionValidationResource },
      { name: 'server-status Resource', fn: testServerStatusResource },
      { name: 'replica-status Resource', fn: testReplicaStatusResource },
      { name: 'performance-metrics Resource', fn: testPerformanceMetricsResource }
    ]
  },
  {
    name: 'Prompts',
    tests: [
      { name: 'query-builder Prompt', fn: testQueryBuilderPrompt },
      { name: 'aggregation-builder Prompt', fn: testAggregationBuilderPrompt },
      { name: 'mongo-shell Prompt', fn: testMongoShellPrompt },
      { name: 'sql-to-mongodb Prompt', fn: testSqlToMongodbPrompt },
      { name: 'schema-analysis Prompt', fn: testSchemaAnalysisPrompt },
      { name: 'data-modeling Prompt', fn: testDataModelingPrompt },
      { name: 'schema-versioning Prompt', fn: testSchemaVersioningPrompt },
      { name: 'multi-tenant-design Prompt', fn: testMultiTenantDesignPrompt },
      { name: 'index-recommendation Prompt', fn: testIndexRecommendationPrompt },
      { name: 'query-optimizer Prompt', fn: testQueryOptimizerPrompt },
      { name: 'security-audit Prompt', fn: testSecurityAuditPrompt },
      { name: 'backup-strategy Prompt', fn: testBackupStrategyPrompt },
      { name: 'migration-guide Prompt', fn: testMigrationGuidePrompt },
      { name: 'database-health-check Prompt', fn: testDatabaseHealthCheckPrompt }
    ]
  }
]

runTests().catch(err => {
  console.error(`${COLORS.red}Test runner error: ${err.message}${COLORS.reset}`)
  if (lensProcess) lensProcess.kill()
  process.exit(1)
})

```
Page 1/2FirstPrevNextLast