diff --git a/CONFIGURATION.md b/CONFIGURATION.md new file mode 100644 index 000000000..479f47f7d --- /dev/null +++ b/CONFIGURATION.md @@ -0,0 +1,442 @@ +# Configuration + +> **πŸ”’ Security Best Practice:** We strongly recommend using environment variables for sensitive configuration such as API credentials (`MDB_MCP_API_CLIENT_ID`, `MDB_MCP_API_CLIENT_SECRET`) and connection strings (`MDB_MCP_CONNECTION_STRING`) instead of command-line arguments. Environment variables are not visible in process lists and provide better security for your sensitive data. + +The MongoDB MCP Server can be configured using multiple methods, with the following precedence (highest to lowest): + +1. Command-line arguments +2. Environment variables +3. Configuration File + +## πŸ“š Table of Contents + +- [Configuration Options](#configuration-options) + - [Atlas API Access](#atlas-api-access) + - [Atlas API Permissions](#atlas-api-permissions) + - [Configuration Methods](#configuration-methods) + - [Configuration File](#configuration-file) + - [Environment Variables](#environment-variables) + - [Command-Line Arguments](#command-line-arguments) + - [Proxy Support](#proxy-support) + +## Configuration Options + +| CLI Option | Environment Variable | Default | Description | +| -------------------------------------- | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `apiClientId` | `MDB_MCP_API_CLIENT_ID` | `` | Atlas API client ID for authentication. Required for running Atlas tools. | +| `apiClientSecret` | `MDB_MCP_API_CLIENT_SECRET` | `` | Atlas API client secret for authentication. Required for running Atlas tools. | +| `atlasTemporaryDatabaseUserLifetimeMs` | `MDB_MCP_ATLAS_TEMPORARY_DATABASE_USER_LIFETIME_MS` | `14400000` | Time in milliseconds that temporary database users created when connecting to MongoDB Atlas clusters will remain active before being automatically deleted. | +| `confirmationRequiredTools` | `MDB_MCP_CONFIRMATION_REQUIRED_TOOLS` | `"atlas-create-access-list,atlas-create-db-user,drop-database,drop-collection,delete-many,drop-index"` | Comma separated values of tool names that require user confirmation before execution. Requires the client to support elicitation. | +| `connectionString` | `MDB_MCP_CONNECTION_STRING` | `` | MongoDB connection string for direct database connections. Optional, if not set, you'll need to call the connect tool before interacting with MongoDB data. | +| `disableEmbeddingsValidation` | `MDB_MCP_DISABLE_EMBEDDINGS_VALIDATION` | `false` | When set to true, disables validation of embeddings dimensions. | +| `disabledTools` | `MDB_MCP_DISABLED_TOOLS` | `""` | Comma separated values of tool names, operation types, and/or categories of tools that will be disabled. | +| `exportCleanupIntervalMs` | `MDB_MCP_EXPORT_CLEANUP_INTERVAL_MS` | `120000` | Time in milliseconds between export cleanup cycles that remove expired export files. | +| `exportTimeoutMs` | `MDB_MCP_EXPORT_TIMEOUT_MS` | `300000` | Time in milliseconds after which an export is considered expired and eligible for cleanup. | +| `exportsPath` | `MDB_MCP_EXPORTS_PATH` | see below\* | Folder to store exported data files. | +| `httpHost` | `MDB_MCP_HTTP_HOST` | `"127.0.0.1"` | Host address to bind the HTTP server to (only used when transport is 'http'). | +| `httpPort` | `MDB_MCP_HTTP_PORT` | `3000` | Port number for the HTTP server (only used when transport is 'http'). | +| `idleTimeoutMs` | `MDB_MCP_IDLE_TIMEOUT_MS` | `600000` | Idle timeout for a client to disconnect (only applies to http transport). | +| `indexCheck` | `MDB_MCP_INDEX_CHECK` | `false` | When set to true, enforces that query operations must use an index, rejecting queries that perform a collection scan. | +| `logPath` | `MDB_MCP_LOG_PATH` | see below\* | Folder to store logs. | +| `loggers` | `MDB_MCP_LOGGERS` | `"disk,mcp"` see below\* | Comma separated values of logger types. | +| `maxBytesPerQuery` | `MDB_MCP_MAX_BYTES_PER_QUERY` | `16777216` | The maximum size in bytes for results from a find or aggregate tool call. This serves as an upper bound for the responseBytesLimit parameter in those tools. | +| `maxDocumentsPerQuery` | `MDB_MCP_MAX_DOCUMENTS_PER_QUERY` | `100` | The maximum number of documents that can be returned by a find or aggregate tool call. For the find tool, the effective limit will be the smaller of this value and the tool's limit parameter. | +| `notificationTimeoutMs` | `MDB_MCP_NOTIFICATION_TIMEOUT_MS` | `540000` | Notification timeout for a client to be aware of disconnect (only applies to http transport). | +| `previewFeatures` | `MDB_MCP_PREVIEW_FEATURES` | `""` | Comma separated values of preview features that are enabled. | +| `readOnly` | `MDB_MCP_READ_ONLY` | `false` | When set to true, only allows read, connect, and metadata operation types, disabling create/update/delete operations. | +| `telemetry` | `MDB_MCP_TELEMETRY` | `"enabled"` | When set to disabled, disables telemetry collection. | +| `toolMetadataOverrides` | `MDB_MCP_TOOL_METADATA_OVERRIDES` | `"{}"` | A map of name of the MongoDB MCP server tool to the metadata that needs to be used for that tool. Example: `{ "toolMetadataOverrides": { "find": { "name": "query" } } }` | +| `transport` | `MDB_MCP_TRANSPORT` | `"stdio"` | Either 'stdio' or 'http'. | +| `voyageApiKey` | `MDB_MCP_VOYAGE_API_KEY` | `""` | API key for Voyage AI embeddings service (required for vector search operations with text-to-embedding conversion). | + +### Logger Options + +The `loggers` configuration option controls where logs are sent. You can specify one or more logger types as a comma-separated list. The available options are: + +- `mcp`: Sends logs to the MCP client (if supported by the client/transport). +- `disk`: Writes logs to disk files. Log files are stored in the log path (see `logPath` above). +- `stderr`: Outputs logs to standard error (stderr), useful for debugging or when running in containers. + +**Default:** `disk,mcp` (logs are written to disk and sent to the MCP client). + +You can combine multiple loggers, e.g. `--loggers disk stderr` or `export MDB_MCP_LOGGERS="mcp,stderr"`. + +#### Example: Set logger via environment variable + +```shell +export MDB_MCP_LOGGERS="disk,stderr" +``` + +> **πŸ’‘ Platform Note:** For Windows users, see [Environment Variables](#environment-variables) for platform-specific instructions. + +#### Example: Set logger via command-line argument + +```shell +npx -y mongodb-mcp-server@latest --loggers mcp stderr +``` + +#### Log File Location + +When using the `disk` logger, log files are stored in: + +- **Windows:** `%LOCALAPPDATA%\mongodb\mongodb-mcp\.app-logs` +- **macOS/Linux:** `~/.mongodb/mongodb-mcp/.app-logs` + +You can override the log directory with the `logPath` option. + +> **πŸ”’ Security Note:** When configuring a custom `logPath`, ensure the directory is owned and writable only by the user running the MongoDB MCP server process. On Linux/macOS, use `chmod 700` and verify ownership with `chown`. On Windows, restrict write permissions to the service account only. + +### Disabled Tools + +You can disable specific tools or categories of tools by using the `disabledTools` option. This option accepts an array of strings, +where each string can be a tool name, operation type, or category. + +The way the array is constructed depends on the type of configuration method you use: + +- For **environment variable** configuration, use a comma-separated string: `export MDB_MCP_DISABLED_TOOLS="create,update,delete,atlas,collectionSchema"`. +- For **command-line argument** configuration, use a space-separated string: `--disabledTools create update delete atlas collectionSchema`. + +Categories of tools: + +- `atlas` - MongoDB Atlas tools, such as list clusters, create cluster, etc. +- `mongodb` - MongoDB database tools, such as find, aggregate, etc. + +Operation types: + +- `create` - Tools that create resources, such as create cluster, insert document, etc. +- `update` - Tools that update resources, such as update document, rename collection, etc. +- `delete` - Tools that delete resources, such as delete document, drop collection, etc. +- `read` - Tools that read resources, such as find, aggregate, list clusters, etc. +- `metadata` - Tools that read metadata, such as list databases/collections/indexes, infer collection schema, etc. +- `connect` - Tools that allow you to connect or switch the connection to a MongoDB instance. If this is disabled, you will need to provide a connection string through the config when starting the server. + +### Require Confirmation + +If your client supports [elicitation](https://modelcontextprotocol.io/specification/draft/client/elicitation), you can set the MongoDB MCP server to request user confirmation before executing certain tools. + +When a tool is marked as requiring confirmation, the server will send an elicitation request to the client. The client with elicitation support will then prompt the user for confirmation and send the response back to the server. If the client does not support elicitation, the tool will execute without confirmation. + +You can set the `confirmationRequiredTools` configuration option to specify the names of tools which require confirmation. By default, the following tools have this setting enabled: `drop-database`, `drop-collection`, `delete-many`, `atlas-create-db-user`, `atlas-create-access-list`. + +### Read-Only Mode + +The `readOnly` configuration option allows you to restrict the MCP server to only use tools with "read", "connect", and "metadata" operation types. When enabled, all tools that have "create", "update" or "delete" operation types will not be registered with the server. + +This is useful for scenarios where you want to provide access to MongoDB data for analysis without allowing any modifications to the data or infrastructure. + +You can enable read-only mode using: + +- **Environment variable**: `export MDB_MCP_READ_ONLY=true` +- **Command-line argument**: `--readOnly` + +> **πŸ’‘ Platform Note:** For Windows users, see [Environment Variables](#environment-variables) for platform-specific instructions. + +When read-only mode is active, you'll see a message in the server logs indicating which tools were prevented from registering due to this restriction. + +### Index Check Mode + +The `indexCheck` configuration option allows you to enforce that query operations must use an index. When enabled, queries that perform a collection scan will be rejected to ensure better performance. + +This is useful for scenarios where you want to ensure that database queries are optimized. + +You can enable index check mode using: + +- **Environment variable**: `export MDB_MCP_INDEX_CHECK=true` +- **Command-line argument**: `--indexCheck` + +> **πŸ’‘ Platform Note:** For Windows users, see [Environment Variables](#environment-variables) for platform-specific instructions. + +When index check mode is active, you'll see an error message if a query is rejected due to not using an index. + +### Exports + +The data exported by the `export` tool is temporarily stored in the configured `exportsPath` on the machine running the MCP server until cleaned up by the export cleanup process. If the `exportsPath` configuration is not provided, the following defaults are used: + +- **Windows:** `%LOCALAPPDATA%\mongodb\mongodb-mcp\exports` +- **macOS/Linux:** `~/.mongodb/mongodb-mcp/exports` + +> **πŸ”’ Security Note:** When configuring a custom `exportsPath`, ensure the directory is owned and writable only by the user running the MongoDB MCP server process. Exported data may contain sensitive information from your database. On Linux/macOS, use `chmod 700` and verify ownership with `chown`. On Windows, restrict write permissions to the service account only. + +The `exportTimeoutMs` configuration controls the time after which the exported data is considered expired and eligible for cleanup. By default, exports expire after 5 minutes (300000ms). + +The `exportCleanupIntervalMs` configuration controls how frequently the cleanup process runs to remove expired export files. By default, cleanup runs every 2 minutes (120000ms). + +### Telemetry + +The `telemetry` configuration option allows you to disable telemetry collection. When enabled, the MCP server will collect usage data and send it to MongoDB. + +You can disable telemetry using: + +- **Environment variable**: `export MDB_MCP_TELEMETRY=disabled` +- **Command-line argument**: `--telemetry disabled` +- **DO_NOT_TRACK environment variable**: `export DO_NOT_TRACK=1` + +> **πŸ’‘ Platform Note:** For Windows users, see [Environment Variables](#environment-variables) for platform-specific instructions. + +### Opting into Preview Features + +The MongoDB MCP Server may offer functionality that is still in development and may change in future releases. These features are considered "preview features" and are not enabled by default. Generally, these features are well tested, but may not offer the complete functionality we intend to provide in the final release or we'd like to gather feedback before making them generally available. To enable one or more preview features, use the `previewFeatures` configuration option. + +- For **environment variable** configuration, use a comma-separated string: `export MDB_MCP_PREVIEW_FEATURES="vectorSearch,feature1,feature2"`. +- For **command-line argument** configuration, use a space-separated string: `--previewFeatures vectorSearch feature1 feature2`. + +List of available preview features: + +- `vectorSearch` - Enables tools or functionality related to Vector Search in MongoDB Atlas: + - Index management, such as creating, listing, and dropping search and vector search indexes. + - Querying collections using vector search capabilities. This requires a configured embedding model that will be used to generate vector representations of the query data. Currently, only [Voyage AI](https://www.voyageai.com) embedding models are supported. Set the `voyageApiKey` configuration option with your Voyage AI API key to use this feature. + +## Atlas API Access + +To use the Atlas API tools, you'll need to create a service account in MongoDB Atlas: + +> **ℹ️ Note:** For a detailed breakdown of the minimum required permissions for each Atlas operation, see the [Atlas API Permissions](#atlas-api-permissions) section below. + +1. **Create a Service Account:** + - Log in to MongoDB Atlas at [cloud.mongodb.com](https://cloud.mongodb.com) + - Navigate to Access Manager > Organization Access + - Click Add New > Applications > Service Accounts + - Enter name, description and expiration for your service account (e.g., "MCP, MCP Server Access, 7 days") + - **Assign only the minimum permissions needed for your use case.** + - See [Atlas API Permissions](#atlas-api-permissions) for details. + - Click "Create" + +To learn more about Service Accounts, check the [MongoDB Atlas documentation](https://www.mongodb.com/docs/atlas/api/service-accounts-overview/). + +2. **Save Client Credentials:** + - After creation, you'll be shown the Client ID and Client Secret + - **Important:** Copy and save the Client Secret immediately as it won't be displayed again + +3. **Add Access List Entry:** + - Add your IP address to the API access list + +4. **Configure the MCP Server:** + - Use one of the configuration methods below to set your `apiClientId` and `apiClientSecret` + +## Atlas API Permissions + +> **Security Warning:** Granting the Organization Owner role is rarely necessary and can be a security risk. Assign only the minimum permissions needed for your use case. + +### Quick Reference: Required roles per operation + +| What you want to do | Safest Role to Assign (where) | +| ------------------------------------ | --------------------------------------- | +| List orgs/projects | Org Member or Org Read Only (Org) | +| Create new projects | Org Project Creator (Org) | +| View clusters/databases in a project | Project Read Only (Project) | +| Create/manage clusters in a project | Project Cluster Manager (Project) | +| Manage project access lists | Project IP Access List Admin (Project) | +| Manage database users | Project Database Access Admin (Project) | + +- **Prefer project-level roles** for most operations. Assign only to the specific projects you need to manage or view. +- **Avoid Organization Owner** unless you require full administrative control over all projects and settings in the organization. + +For a full list of roles and their privileges, see the [Atlas User Roles documentation](https://www.mongodb.com/docs/atlas/reference/user-roles/#service-user-roles). + +## Configuration Methods + +### Configuration File + +Store configuration in a JSON file and load it using the `MDB_MCP_CONFIG` environment variable (recommended) or `--config` command-line argument. + +> **πŸ”’ Security Best Practice:** Prefer using the `MDB_MCP_CONFIG` environment variable over the `--config` CLI argument. Command-line arguments are visible in process listings. + +> **πŸ”’ File Security:** Ensure your configuration file has proper ownership and permissions, limited to the user running the MongoDB MCP server: +> +> **Linux/macOS:** +> +> ```bash +> chmod 600 /path/to/config.json +> chown your-username /path/to/config.json +> ``` +> +> **Windows:** Right-click the file β†’ Properties β†’ Security β†’ Restrict access to your user account only. + +Create a JSON file with your configuration (all keys use camelCase): + +```json +{ + "connectionString": "mongodb://localhost:27017", + "readOnly": true, + "loggers": ["stderr", "mcp"], + "apiClientId": "your-atlas-service-accounts-client-id", + "apiClientSecret": "your-atlas-service-accounts-client-secret", + "maxDocumentsPerQuery": 100 +} +``` + +**Linux/macOS (bash/zsh):** + +```bash +export MDB_MCP_CONFIG="/path/to/config.json" +npx -y mongodb-mcp-server@latest +``` + +**Windows Command Prompt (cmd):** + +```cmd +set "MDB_MCP_CONFIG=C:\path\to\config.json" +npx -y mongodb-mcp-server@latest +``` + +**Windows PowerShell:** + +```powershell +$env:MDB_MCP_CONFIG="C:\path\to\config.json" +npx -y mongodb-mcp-server@latest +``` + +Alternatively, use `--config` argument (less secure): + +```bash +npx -y mongodb-mcp-server@latest --config /path/to/config.json +``` + +### Environment Variables + +Set environment variables with the prefix `MDB_MCP_` followed by the option name in uppercase with underscores: + +**Linux/macOS (bash/zsh):** + +```bash +# Set Atlas API credentials (via Service Accounts) +export MDB_MCP_API_CLIENT_ID="your-atlas-service-accounts-client-id" +export MDB_MCP_API_CLIENT_SECRET="your-atlas-service-accounts-client-secret" + +# Set a custom MongoDB connection string +export MDB_MCP_CONNECTION_STRING="mongodb+srv://username:password@cluster.mongodb.net/myDatabase" + +# Set log path +export MDB_MCP_LOG_PATH="/path/to/logs" +``` + +**Windows Command Prompt (cmd):** + +```cmd +set "MDB_MCP_API_CLIENT_ID=your-atlas-service-accounts-client-id" +set "MDB_MCP_API_CLIENT_SECRET=your-atlas-service-accounts-client-secret" + +set "MDB_MCP_CONNECTION_STRING=mongodb+srv://username:password@cluster.mongodb.net/myDatabase" + +set "MDB_MCP_LOG_PATH=C:\path\to\logs" +``` + +**Windows PowerShell:** + +```powershell +# Set Atlas API credentials (via Service Accounts) +$env:MDB_MCP_API_CLIENT_ID="your-atlas-service-accounts-client-id" +$env:MDB_MCP_API_CLIENT_SECRET="your-atlas-service-accounts-client-secret" + +# Set a custom MongoDB connection string +$env:MDB_MCP_CONNECTION_STRING="mongodb+srv://username:password@cluster.mongodb.net/myDatabase" + +# Set log path +$env:MDB_MCP_LOG_PATH="C:\path\to\logs" +``` + +#### MCP configuration file examples + +##### Connection String with environment variables + +```json +{ + "mcpServers": { + "MongoDB": { + "command": "npx", + "args": ["-y", "mongodb-mcp-server"], + "env": { + "MDB_MCP_CONNECTION_STRING": "mongodb+srv://username:password@cluster.mongodb.net/myDatabase" + } + } + } +} +``` + +##### Atlas API credentials with environment variables + +```json +{ + "mcpServers": { + "MongoDB": { + "command": "npx", + "args": ["-y", "mongodb-mcp-server"], + "env": { + "MDB_MCP_API_CLIENT_ID": "your-atlas-service-accounts-client-id", + "MDB_MCP_API_CLIENT_SECRET": "your-atlas-service-accounts-client-secret" + } + } + } +} +``` + +### Command-Line Arguments + +Pass configuration options as command-line arguments when starting the server: + +> **πŸ”’ Security Note:** For sensitive configuration like API credentials and connection strings, use environment variables instead of command-line arguments. + +```shell +# Set sensitive data as environment variable +export MDB_MCP_API_CLIENT_ID="your-atlas-service-accounts-client-id" +export MDB_MCP_API_CLIENT_SECRET="your-atlas-service-accounts-client-secret" +export MDB_MCP_CONNECTION_STRING="mongodb+srv://username:password@cluster.mongodb.net/myDatabase" + +# Start the server with command line arguments +npx -y mongodb-mcp-server@latest --logPath=/path/to/logs --readOnly --indexCheck +``` + +> **πŸ’‘ Platform Note:** The examples above use Unix/Linux/macOS syntax. For Windows users, see [Environment Variables](#environment-variables) for platform-specific instructions. + +#### MCP client configuration file examples + +##### Connection String with command-line arguments + +> **πŸ”’ Security Note:** We do not recommend passing connection string as command line argument. Connection string might contain credentials which can be visible in process lists and logged in various system locations, potentially exposing your credentials. Instead configure [connection string through environment variables](#connection-string-with-environment-variables) + +```json +{ + "mcpServers": { + "MongoDB": { + "command": "npx", + "args": [ + "-y", + "mongodb-mcp-server", + "--connectionString", + "mongodb+srv://username:password@cluster.mongodb.net/myDatabase", + "--readOnly" + ] + } + } +} +``` + +##### Atlas API credentials with command-line arguments + +> **πŸ”’ Security Note:** We do not recommend passing Atlas API credentials as command line argument. The provided credentials can be visible in process lists and logged in various system locations, potentially exposing your credentials. Instead configure [Atlas API credentials through environment variables](#atlas-api-credentials-with-environment-variables) + +```json +{ + "mcpServers": { + "MongoDB": { + "command": "npx", + "args": [ + "-y", + "mongodb-mcp-server", + "--apiClientId", + "your-atlas-service-accounts-client-id", + "--apiClientSecret", + "your-atlas-service-accounts-client-secret", + "--readOnly" + ] + } + } +} +``` + +## Proxy Support + +The MCP Server will detect typical PROXY environment variables and use them for +connecting to the Atlas API, your MongoDB Cluster, or any other external calls +to third-party services like OID Providers. The behaviour is the same as what +`mongosh` does, so the same settings will work in the MCP Server. diff --git a/README.md b/README.md index d16928200..93779bd45 100644 --- a/README.md +++ b/README.md @@ -16,13 +16,6 @@ A Model Context Protocol server for interacting with MongoDB Databases and Mongo - [MongoDB Database Tools](#mongodb-database-tools) - [πŸ“„ Supported Resources](#supported-resources) - [βš™οΈ Configuration](#configuration) - - [Configuration Options](#configuration-options) - - [Atlas API Access](#atlas-api-access) - - [Configuration Methods](#configuration-methods) - - [Environment Variables](#environment-variables) - - [Command-Line Arguments](#command-line-arguments) - - [MCP Client Configuration](#mcp-configuration-file-examples) - - [Proxy Support](#proxy-support) - [πŸš€ Deploy on Public Clouds](#deploy-on-public-clouds) - [Azure Cloud](#azure) - [🀝 Contributing](#contributing) @@ -335,372 +328,7 @@ NOTE: atlas tools are only available when you set credentials on [configuration] ## Configuration -> **πŸ”’ Security Best Practice:** We strongly recommend using environment variables for sensitive configuration such as API credentials (`MDB_MCP_API_CLIENT_ID`, `MDB_MCP_API_CLIENT_SECRET`) and connection strings (`MDB_MCP_CONNECTION_STRING`) instead of command-line arguments. Environment variables are not visible in process lists and provide better security for your sensitive data. - -The MongoDB MCP Server can be configured using multiple methods, with the following precedence (highest to lowest): - -1. Command-line arguments -2. Environment variables - -### Configuration Options - -| CLI Option | Environment Variable | Default | Description | -| -------------------------------------- | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `apiClientId` | `MDB_MCP_API_CLIENT_ID` | `` | Atlas API client ID for authentication. Required for running Atlas tools. | -| `apiClientSecret` | `MDB_MCP_API_CLIENT_SECRET` | `` | Atlas API client secret for authentication. Required for running Atlas tools. | -| `atlasTemporaryDatabaseUserLifetimeMs` | `MDB_MCP_ATLAS_TEMPORARY_DATABASE_USER_LIFETIME_MS` | `14400000` | Time in milliseconds that temporary database users created when connecting to MongoDB Atlas clusters will remain active before being automatically deleted. | -| `confirmationRequiredTools` | `MDB_MCP_CONFIRMATION_REQUIRED_TOOLS` | `"atlas-create-access-list,atlas-create-db-user,drop-database,drop-collection,delete-many,drop-index"` | Comma separated values of tool names that require user confirmation before execution. Requires the client to support elicitation. | -| `connectionString` | `MDB_MCP_CONNECTION_STRING` | `` | MongoDB connection string for direct database connections. Optional, if not set, you'll need to call the connect tool before interacting with MongoDB data. | -| `disableEmbeddingsValidation` | `MDB_MCP_DISABLE_EMBEDDINGS_VALIDATION` | `false` | When set to true, disables validation of embeddings dimensions. | -| `disabledTools` | `MDB_MCP_DISABLED_TOOLS` | `""` | Comma separated values of tool names, operation types, and/or categories of tools that will be disabled. | -| `exportCleanupIntervalMs` | `MDB_MCP_EXPORT_CLEANUP_INTERVAL_MS` | `120000` | Time in milliseconds between export cleanup cycles that remove expired export files. | -| `exportTimeoutMs` | `MDB_MCP_EXPORT_TIMEOUT_MS` | `300000` | Time in milliseconds after which an export is considered expired and eligible for cleanup. | -| `exportsPath` | `MDB_MCP_EXPORTS_PATH` | see below\* | Folder to store exported data files. | -| `httpHost` | `MDB_MCP_HTTP_HOST` | `"127.0.0.1"` | Host address to bind the HTTP server to (only used when transport is 'http'). | -| `httpPort` | `MDB_MCP_HTTP_PORT` | `3000` | Port number for the HTTP server (only used when transport is 'http'). | -| `idleTimeoutMs` | `MDB_MCP_IDLE_TIMEOUT_MS` | `600000` | Idle timeout for a client to disconnect (only applies to http transport). | -| `indexCheck` | `MDB_MCP_INDEX_CHECK` | `false` | When set to true, enforces that query operations must use an index, rejecting queries that perform a collection scan. | -| `logPath` | `MDB_MCP_LOG_PATH` | see below\* | Folder to store logs. | -| `loggers` | `MDB_MCP_LOGGERS` | `"disk,mcp"` see below\* | Comma separated values of logger types. | -| `maxBytesPerQuery` | `MDB_MCP_MAX_BYTES_PER_QUERY` | `16777216` | The maximum size in bytes for results from a find or aggregate tool call. This serves as an upper bound for the responseBytesLimit parameter in those tools. | -| `maxDocumentsPerQuery` | `MDB_MCP_MAX_DOCUMENTS_PER_QUERY` | `100` | The maximum number of documents that can be returned by a find or aggregate tool call. For the find tool, the effective limit will be the smaller of this value and the tool's limit parameter. | -| `notificationTimeoutMs` | `MDB_MCP_NOTIFICATION_TIMEOUT_MS` | `540000` | Notification timeout for a client to be aware of disconnect (only applies to http transport). | -| `previewFeatures` | `MDB_MCP_PREVIEW_FEATURES` | `""` | Comma separated values of preview features that are enabled. | -| `readOnly` | `MDB_MCP_READ_ONLY` | `false` | When set to true, only allows read, connect, and metadata operation types, disabling create/update/delete operations. | -| `telemetry` | `MDB_MCP_TELEMETRY` | `"enabled"` | When set to disabled, disables telemetry collection. | -| `transport` | `MDB_MCP_TRANSPORT` | `"stdio"` | Either 'stdio' or 'http'. | -| `voyageApiKey` | `MDB_MCP_VOYAGE_API_KEY` | `""` | API key for Voyage AI embeddings service (required for vector search operations with text-to-embedding conversion). | - -#### Logger Options - -The `loggers` configuration option controls where logs are sent. You can specify one or more logger types as a comma-separated list. The available options are: - -- `mcp`: Sends logs to the MCP client (if supported by the client/transport). -- `disk`: Writes logs to disk files. Log files are stored in the log path (see `logPath` above). -- `stderr`: Outputs logs to standard error (stderr), useful for debugging or when running in containers. - -**Default:** `disk,mcp` (logs are written to disk and sent to the MCP client). - -You can combine multiple loggers, e.g. `--loggers disk stderr` or `export MDB_MCP_LOGGERS="mcp,stderr"`. - -##### Example: Set logger via environment variable - -```shell -export MDB_MCP_LOGGERS="disk,stderr" -``` - -> **πŸ’‘ Platform Note:** For Windows users, see [Environment Variables](#environment-variables) for platform-specific instructions. - -##### Example: Set logger via command-line argument - -```shell -npx -y mongodb-mcp-server@latest --loggers mcp stderr -``` - -##### Log File Location - -When using the `disk` logger, log files are stored in: - -- **Windows:** `%LOCALAPPDATA%\mongodb\mongodb-mcp\.app-logs` -- **macOS/Linux:** `~/.mongodb/mongodb-mcp/.app-logs` - -You can override the log directory with the `logPath` option. - -#### Disabled Tools - -You can disable specific tools or categories of tools by using the `disabledTools` option. This option accepts an array of strings, -where each string can be a tool name, operation type, or category. - -The way the array is constructed depends on the type of configuration method you use: - -- For **environment variable** configuration, use a comma-separated string: `export MDB_MCP_DISABLED_TOOLS="create,update,delete,atlas,collectionSchema"`. -- For **command-line argument** configuration, use a space-separated string: `--disabledTools create update delete atlas collectionSchema`. - -Categories of tools: - -- `atlas` - MongoDB Atlas tools, such as list clusters, create cluster, etc. -- `mongodb` - MongoDB database tools, such as find, aggregate, etc. - -Operation types: - -- `create` - Tools that create resources, such as create cluster, insert document, etc. -- `update` - Tools that update resources, such as update document, rename collection, etc. -- `delete` - Tools that delete resources, such as delete document, drop collection, etc. -- `read` - Tools that read resources, such as find, aggregate, list clusters, etc. -- `metadata` - Tools that read metadata, such as list databases/collections/indexes, infer collection schema, etc. -- `connect` - Tools that allow you to connect or switch the connection to a MongoDB instance. If this is disabled, you will need to provide a connection string through the config when starting the server. - -#### Require Confirmation - -If your client supports [elicitation](https://modelcontextprotocol.io/specification/draft/client/elicitation), you can set the MongoDB MCP server to request user confirmation before executing certain tools. - -When a tool is marked as requiring confirmation, the server will send an elicitation request to the client. The client with elicitation support will then prompt the user for confirmation and send the response back to the server. If the client does not support elicitation, the tool will execute without confirmation. - -You can set the `confirmationRequiredTools` configuration option to specify the names of tools which require confirmation. By default, the following tools have this setting enabled: `drop-database`, `drop-collection`, `delete-many`, `atlas-create-db-user`, `atlas-create-access-list`. - -#### Read-Only Mode - -The `readOnly` configuration option allows you to restrict the MCP server to only use tools with "read", "connect", and "metadata" operation types. When enabled, all tools that have "create", "update" or "delete" operation types will not be registered with the server. - -This is useful for scenarios where you want to provide access to MongoDB data for analysis without allowing any modifications to the data or infrastructure. - -You can enable read-only mode using: - -- **Environment variable**: `export MDB_MCP_READ_ONLY=true` -- **Command-line argument**: `--readOnly` - -> **πŸ’‘ Platform Note:** For Windows users, see [Environment Variables](#environment-variables) for platform-specific instructions. - -When read-only mode is active, you'll see a message in the server logs indicating which tools were prevented from registering due to this restriction. - -#### Index Check Mode - -The `indexCheck` configuration option allows you to enforce that query operations must use an index. When enabled, queries that perform a collection scan will be rejected to ensure better performance. - -This is useful for scenarios where you want to ensure that database queries are optimized. - -You can enable index check mode using: - -- **Environment variable**: `export MDB_MCP_INDEX_CHECK=true` -- **Command-line argument**: `--indexCheck` - -> **πŸ’‘ Platform Note:** For Windows users, see [Environment Variables](#environment-variables) for platform-specific instructions. - -When index check mode is active, you'll see an error message if a query is rejected due to not using an index. - -#### Exports - -The data exported by the `export` tool is temporarily stored in the configured `exportsPath` on the machine running the MCP server until cleaned up by the export cleanup process. If the `exportsPath` configuration is not provided, the following defaults are used: - -- **Windows:** `%LOCALAPPDATA%\mongodb\mongodb-mcp\exports` -- **macOS/Linux:** `~/.mongodb/mongodb-mcp/exports` - -The `exportTimeoutMs` configuration controls the time after which the exported data is considered expired and eligible for cleanup. By default, exports expire after 5 minutes (300000ms). - -The `exportCleanupIntervalMs` configuration controls how frequently the cleanup process runs to remove expired export files. By default, cleanup runs every 2 minutes (120000ms). - -#### Telemetry - -The `telemetry` configuration option allows you to disable telemetry collection. When enabled, the MCP server will collect usage data and send it to MongoDB. - -You can disable telemetry using: - -- **Environment variable**: `export MDB_MCP_TELEMETRY=disabled` -- **Command-line argument**: `--telemetry disabled` -- **DO_NOT_TRACK environment variable**: `export DO_NOT_TRACK=1` - -> **πŸ’‘ Platform Note:** For Windows users, see [Environment Variables](#environment-variables) for platform-specific instructions. - -#### Opting into Preview Features - -The MongoDB MCP Server may offer functionality that is still in development and may change in future releases. These features are considered "preview features" and are not enabled by default. Generally, these features are well tested, but may not offer the complete functionality we intend to provide in the final release or we'd like to gather feedback before making them generally available. To enable one or more preview features, use the `previewFeatures` configuration option. - -- For **environment variable** configuration, use a comma-separated string: `export MDB_MCP_PREVIEW_FEATURES="vectorSearch,feature1,feature2"`. -- For **command-line argument** configuration, use a space-separated string: `--previewFeatures vectorSearch feature1 feature2`. - -List of available preview features: - -- `vectorSearch` - Enables tools or functionality related to Vector Search in MongoDB Atlas: - - Index management, such as creating, listing, and dropping search and vector search indexes. - - Querying collections using vector search capabilities. This requires a configured embedding model that will be used to generate vector representations of the query data. Currently, only [Voyage AI](https://www.voyageai.com) embedding models are supported. Set the `voyageApiKey` configuration option with your Voyage AI API key to use this feature. - -### Atlas API Access - -To use the Atlas API tools, you'll need to create a service account in MongoDB Atlas: - -> **ℹ️ Note:** For a detailed breakdown of the minimum required permissions for each Atlas operation, see the [Atlas API Permissions](#atlas-api-permissions) section below. - -1. **Create a Service Account:** - - Log in to MongoDB Atlas at [cloud.mongodb.com](https://cloud.mongodb.com) - - Navigate to Access Manager > Organization Access - - Click Add New > Applications > Service Accounts - - Enter name, description and expiration for your service account (e.g., "MCP, MCP Server Access, 7 days") - - **Assign only the minimum permissions needed for your use case.** - - See [Atlas API Permissions](#atlas-api-permissions) for details. - - Click "Create" - -To learn more about Service Accounts, check the [MongoDB Atlas documentation](https://www.mongodb.com/docs/atlas/api/service-accounts-overview/). - -2. **Save Client Credentials:** - - After creation, you'll be shown the Client ID and Client Secret - - **Important:** Copy and save the Client Secret immediately as it won't be displayed again - -3. **Add Access List Entry:** - - Add your IP address to the API access list - -4. **Configure the MCP Server:** - - Use one of the configuration methods below to set your `apiClientId` and `apiClientSecret` - -### Atlas API Permissions - -> **Security Warning:** Granting the Organization Owner role is rarely necessary and can be a security risk. Assign only the minimum permissions needed for your use case. - -#### Quick Reference: Required roles per operation - -| What you want to do | Safest Role to Assign (where) | -| ------------------------------------ | --------------------------------------- | -| List orgs/projects | Org Member or Org Read Only (Org) | -| Create new projects | Org Project Creator (Org) | -| View clusters/databases in a project | Project Read Only (Project) | -| Create/manage clusters in a project | Project Cluster Manager (Project) | -| Manage project access lists | Project IP Access List Admin (Project) | -| Manage database users | Project Database Access Admin (Project) | - -- **Prefer project-level roles** for most operations. Assign only to the specific projects you need to manage or view. -- **Avoid Organization Owner** unless you require full administrative control over all projects and settings in the organization. - -For a full list of roles and their privileges, see the [Atlas User Roles documentation](https://www.mongodb.com/docs/atlas/reference/user-roles/#service-user-roles). - -### Configuration Methods - -#### Environment Variables - -Set environment variables with the prefix `MDB_MCP_` followed by the option name in uppercase with underscores: - -**Linux/macOS (bash/zsh):** - -```bash -# Set Atlas API credentials (via Service Accounts) -export MDB_MCP_API_CLIENT_ID="your-atlas-service-accounts-client-id" -export MDB_MCP_API_CLIENT_SECRET="your-atlas-service-accounts-client-secret" - -# Set a custom MongoDB connection string -export MDB_MCP_CONNECTION_STRING="mongodb+srv://username:password@cluster.mongodb.net/myDatabase" - -# Set log path -export MDB_MCP_LOG_PATH="/path/to/logs" -``` - -**Windows Command Prompt (cmd):** - -```cmd -set "MDB_MCP_API_CLIENT_ID=your-atlas-service-accounts-client-id" -set "MDB_MCP_API_CLIENT_SECRET=your-atlas-service-accounts-client-secret" - -set "MDB_MCP_CONNECTION_STRING=mongodb+srv://username:password@cluster.mongodb.net/myDatabase" - -set "MDB_MCP_LOG_PATH=C:\path\to\logs" -``` - -**Windows PowerShell:** - -```powershell -# Set Atlas API credentials (via Service Accounts) -$env:MDB_MCP_API_CLIENT_ID="your-atlas-service-accounts-client-id" -$env:MDB_MCP_API_CLIENT_SECRET="your-atlas-service-accounts-client-secret" - -# Set a custom MongoDB connection string -$env:MDB_MCP_CONNECTION_STRING="mongodb+srv://username:password@cluster.mongodb.net/myDatabase" - -# Set log path -$env:MDB_MCP_LOG_PATH="C:\path\to\logs" -``` - -#### MCP configuration file examples - -##### Connection String with environment variables - -```json -{ - "mcpServers": { - "MongoDB": { - "command": "npx", - "args": ["-y", "mongodb-mcp-server"], - "env": { - "MDB_MCP_CONNECTION_STRING": "mongodb+srv://username:password@cluster.mongodb.net/myDatabase" - } - } - } -} -``` - -##### Atlas API credentials with environment variables - -```json -{ - "mcpServers": { - "MongoDB": { - "command": "npx", - "args": ["-y", "mongodb-mcp-server"], - "env": { - "MDB_MCP_API_CLIENT_ID": "your-atlas-service-accounts-client-id", - "MDB_MCP_API_CLIENT_SECRET": "your-atlas-service-accounts-client-secret" - } - } - } -} -``` - -#### Command-Line Arguments - -Pass configuration options as command-line arguments when starting the server: - -> **πŸ”’ Security Note:** For sensitive configuration like API credentials and connection strings, use environment variables instead of command-line arguments. - -```shell -# Set sensitive data as environment variable -export MDB_MCP_API_CLIENT_ID="your-atlas-service-accounts-client-id" -export MDB_MCP_API_CLIENT_SECRET="your-atlas-service-accounts-client-secret" -export MDB_MCP_CONNECTION_STRING="mongodb+srv://username:password@cluster.mongodb.net/myDatabase" - -# Start the server with command line arguments -npx -y mongodb-mcp-server@latest --logPath=/path/to/logs --readOnly --indexCheck -``` - -> **πŸ’‘ Platform Note:** The examples above use Unix/Linux/macOS syntax. For Windows users, see [Environment Variables](#environment-variables) for platform-specific instructions. - -#### MCP configuration file examples - -##### Connection String with command-line arguments - -> **πŸ”’ Security Note:** We do not recommend passing connection string as command line argument. Connection string might contain credentials which can be visible in process lists and logged in various system locations, potentially exposing your credentials. Instead configure [connection string through environment variables](#connection-string-with-environment-variables) - -```json -{ - "mcpServers": { - "MongoDB": { - "command": "npx", - "args": [ - "-y", - "mongodb-mcp-server", - "--connectionString", - "mongodb+srv://username:password@cluster.mongodb.net/myDatabase", - "--readOnly" - ] - } - } -} -``` - -##### Atlas API credentials with command-line arguments - -> **πŸ”’ Security Note:** We do not recommend passing Atlas API credentials as command line argument. The provided credentials can be visible in process lists and logged in various system locations, potentially exposing your credentials. Instead configure [Atlas API credentials through environment variables](#atlas-api-credentials-with-environment-variables) - -```json -{ - "mcpServers": { - "MongoDB": { - "command": "npx", - "args": [ - "-y", - "mongodb-mcp-server", - "--apiClientId", - "your-atlas-service-accounts-client-id", - "--apiClientSecret", - "your-atlas-service-accounts-client-secret", - "--readOnly" - ] - } - } -} -``` - -### Proxy Support - -The MCP Server will detect typical PROXY environment variables and use them for -connecting to the Atlas API, your MongoDB Cluster, or any other external calls -to third-party services like OID Providers. The behaviour is the same as what -`mongosh` does, so the same settings will work in the MCP Server. +For complete configuration details (all options, security practices, Atlas setup, examples), see the dedicated documentation in [CONFIGURATION.md](CONFIGURATION.md). ## πŸš€Deploy on Public Clouds diff --git a/package-lock.json b/package-lock.json index b554582d4..d426223ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1681,7 +1681,6 @@ "resolved": "https://registry.npmjs.org/@mongodb-js/oidc-plugin/-/oidc-plugin-2.0.4.tgz", "integrity": "sha512-mB7kEK80+DD2QrB01GmtFKm02ItJpIO9j7OARMHI4RL+rVQD3Ey9giluf3xQtuSdcmg7a+bf5fkJgQZCWMvRPg==", "license": "Apache-2.0", - "peer": true, "dependencies": { "express": "^5.1.0", "node-fetch": "^3.3.2", @@ -1935,7 +1934,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=8.0.0" } @@ -4077,7 +4075,6 @@ "integrity": "sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.14.0" } @@ -5043,7 +5040,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5107,7 +5103,6 @@ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -5825,7 +5820,6 @@ "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.4.tgz", "integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==", "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=16.20.1" } @@ -7332,7 +7326,6 @@ "integrity": "sha512-QePbBFMJFjgmlE+cXAlbHZbHpdFVS2E/6vzCy7aKlebddvl1vadiC4JFV5u/wqTkNUwEV8WrQi257jf5f06hrg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -7394,7 +7387,6 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", - "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -7708,7 +7700,6 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", "license": "MIT", - "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", @@ -9993,7 +9984,6 @@ "integrity": "sha512-UczzB+0nnwGotYSgllfARAqWCJ5e/skuV2K/l+Zyck/H6pJIhLXuBnz+6vn2i211o7DtbE78HQtsYEKICHGI+g==", "dev": true, "license": "MIT", - "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/mobx" @@ -10056,7 +10046,6 @@ "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.21.0.tgz", "integrity": "sha512-URyb/VXMjJ4da46OeSXg+puO39XH9DeQpWCslifrRn9JWugy0D+DvvBvkm2WxmHe61O/H19JM66p1z7RHVkZ6A==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@mongodb-js/saslprep": "^1.3.0", "bson": "^6.10.4", @@ -10206,7 +10195,6 @@ "resolved": "https://registry.npmjs.org/mongodb-log-writer/-/mongodb-log-writer-2.4.2.tgz", "integrity": "sha512-jXKSNG/z3sBgD42p2puOoBHKcxKHJhiIVfvGhSlwNezJIr7aL74kpKowQ3kG8Oq+nljhjfDNru8Meeq24Em3lg==", "license": "Apache-2.0", - "peer": true, "dependencies": { "heap-js": "^2.3.0" }, @@ -11263,7 +11251,6 @@ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -11548,7 +11535,6 @@ "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -11562,7 +11548,6 @@ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -13167,7 +13152,6 @@ "integrity": "sha512-1v/e3Dl1BknC37cXMhwGomhO8AkYmN41CqyX9xhUDxry1ns3BFQy2lLDRQXJRdVVWB9OHemv/53xaStimvWyuA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@emotion/is-prop-valid": "1.2.2", "@emotion/unitless": "0.8.1", @@ -13780,7 +13764,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -13969,7 +13952,6 @@ "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" @@ -14123,7 +14105,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -14397,7 +14378,6 @@ "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -14514,7 +14494,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -14557,7 +14536,6 @@ "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", @@ -15165,7 +15143,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/scripts/generateArguments.ts b/scripts/generateArguments.ts index 04ff001ef..6bd88fe00 100644 --- a/scripts/generateArguments.ts +++ b/scripts/generateArguments.ts @@ -3,7 +3,7 @@ /** * This script generates argument definitions and updates: * - server.json arrays - * - README.md configuration table + * - CONFIGURATION.md configuration table * * It uses the Zod schema and OPTIONS defined in src/common/config.ts */ @@ -244,7 +244,7 @@ function updateServerJsonEnvVars(envVars: ArgumentInfo[]): void { console.log(`βœ“ Updated server.json (version ${version})`); } -function generateReadmeConfigTable(argumentInfos: ArgumentInfo[]): string { +function generateConfigurationTable(argumentInfos: ArgumentInfo[]): string { const rows = [ "| CLI Option | Environment Variable | Default | Description |", "| -------------------------------------- | --------------------------------------------------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |", @@ -275,6 +275,10 @@ function generateReadmeConfigTable(argumentInfos: ArgumentInfo[]): string { case "string": defaultValueString = `\`"${defaultValue}"\``; break; + case "object": { + defaultValueString = `\`"${JSON.stringify(defaultValue)}"\``; + break; + } default: throw new Error(`Unsupported default value type: ${typeof defaultValue}`); } @@ -290,23 +294,23 @@ function generateReadmeConfigTable(argumentInfos: ArgumentInfo[]): string { return rows.join("\n"); } -function updateReadmeConfigTable(envVars: ArgumentInfo[]): void { - const readmePath = join(__dirname, "..", "README.md"); - let content = readFileSync(readmePath, "utf-8"); +function updateConfigurationTable(envVars: ArgumentInfo[]): void { + const configurationPath = join(__dirname, "..", "CONFIGURATION.md"); + let content = readFileSync(configurationPath, "utf-8"); - const newTable = generateReadmeConfigTable(envVars); + const newTable = generateConfigurationTable(envVars); // Find and replace the configuration options table - const tableRegex = /### Configuration Options\n\n\| CLI Option[\s\S]*?\n\n####/; - const replacement = `### Configuration Options\n\n${newTable}\n\n####`; + const tableRegex = /## Configuration Options\n\n\| CLI Option[\s\S]*?\n\n###/; + const replacement = `## Configuration Options\n\n${newTable}\n\n###`; content = content.replace(tableRegex, replacement); - writeFileSync(readmePath, content, "utf-8"); - console.log("βœ“ Updated README.md configuration table"); + writeFileSync(configurationPath, content, "utf-8"); + console.log("βœ“ Updated CONFIGURATION.md configuration table"); - // Run prettier on the README.md file - execSync("npx prettier --write README.md", { cwd: join(__dirname, "..") }); + // Run prettier on the CONFIGURATION.md file + execSync("npx prettier --write CONFIGURATION.md", { cwd: join(__dirname, "..") }); } function main(): void { @@ -314,7 +318,7 @@ function main(): void { const argumentInfo = getArgumentInfo(OPTIONS, zodMetadata); updateServerJsonEnvVars(argumentInfo); - updateReadmeConfigTable(argumentInfo); + updateConfigurationTable(argumentInfo); } main(); diff --git a/server.json b/server.json index bc0c7ada2..7a49f9897 100644 --- a/server.json +++ b/server.json @@ -170,6 +170,13 @@ "format": "string", "isSecret": false }, + { + "name": "MDB_MCP_TOOL_METADATA_OVERRIDES", + "description": "A map of name of the MongoDB MCP server tool to the metadata that needs to be used for that tool. Example: `{ \"toolMetadataOverrides\": { \"find\": { \"name\": \"query\" } } }`", + "isRequired": false, + "format": "string", + "isSecret": false + }, { "name": "MDB_MCP_TRANSPORT", "description": "Either 'stdio' or 'http'.", @@ -323,6 +330,12 @@ "description": "When set to disabled, disables telemetry collection.", "isRequired": false }, + { + "type": "named", + "name": "--toolMetadataOverrides", + "description": "A map of name of the MongoDB MCP server tool to the metadata that needs to be used for that tool. Example: `{ \"toolMetadataOverrides\": { \"find\": { \"name\": \"query\" } } }`", + "isRequired": false + }, { "type": "named", "name": "--transport", @@ -498,6 +511,13 @@ "format": "string", "isSecret": false }, + { + "name": "MDB_MCP_TOOL_METADATA_OVERRIDES", + "description": "A map of name of the MongoDB MCP server tool to the metadata that needs to be used for that tool. Example: `{ \"toolMetadataOverrides\": { \"find\": { \"name\": \"query\" } } }`", + "isRequired": false, + "format": "string", + "isSecret": false + }, { "name": "MDB_MCP_TRANSPORT", "description": "Either 'stdio' or 'http'.", @@ -651,6 +671,12 @@ "description": "When set to disabled, disables telemetry collection.", "isRequired": false }, + { + "type": "named", + "name": "--toolMetadataOverrides", + "description": "A map of name of the MongoDB MCP server tool to the metadata that needs to be used for that tool. Example: `{ \"toolMetadataOverrides\": { \"find\": { \"name\": \"query\" } } }`", + "isRequired": false + }, { "type": "named", "name": "--transport", diff --git a/src/common/config/argsParserOptions.ts b/src/common/config/argsParserOptions.ts index 8decc318a..fb78c66f1 100644 --- a/src/common/config/argsParserOptions.ts +++ b/src/common/config/argsParserOptions.ts @@ -55,6 +55,11 @@ export const OPTIONS = { "exportTimeoutMs", "exportCleanupIntervalMs", "voyageApiKey", + // Note: toolMetadataOverrides expects a JSON object but we mention it + // as a string only to let yargs-parser know that this key needs to be + // parsed. The internal handling of the key as an object is done within + // yargs-parser itself. + "toolMetadataOverrides", ], boolean: [ "apiDeprecationErrors", diff --git a/src/common/config/createUserConfig.ts b/src/common/config/createUserConfig.ts index 335eeed66..c791be935 100644 --- a/src/common/config/createUserConfig.ts +++ b/src/common/config/createUserConfig.ts @@ -95,6 +95,10 @@ function parseUserConfigSources(cliArguments: string[]): { ...OPTIONS, // This helps parse the relevant environment variables. envPrefix: "MDB_MCP_", + // This is the name of key that yargs-parser will look up in CLI + // arguments (--config) and ENV variables (MDB_MCP_CONFIG) to load an + // initial configuration from. + config: "config", configuration: { ...OPTIONS.configuration, // Setting this to true will populate `_` variable which is diff --git a/src/common/config/userConfig.ts b/src/common/config/userConfig.ts index 938155ab4..ecfb50bd2 100644 --- a/src/common/config/userConfig.ts +++ b/src/common/config/userConfig.ts @@ -167,4 +167,16 @@ export const UserConfigSchema = z4.object({ ) .default([]) .describe("An array of preview features that are enabled."), + toolMetadataOverrides: z4 + .record( + z4.string().describe("Original name of the MongoDB MCP server tool that needs to be overridden."), + z4.object({ + name: z4.string().nonempty().optional().describe("New name to be used for the tool."), + description: z4.string().nonempty().optional().describe("New description to be used for the tool."), + }) + ) + .default({}) + .describe( + 'A map of name of the MongoDB MCP server tool to the metadata that needs to be used for that tool. Example: `{ "toolMetadataOverrides": { "find": { "name": "query" } } }`' + ), }); diff --git a/src/server.ts b/src/server.ts index 3c99b376a..6c3be3fce 100644 --- a/src/server.ts +++ b/src/server.ts @@ -240,6 +240,9 @@ export class Server { } private registerTools(): void { + const toolsToRegister: ToolBase[] = []; + const usedNames = new Set(); + for (const toolConstructor of this.toolConstructors) { const tool = new toolConstructor({ session: this.session, @@ -247,6 +250,17 @@ export class Server { telemetry: this.telemetry, elicitation: this.elicitation, }); + + if (usedNames.has(tool.name)) { + throw new Error( + `Tool name collision detected: '${tool.name}'. This might be due to toolMetadataOverrides configuration.` + ); + } + usedNames.add(tool.name); + toolsToRegister.push(tool); + } + + for (const tool of toolsToRegister) { if (tool.register(this)) { this.tools.push(tool); } diff --git a/src/tools/atlas/connect/connectCluster.ts b/src/tools/atlas/connect/connectCluster.ts index 96a0a51c3..97fb9adbd 100644 --- a/src/tools/atlas/connect/connectCluster.ts +++ b/src/tools/atlas/connect/connectCluster.ts @@ -30,8 +30,8 @@ export const ConnectClusterArgs = { }; export class ConnectClusterTool extends AtlasToolBase { - public name = "atlas-connect-cluster"; - protected description = "Connect to MongoDB Atlas cluster"; + public internalName = "atlas-connect-cluster"; + protected internalDescription = "Connect to MongoDB Atlas cluster"; public operationType: OperationType = "connect"; protected argsShape = { ...ConnectClusterArgs, diff --git a/src/tools/atlas/create/createAccessList.ts b/src/tools/atlas/create/createAccessList.ts index 2bf1649b9..54a8ef260 100644 --- a/src/tools/atlas/create/createAccessList.ts +++ b/src/tools/atlas/create/createAccessList.ts @@ -17,8 +17,8 @@ export const CreateAccessListArgs = { }; export class CreateAccessListTool extends AtlasToolBase { - public name = "atlas-create-access-list"; - protected description = "Allow Ip/CIDR ranges to access your MongoDB Atlas clusters."; + public internalName = "atlas-create-access-list"; + protected internalDescription = "Allow Ip/CIDR ranges to access your MongoDB Atlas clusters."; public operationType: OperationType = "create"; protected argsShape = { ...CreateAccessListArgs, diff --git a/src/tools/atlas/create/createDBUser.ts b/src/tools/atlas/create/createDBUser.ts index c8e8ea014..c83d4f50b 100644 --- a/src/tools/atlas/create/createDBUser.ts +++ b/src/tools/atlas/create/createDBUser.ts @@ -34,8 +34,8 @@ export const CreateDBUserArgs = { }; export class CreateDBUserTool extends AtlasToolBase { - public name = "atlas-create-db-user"; - protected description = "Create an MongoDB Atlas database user"; + public internalName = "atlas-create-db-user"; + protected internalDescription = "Create an MongoDB Atlas database user"; public operationType: OperationType = "create"; protected argsShape = { ...CreateDBUserArgs, diff --git a/src/tools/atlas/create/createFreeCluster.ts b/src/tools/atlas/create/createFreeCluster.ts index 6b1ac98eb..0fc4fef19 100644 --- a/src/tools/atlas/create/createFreeCluster.ts +++ b/src/tools/atlas/create/createFreeCluster.ts @@ -6,8 +6,8 @@ import { ensureCurrentIpInAccessList } from "../../../common/atlas/accessListUti import { AtlasArgs } from "../../args.js"; export class CreateFreeClusterTool extends AtlasToolBase { - public name = "atlas-create-free-cluster"; - protected description = "Create a free MongoDB Atlas cluster"; + public internalName = "atlas-create-free-cluster"; + protected internalDescription = "Create a free MongoDB Atlas cluster"; public operationType: OperationType = "create"; protected argsShape = { projectId: AtlasArgs.projectId().describe("Atlas project ID to create the cluster in"), diff --git a/src/tools/atlas/create/createProject.ts b/src/tools/atlas/create/createProject.ts index 3ce9f0259..78694b806 100644 --- a/src/tools/atlas/create/createProject.ts +++ b/src/tools/atlas/create/createProject.ts @@ -5,8 +5,8 @@ import type { Group } from "../../../common/atlas/openapi.js"; import { AtlasArgs } from "../../args.js"; export class CreateProjectTool extends AtlasToolBase { - public name = "atlas-create-project"; - protected description = "Create a MongoDB Atlas project"; + public internalName = "atlas-create-project"; + protected internalDescription = "Create a MongoDB Atlas project"; public operationType: OperationType = "create"; protected argsShape = { projectName: AtlasArgs.projectName().optional().describe("Name for the new project"), diff --git a/src/tools/atlas/read/getPerformanceAdvisor.ts b/src/tools/atlas/read/getPerformanceAdvisor.ts index f62450106..b167ecaed 100644 --- a/src/tools/atlas/read/getPerformanceAdvisor.ts +++ b/src/tools/atlas/read/getPerformanceAdvisor.ts @@ -24,8 +24,8 @@ const PerformanceAdvisorOperationType = z.enum([ ]); export class GetPerformanceAdvisorTool extends AtlasToolBase { - public name = "atlas-get-performance-advisor"; - protected description = `Get MongoDB Atlas performance advisor recommendations, which includes the operations: suggested indexes, drop index suggestions, schema suggestions, and a sample of the most recent (max ${DEFAULT_SLOW_QUERY_LOGS_LIMIT}) slow query logs`; + public internalName = "atlas-get-performance-advisor"; + protected internalDescription = `Get MongoDB Atlas performance advisor recommendations, which includes the operations: suggested indexes, drop index suggestions, schema suggestions, and a sample of the most recent (max ${DEFAULT_SLOW_QUERY_LOGS_LIMIT}) slow query logs`; public operationType: OperationType = "read"; protected argsShape = { projectId: AtlasArgs.projectId().describe( diff --git a/src/tools/atlas/read/inspectAccessList.ts b/src/tools/atlas/read/inspectAccessList.ts index 7db73e7b2..8620ef7e3 100644 --- a/src/tools/atlas/read/inspectAccessList.ts +++ b/src/tools/atlas/read/inspectAccessList.ts @@ -8,8 +8,8 @@ export const InspectAccessListArgs = { }; export class InspectAccessListTool extends AtlasToolBase { - public name = "atlas-inspect-access-list"; - protected description = "Inspect Ip/CIDR ranges with access to your MongoDB Atlas clusters."; + public internalName = "atlas-inspect-access-list"; + protected internalDescription = "Inspect Ip/CIDR ranges with access to your MongoDB Atlas clusters."; public operationType: OperationType = "read"; protected argsShape = { ...InspectAccessListArgs, diff --git a/src/tools/atlas/read/inspectCluster.ts b/src/tools/atlas/read/inspectCluster.ts index fd8806105..fe920b6bb 100644 --- a/src/tools/atlas/read/inspectCluster.ts +++ b/src/tools/atlas/read/inspectCluster.ts @@ -11,8 +11,8 @@ export const InspectClusterArgs = { }; export class InspectClusterTool extends AtlasToolBase { - public name = "atlas-inspect-cluster"; - protected description = "Inspect MongoDB Atlas cluster"; + public internalName = "atlas-inspect-cluster"; + protected internalDescription = "Inspect MongoDB Atlas cluster"; public operationType: OperationType = "read"; protected argsShape = { ...InspectClusterArgs, diff --git a/src/tools/atlas/read/listAlerts.ts b/src/tools/atlas/read/listAlerts.ts index 1e3a6998e..be6dfe46c 100644 --- a/src/tools/atlas/read/listAlerts.ts +++ b/src/tools/atlas/read/listAlerts.ts @@ -8,8 +8,8 @@ export const ListAlertsArgs = { }; export class ListAlertsTool extends AtlasToolBase { - public name = "atlas-list-alerts"; - protected description = "List MongoDB Atlas alerts"; + public internalName = "atlas-list-alerts"; + protected internalDescription = "List MongoDB Atlas alerts"; public operationType: OperationType = "read"; protected argsShape = { ...ListAlertsArgs, diff --git a/src/tools/atlas/read/listClusters.ts b/src/tools/atlas/read/listClusters.ts index 1d1e3656a..7f780d3b7 100644 --- a/src/tools/atlas/read/listClusters.ts +++ b/src/tools/atlas/read/listClusters.ts @@ -16,8 +16,8 @@ export const ListClustersArgs = { }; export class ListClustersTool extends AtlasToolBase { - public name = "atlas-list-clusters"; - protected description = "List MongoDB Atlas clusters"; + public internalName = "atlas-list-clusters"; + protected internalDescription = "List MongoDB Atlas clusters"; public operationType: OperationType = "read"; protected argsShape = { ...ListClustersArgs, diff --git a/src/tools/atlas/read/listDBUsers.ts b/src/tools/atlas/read/listDBUsers.ts index 7103f2664..c32cefb64 100644 --- a/src/tools/atlas/read/listDBUsers.ts +++ b/src/tools/atlas/read/listDBUsers.ts @@ -9,8 +9,8 @@ export const ListDBUsersArgs = { }; export class ListDBUsersTool extends AtlasToolBase { - public name = "atlas-list-db-users"; - protected description = "List MongoDB Atlas database users"; + public internalName = "atlas-list-db-users"; + protected internalDescription = "List MongoDB Atlas database users"; public operationType: OperationType = "read"; protected argsShape = { ...ListDBUsersArgs, diff --git a/src/tools/atlas/read/listOrgs.ts b/src/tools/atlas/read/listOrgs.ts index f8bb3200b..93fcada2f 100644 --- a/src/tools/atlas/read/listOrgs.ts +++ b/src/tools/atlas/read/listOrgs.ts @@ -4,8 +4,8 @@ import type { OperationType } from "../../tool.js"; import { formatUntrustedData } from "../../tool.js"; export class ListOrganizationsTool extends AtlasToolBase { - public name = "atlas-list-orgs"; - protected description = "List MongoDB Atlas organizations"; + public internalName = "atlas-list-orgs"; + protected internalDescription = "List MongoDB Atlas organizations"; public operationType: OperationType = "read"; protected argsShape = {}; diff --git a/src/tools/atlas/read/listProjects.ts b/src/tools/atlas/read/listProjects.ts index 5ee2ae8d6..03ee695d3 100644 --- a/src/tools/atlas/read/listProjects.ts +++ b/src/tools/atlas/read/listProjects.ts @@ -6,8 +6,8 @@ import type { ToolArgs } from "../../tool.js"; import { AtlasArgs } from "../../args.js"; export class ListProjectsTool extends AtlasToolBase { - public name = "atlas-list-projects"; - protected description = "List MongoDB Atlas projects"; + public internalName = "atlas-list-projects"; + protected internalDescription = "List MongoDB Atlas projects"; public operationType: OperationType = "read"; protected argsShape = { orgId: AtlasArgs.organizationId() diff --git a/src/tools/atlasLocal/connect/connectDeployment.ts b/src/tools/atlasLocal/connect/connectDeployment.ts index 7bf8db4bb..7605ac93c 100644 --- a/src/tools/atlasLocal/connect/connectDeployment.ts +++ b/src/tools/atlasLocal/connect/connectDeployment.ts @@ -6,8 +6,8 @@ import { CommonArgs } from "../../args.js"; import type { ConnectionMetadata } from "../../../telemetry/types.js"; export class ConnectDeploymentTool extends AtlasLocalToolBase { - public name = "atlas-local-connect-deployment"; - protected description = "Connect to a MongoDB Atlas Local deployment"; + public internalName = "atlas-local-connect-deployment"; + protected internalDescription = "Connect to a MongoDB Atlas Local deployment"; public operationType: OperationType = "connect"; protected argsShape = { deploymentName: CommonArgs.string().describe("Name of the deployment to connect to"), diff --git a/src/tools/atlasLocal/create/createDeployment.ts b/src/tools/atlasLocal/create/createDeployment.ts index 54f28e8af..7ae104c5f 100644 --- a/src/tools/atlasLocal/create/createDeployment.ts +++ b/src/tools/atlasLocal/create/createDeployment.ts @@ -5,8 +5,8 @@ import type { Client, CreateDeploymentOptions } from "@mongodb-js/atlas-local"; import { CommonArgs } from "../../args.js"; export class CreateDeploymentTool extends AtlasLocalToolBase { - public name = "atlas-local-create-deployment"; - protected description = "Create a MongoDB Atlas local deployment"; + public internalName = "atlas-local-create-deployment"; + protected internalDescription = "Create a MongoDB Atlas local deployment"; public operationType: OperationType = "create"; protected argsShape = { deploymentName: CommonArgs.string().describe("Name of the deployment to create").optional(), diff --git a/src/tools/atlasLocal/delete/deleteDeployment.ts b/src/tools/atlasLocal/delete/deleteDeployment.ts index 669a1ab05..661f7a0e8 100644 --- a/src/tools/atlasLocal/delete/deleteDeployment.ts +++ b/src/tools/atlasLocal/delete/deleteDeployment.ts @@ -5,8 +5,8 @@ import type { Client } from "@mongodb-js/atlas-local"; import { CommonArgs } from "../../args.js"; export class DeleteDeploymentTool extends AtlasLocalToolBase { - public name = "atlas-local-delete-deployment"; - protected description = "Delete a MongoDB Atlas local deployment"; + public internalName = "atlas-local-delete-deployment"; + protected internalDescription = "Delete a MongoDB Atlas local deployment"; public operationType: OperationType = "delete"; protected argsShape = { deploymentName: CommonArgs.string().describe("Name of the deployment to delete"), diff --git a/src/tools/atlasLocal/read/listDeployments.ts b/src/tools/atlasLocal/read/listDeployments.ts index 32a541174..0999c0f29 100644 --- a/src/tools/atlasLocal/read/listDeployments.ts +++ b/src/tools/atlasLocal/read/listDeployments.ts @@ -6,8 +6,8 @@ import type { Deployment } from "@mongodb-js/atlas-local"; import type { Client } from "@mongodb-js/atlas-local"; export class ListDeploymentsTool extends AtlasLocalToolBase { - public name = "atlas-local-list-deployments"; - protected description = "List MongoDB Atlas local deployments"; + public internalName = "atlas-local-list-deployments"; + protected internalDescription = "List MongoDB Atlas local deployments"; public operationType: OperationType = "read"; protected argsShape = {}; diff --git a/src/tools/mongodb/connect/connect.ts b/src/tools/mongodb/connect/connect.ts index 87219d0db..17c3885dd 100644 --- a/src/tools/mongodb/connect/connect.ts +++ b/src/tools/mongodb/connect/connect.ts @@ -32,8 +32,9 @@ const disconnectedDescription = "Connect to a MongoDB instance. The config resource captures if the server is already connected to a MongoDB cluster. If the user has configured a connection string or has previously called the connect tool, a connection is already established and there's no need to call this tool unless the user has explicitly requested to switch to a new MongoDB cluster."; export class ConnectTool extends MongoDBToolBase { - public name: typeof connectedName | typeof disconnectedName = disconnectedName; - protected description: typeof connectedDescription | typeof disconnectedDescription = disconnectedDescription; + public internalName: typeof connectedName | typeof disconnectedName = disconnectedName; + protected internalDescription: typeof connectedDescription | typeof disconnectedDescription = + disconnectedDescription; // Here the default is empty just to trigger registration, but we're going to override it with the correct // schema in the register method. @@ -55,19 +56,18 @@ export class ConnectTool extends MongoDBToolBase { } protected async execute({ connectionString }: ToolArgs): Promise { - switch (this.name) { - case disconnectedName: - assert(connectionString, "Connection string is required"); - break; - case connectedName: - connectionString ??= this.config.connectionString; - assert( - connectionString, - "Cannot switch to a new connection because no connection string was provided and no default connection string is configured." - ); - break; + const isSwitchConnectionTool = this.name === this.getConfiguredNameFor(connectedName); + if (isSwitchConnectionTool) { + connectionString ??= this.config.connectionString; } + assert( + connectionString, + isSwitchConnectionTool + ? "Cannot switch to a new connection because no connection string was provided and no default connection string is configured." + : "Connection string is required" + ); + await this.session.connectToMongoDB({ connectionString }); this.updateMetadata(); @@ -107,8 +107,8 @@ export class ConnectTool extends MongoDBToolBase { }); this.update?.({ - name, - description, + internalName: name, + internalDescription: description, inputSchema, }); } diff --git a/src/tools/mongodb/create/createCollection.ts b/src/tools/mongodb/create/createCollection.ts index 22f9336f6..4d8e439fb 100644 --- a/src/tools/mongodb/create/createCollection.ts +++ b/src/tools/mongodb/create/createCollection.ts @@ -3,8 +3,8 @@ import { DbOperationArgs, MongoDBToolBase } from "../mongodbTool.js"; import type { OperationType, ToolArgs } from "../../tool.js"; export class CreateCollectionTool extends MongoDBToolBase { - public name = "create-collection"; - protected description = + public internalName = "create-collection"; + protected internalDescription = "Creates a new collection in a database. If the database doesn't exist, it will be created automatically."; protected argsShape = DbOperationArgs; diff --git a/src/tools/mongodb/create/createIndex.ts b/src/tools/mongodb/create/createIndex.ts index fcbc12ee7..29dab2a78 100644 --- a/src/tools/mongodb/create/createIndex.ts +++ b/src/tools/mongodb/create/createIndex.ts @@ -64,8 +64,8 @@ export class CreateIndexTool extends MongoDBToolBase { ), }); - public name = "create-index"; - protected description = "Create an index for a collection"; + public internalName = "create-index"; + protected internalDescription = "Create an index for a collection"; protected argsShape = { ...DbOperationArgs, name: z.string().optional().describe("The name of the index"), diff --git a/src/tools/mongodb/create/insertMany.ts b/src/tools/mongodb/create/insertMany.ts index fcde13164..bfc54172b 100644 --- a/src/tools/mongodb/create/insertMany.ts +++ b/src/tools/mongodb/create/insertMany.ts @@ -16,8 +16,8 @@ const zSupportedEmbeddingParametersWithInput = zSupportedEmbeddingParameters.ext }); export class InsertManyTool extends MongoDBToolBase { - public name = "insert-many"; - protected description = "Insert an array of documents into a MongoDB collection"; + public internalName = "insert-many"; + protected internalDescription = "Insert an array of documents into a MongoDB collection"; protected argsShape = { ...DbOperationArgs, documents: z diff --git a/src/tools/mongodb/delete/deleteMany.ts b/src/tools/mongodb/delete/deleteMany.ts index 835cbb4ab..c76ade114 100644 --- a/src/tools/mongodb/delete/deleteMany.ts +++ b/src/tools/mongodb/delete/deleteMany.ts @@ -6,8 +6,8 @@ import { EJSON } from "bson"; import { zEJSON } from "../../args.js"; export class DeleteManyTool extends MongoDBToolBase { - public name = "delete-many"; - protected description = "Removes all documents that match the filter from a MongoDB collection"; + public internalName = "delete-many"; + protected internalDescription = "Removes all documents that match the filter from a MongoDB collection"; protected argsShape = { ...DbOperationArgs, filter: zEJSON() diff --git a/src/tools/mongodb/delete/dropCollection.ts b/src/tools/mongodb/delete/dropCollection.ts index 50bd008a7..f81fb6ee9 100644 --- a/src/tools/mongodb/delete/dropCollection.ts +++ b/src/tools/mongodb/delete/dropCollection.ts @@ -3,8 +3,8 @@ import { DbOperationArgs, MongoDBToolBase } from "../mongodbTool.js"; import type { ToolArgs, OperationType } from "../../tool.js"; export class DropCollectionTool extends MongoDBToolBase { - public name = "drop-collection"; - protected description = + public internalName = "drop-collection"; + protected internalDescription = "Removes a collection or view from the database. The method also removes any indexes associated with the dropped collection."; protected argsShape = { ...DbOperationArgs, diff --git a/src/tools/mongodb/delete/dropDatabase.ts b/src/tools/mongodb/delete/dropDatabase.ts index d33682ce3..ee0605354 100644 --- a/src/tools/mongodb/delete/dropDatabase.ts +++ b/src/tools/mongodb/delete/dropDatabase.ts @@ -3,8 +3,8 @@ import { DbOperationArgs, MongoDBToolBase } from "../mongodbTool.js"; import type { ToolArgs, OperationType } from "../../tool.js"; export class DropDatabaseTool extends MongoDBToolBase { - public name = "drop-database"; - protected description = "Removes the specified database, deleting the associated data files"; + public internalName = "drop-database"; + protected internalDescription = "Removes the specified database, deleting the associated data files"; protected argsShape = { database: DbOperationArgs.database, }; diff --git a/src/tools/mongodb/delete/dropIndex.ts b/src/tools/mongodb/delete/dropIndex.ts index a6f28d0db..15672baa8 100644 --- a/src/tools/mongodb/delete/dropIndex.ts +++ b/src/tools/mongodb/delete/dropIndex.ts @@ -5,8 +5,8 @@ import { DbOperationArgs, MongoDBToolBase } from "../mongodbTool.js"; import { type ToolArgs, type OperationType, formatUntrustedData } from "../../tool.js"; export class DropIndexTool extends MongoDBToolBase { - public name = "drop-index"; - protected description = "Drop an index for the provided database and collection."; + public internalName = "drop-index"; + protected internalDescription = "Drop an index for the provided database and collection."; protected argsShape = { ...DbOperationArgs, indexName: z.string().nonempty().describe("The name of the index to be dropped."), diff --git a/src/tools/mongodb/metadata/collectionIndexes.ts b/src/tools/mongodb/metadata/collectionIndexes.ts index 19007c4f1..95eadcee8 100644 --- a/src/tools/mongodb/metadata/collectionIndexes.ts +++ b/src/tools/mongodb/metadata/collectionIndexes.ts @@ -17,8 +17,8 @@ type IndexStatus = { }; export class CollectionIndexesTool extends MongoDBToolBase { - public name = "collection-indexes"; - protected description = "Describe the indexes for a collection"; + public internalName = "collection-indexes"; + protected internalDescription = "Describe the indexes for a collection"; protected argsShape = DbOperationArgs; public operationType: OperationType = "metadata"; diff --git a/src/tools/mongodb/metadata/collectionSchema.ts b/src/tools/mongodb/metadata/collectionSchema.ts index ad74e9e74..f021cee32 100644 --- a/src/tools/mongodb/metadata/collectionSchema.ts +++ b/src/tools/mongodb/metadata/collectionSchema.ts @@ -11,8 +11,8 @@ import { isObjectEmpty } from "../../../helpers/isObjectEmpty.js"; const MAXIMUM_SAMPLE_SIZE_HARD_LIMIT = 50_000; export class CollectionSchemaTool extends MongoDBToolBase { - public name = "collection-schema"; - protected description = "Describe the schema for a collection"; + public internalName = "collection-schema"; + protected internalDescription = "Describe the schema for a collection"; protected argsShape = { ...DbOperationArgs, sampleSize: z.number().optional().default(50).describe("Number of documents to sample for schema inference"), diff --git a/src/tools/mongodb/metadata/collectionStorageSize.ts b/src/tools/mongodb/metadata/collectionStorageSize.ts index c38ccc076..c4215433d 100644 --- a/src/tools/mongodb/metadata/collectionStorageSize.ts +++ b/src/tools/mongodb/metadata/collectionStorageSize.ts @@ -3,8 +3,8 @@ import { DbOperationArgs, MongoDBToolBase } from "../mongodbTool.js"; import type { ToolArgs, OperationType } from "../../tool.js"; export class CollectionStorageSizeTool extends MongoDBToolBase { - public name = "collection-storage-size"; - protected description = "Gets the size of the collection"; + public internalName = "collection-storage-size"; + protected internalDescription = "Gets the size of the collection"; protected argsShape = DbOperationArgs; public operationType: OperationType = "metadata"; diff --git a/src/tools/mongodb/metadata/dbStats.ts b/src/tools/mongodb/metadata/dbStats.ts index 830df410f..d2ec2f92c 100644 --- a/src/tools/mongodb/metadata/dbStats.ts +++ b/src/tools/mongodb/metadata/dbStats.ts @@ -5,8 +5,8 @@ import { formatUntrustedData } from "../../tool.js"; import { EJSON } from "bson"; export class DbStatsTool extends MongoDBToolBase { - public name = "db-stats"; - protected description = "Returns statistics that reflect the use state of a single database"; + public internalName = "db-stats"; + protected internalDescription = "Returns statistics that reflect the use state of a single database"; protected argsShape = { database: DbOperationArgs.database, }; diff --git a/src/tools/mongodb/metadata/explain.ts b/src/tools/mongodb/metadata/explain.ts index a98d4f6e1..538162e6b 100644 --- a/src/tools/mongodb/metadata/explain.ts +++ b/src/tools/mongodb/metadata/explain.ts @@ -9,8 +9,8 @@ import { FindArgs } from "../read/find.js"; import { CountArgs } from "../read/count.js"; export class ExplainTool extends MongoDBToolBase { - public name = "explain"; - protected description = + public internalName = "explain"; + protected internalDescription = "Returns statistics describing the execution of the winning plan chosen by the query optimizer for the evaluated method"; protected argsShape = { diff --git a/src/tools/mongodb/metadata/listCollections.ts b/src/tools/mongodb/metadata/listCollections.ts index fb879cadc..ca032b1c6 100644 --- a/src/tools/mongodb/metadata/listCollections.ts +++ b/src/tools/mongodb/metadata/listCollections.ts @@ -4,8 +4,8 @@ import type { ToolArgs, OperationType } from "../../tool.js"; import { formatUntrustedData } from "../../tool.js"; export class ListCollectionsTool extends MongoDBToolBase { - public name = "list-collections"; - protected description = "List all collections for a given database"; + public internalName = "list-collections"; + protected internalDescription = "List all collections for a given database"; protected argsShape = { database: DbOperationArgs.database, }; diff --git a/src/tools/mongodb/metadata/listDatabases.ts b/src/tools/mongodb/metadata/listDatabases.ts index e89b25493..1342b389e 100644 --- a/src/tools/mongodb/metadata/listDatabases.ts +++ b/src/tools/mongodb/metadata/listDatabases.ts @@ -5,8 +5,8 @@ import type { OperationType } from "../../tool.js"; import { formatUntrustedData } from "../../tool.js"; export class ListDatabasesTool extends MongoDBToolBase { - public name = "list-databases"; - protected description = "List all databases for a MongoDB connection"; + public internalName = "list-databases"; + protected internalDescription = "List all databases for a MongoDB connection"; protected argsShape = {}; public operationType: OperationType = "metadata"; diff --git a/src/tools/mongodb/metadata/logs.ts b/src/tools/mongodb/metadata/logs.ts index b19fa72c2..0f8e923a0 100644 --- a/src/tools/mongodb/metadata/logs.ts +++ b/src/tools/mongodb/metadata/logs.ts @@ -4,8 +4,8 @@ import { type ToolArgs, type OperationType, formatUntrustedData } from "../../to import { z } from "zod"; export class LogsTool extends MongoDBToolBase { - public name = "mongodb-logs"; - protected description = "Returns the most recent logged mongod events"; + public internalName = "mongodb-logs"; + protected internalDescription = "Returns the most recent logged mongod events"; protected argsShape = { type: z .enum(["global", "startupWarnings"]) diff --git a/src/tools/mongodb/read/aggregate.ts b/src/tools/mongodb/read/aggregate.ts index c7bd439a8..36e389725 100644 --- a/src/tools/mongodb/read/aggregate.ts +++ b/src/tools/mongodb/read/aggregate.ts @@ -48,8 +48,8 @@ Note to LLM: If the entire aggregation result is required, use the "export" tool }) as const; export class AggregateTool extends MongoDBToolBase { - public name = "aggregate"; - protected description = "Run an aggregation against a MongoDB collection"; + public internalName = "aggregate"; + protected internalDescription = "Run an aggregation against a MongoDB collection"; protected argsShape = { ...DbOperationArgs, ...getAggregateArgs(this.isFeatureEnabled("vectorSearch")), diff --git a/src/tools/mongodb/read/count.ts b/src/tools/mongodb/read/count.ts index 435c2c772..3e8f4764a 100644 --- a/src/tools/mongodb/read/count.ts +++ b/src/tools/mongodb/read/count.ts @@ -13,8 +13,8 @@ export const CountArgs = { }; export class CountTool extends MongoDBToolBase { - public name = "count"; - protected description = + public internalName = "count"; + protected internalDescription = "Gets the number of documents in a MongoDB collection using db.collection.count() and query as an optional filter parameter"; protected argsShape = { ...DbOperationArgs, diff --git a/src/tools/mongodb/read/export.ts b/src/tools/mongodb/read/export.ts index a12ed9fba..2a13ce465 100644 --- a/src/tools/mongodb/read/export.ts +++ b/src/tools/mongodb/read/export.ts @@ -9,8 +9,8 @@ import { jsonExportFormat } from "../../../common/exportsManager.js"; import { getAggregateArgs } from "./aggregate.js"; export class ExportTool extends MongoDBToolBase { - public name = "export"; - protected description = "Export a query or aggregation results in the specified EJSON format."; + public internalName = "export"; + protected internalDescription = "Export a query or aggregation results in the specified EJSON format."; protected argsShape = { ...DbOperationArgs, exportTitle: z.string().describe("A short description to uniquely identify the export."), diff --git a/src/tools/mongodb/read/find.ts b/src/tools/mongodb/read/find.ts index eb006f335..40916261f 100644 --- a/src/tools/mongodb/read/find.ts +++ b/src/tools/mongodb/read/find.ts @@ -36,8 +36,8 @@ Note to LLM: If the entire query result is required, use the "export" tool inste }; export class FindTool extends MongoDBToolBase { - public name = "find"; - protected description = "Run a find query against a MongoDB collection"; + public internalName = "find"; + protected internalDescription = "Run a find query against a MongoDB collection"; protected argsShape = { ...DbOperationArgs, ...FindArgs, diff --git a/src/tools/mongodb/update/renameCollection.ts b/src/tools/mongodb/update/renameCollection.ts index 4992a3227..1da70175e 100644 --- a/src/tools/mongodb/update/renameCollection.ts +++ b/src/tools/mongodb/update/renameCollection.ts @@ -4,8 +4,8 @@ import { DbOperationArgs, MongoDBToolBase } from "../mongodbTool.js"; import type { ToolArgs, OperationType } from "../../tool.js"; export class RenameCollectionTool extends MongoDBToolBase { - public name = "rename-collection"; - protected description = "Renames a collection in a MongoDB database"; + public internalName = "rename-collection"; + protected internalDescription = "Renames a collection in a MongoDB database"; protected argsShape = { ...DbOperationArgs, newName: z.string().describe("The new name for the collection"), diff --git a/src/tools/mongodb/update/updateMany.ts b/src/tools/mongodb/update/updateMany.ts index 9d936757f..1b5b6f63c 100644 --- a/src/tools/mongodb/update/updateMany.ts +++ b/src/tools/mongodb/update/updateMany.ts @@ -6,8 +6,8 @@ import { checkIndexUsage } from "../../../helpers/indexCheck.js"; import { zEJSON } from "../../args.js"; export class UpdateManyTool extends MongoDBToolBase { - public name = "update-many"; - protected description = "Updates all documents that match the specified filter for a collection"; + public internalName = "update-many"; + protected internalDescription = "Updates all documents that match the specified filter for a collection"; protected argsShape = { ...DbOperationArgs, filter: zEJSON() diff --git a/src/tools/tool.ts b/src/tools/tool.ts index 6917020eb..e666aeeed 100644 --- a/src/tools/tool.ts +++ b/src/tools/tool.ts @@ -47,13 +47,41 @@ export type ToolConstructorParams = { }; export abstract class ToolBase { - public abstract name: string; + /** + * The internal / original name of the tool. This is the name that OSS + * MongoDB MCP server identifies the tool with. + */ + public abstract internalName: string; + + /** + * The name with which the tool is registered with the MCP server. This + * could be: + * 1. The overridden name specified by `UserConfig.toolMetadataOverrides` + * 2. or, the internalName of the tool + */ + public get name(): string { + return this.config.toolMetadataOverrides?.[this.internalName]?.name ?? this.internalName; + } public abstract category: ToolCategory; public abstract operationType: OperationType; - protected abstract description: string; + /** + * The internal / original description of the tool. + */ + protected abstract internalDescription: string; + + /** + * The description with which the tool is registered with the MCP server. + * This could be: + * 1. The overridden description specified by + * `UserConfig.toolMetadataOverrides` + * 2. or, the internalDescription of the tool + */ + public get description(): string { + return this.config.toolMetadataOverrides?.[this.internalName]?.description ?? this.internalDescription; + } protected abstract argsShape: ZodRawShape; @@ -168,14 +196,28 @@ export abstract class ToolBase { } }; + /** + * Note: We register the tool using `Tool.name` and `Tool.description` + * instead of `Tool.internalName` and `Tool.internalDescription` to + * account for overridden metadata provided by the user through + * `UserConfig.toolMetadataOverrides`. + */ server.mcpServer.tool(this.name, this.description, this.argsShape, this.annotations, callback); - // This is very similar to RegisteredTool.update, but without the bugs around the name. - // In the upstream update method, the name is captured in the closure and not updated when - // the tool name changes. This means that you only get one name update before things end up - // in a broken state. - // See https://github.com/modelcontextprotocol/typescript-sdk/issues/414 for more details. - this.update = (updates: { name?: string; description?: string; inputSchema?: AnyZodObject }): void => { + /** + * This is very similar to RegisteredTool.update, but without the bugs + * around the name. In the upstream update method, the name is captured + * in the closure and not updated when the tool name changes. This means + * that you only get one name update before things end up in a broken + * state. See + * https://github.com/modelcontextprotocol/typescript-sdk/issues/414 for + * more details. + */ + this.update = (updates: { + internalName: string; + internalDescription?: string; + inputSchema?: AnyZodObject; + }): void => { const tools = server.mcpServer["_registeredTools"] as { [toolName: string]: RegisteredTool }; const existingTool = tools[this.name]; @@ -191,16 +233,18 @@ export abstract class ToolBase { existingTool.annotations = this.annotations; - if (updates.name && updates.name !== this.name) { - existingTool.annotations.title = updates.name; + if (updates.internalName && updates.internalName !== this.internalName) { + existingTool.annotations.title = this.getConfiguredNameFor(updates.internalName); delete tools[this.name]; - this.name = updates.name; + this.internalName = updates.internalName; tools[this.name] = existingTool; } - if (updates.description) { - existingTool.description = updates.description; - this.description = updates.description; + if (updates.internalDescription) { + const descriptionToBeUsed = + this.getConfiguredDescriptionFor(updates.internalName) ?? updates.internalDescription; + existingTool.description = descriptionToBeUsed; + this.internalDescription = descriptionToBeUsed; } if (updates.inputSchema) { @@ -213,7 +257,19 @@ export abstract class ToolBase { return true; } - protected update?: (updates: { name?: string; description?: string; inputSchema?: AnyZodObject }) => void; + /** + * Updates the registered tool's metadata using the provided metadata while + * taking into account the configured overrides provided through + * `UserConfig.toolMetadataOverrides`. The caller of this function is + * expected to call the method with the internal name and internal + * description so that the correct overrides can be figured out during + * update. + */ + protected update?: (updates: { + internalName: string; + internalDescription?: string; + inputSchema?: AnyZodObject; + }) => void; // Checks if a tool is allowed to run based on the config protected verifyAllowed(): boolean { @@ -315,6 +371,14 @@ export abstract class ToolBase { return metadata; } + + protected getConfiguredNameFor(internalToolName: string): string { + return this.config.toolMetadataOverrides?.[internalToolName]?.name ?? internalToolName; + } + + protected getConfiguredDescriptionFor(internalToolName: string): string | undefined { + return this.config.toolMetadataOverrides?.[internalToolName]?.description; + } } /** diff --git a/tests/fixtures/config-with-invalid-value.json b/tests/fixtures/config-with-invalid-value.json new file mode 100644 index 000000000..128b3d181 --- /dev/null +++ b/tests/fixtures/config-with-invalid-value.json @@ -0,0 +1,4 @@ +{ + "connectionString": "mongodb://invalid-value-json-localhost:1000", + "loggers": "stderr,stderr" +} diff --git a/tests/fixtures/valid-config.json b/tests/fixtures/valid-config.json new file mode 100644 index 000000000..e364276c9 --- /dev/null +++ b/tests/fixtures/valid-config.json @@ -0,0 +1,4 @@ +{ + "connectionString": "mongodb://valid-json-localhost:1000", + "loggers": "stderr" +} diff --git a/tests/integration/customTools.test.ts b/tests/integration/customTools.test.ts index 955999d35..04995383b 100644 --- a/tests/integration/customTools.test.ts +++ b/tests/integration/customTools.test.ts @@ -79,10 +79,10 @@ describe("Custom Tools", () => { * Example custom tool that can be provided by library consumers */ class CustomGreetingTool extends ToolBase { - name = "custom_greeting"; + internalName = "custom_greeting"; category = "mongodb" as const; operationType = "read" as const; - protected description = "A custom tool that greets the user"; + protected internalDescription = "A custom tool that greets the user"; protected argsShape = { name: z.string().describe("The name to greet"), }; @@ -107,10 +107,10 @@ class CustomGreetingTool extends ToolBase { * Another example custom tool that performs a calculation */ class CustomCalculatorTool extends ToolBase { - name = "custom_calculator"; + internalName = "custom_calculator"; category = "mongodb" as const; operationType = "read" as const; - protected description = "A custom tool that performs calculations"; + protected internalDescription = "A custom tool that performs calculations"; protected argsShape = { a: z.number().describe("First number"), b: z.number().describe("Second number"), diff --git a/tests/integration/server.test.ts b/tests/integration/server.test.ts index 2c362a963..4f53ff883 100644 --- a/tests/integration/server.test.ts +++ b/tests/integration/server.test.ts @@ -1,6 +1,69 @@ -import { defaultTestConfig, expectDefined } from "./helpers.js"; +import { MCPConnectionManager } from "../../src/common/connectionManager.js"; +import { ExportsManager } from "../../src/common/exportsManager.js"; +import { CompositeLogger } from "../../src/common/logger.js"; +import { DeviceId } from "../../src/helpers/deviceId.js"; +import { Session } from "../../src/common/session.js"; +import { + defaultTestConfig, + expectDefined, + validateThrowsForInvalidArguments, + validateToolMetadata, +} from "./helpers.js"; import { describeWithMongoDB } from "./tools/mongodb/mongodbHelpers.js"; import { describe, expect, it } from "vitest"; +import { Elicitation, Keychain, Telemetry } from "../../src/lib.js"; +import { VectorSearchEmbeddingsManager } from "../../src/common/search/vectorSearchEmbeddingsManager.js"; +import { defaultCreateAtlasLocalClient } from "../../src/common/atlasLocal.js"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { Server } from "../../src/server.js"; +import { connectionErrorHandler } from "../../src/common/connectionErrorHandler.js"; +import { InMemoryTransport } from "./inMemoryTransport.js"; +import type { UserConfig } from "../../src/common/config/userConfig.js"; +import { type OperationType, ToolBase, type ToolCategory } from "../../src/tools/tool.js"; +import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import type { TelemetryToolMetadata } from "../../src/telemetry/types.js"; +import { AllTools } from "../../src/tools/index.js"; +class TestToolOne extends ToolBase { + public internalName = "test-tool-one"; + public category: ToolCategory = "mongodb"; + public operationType: OperationType = "delete"; + protected internalDescription = "A test tool one for verification tests"; + protected argsShape = {}; + protected async execute(): Promise { + return Promise.resolve({ + content: [ + { + type: "text", + text: "Test tool executed successfully", + }, + ], + }); + } + protected resolveTelemetryMetadata(): TelemetryToolMetadata { + return {}; + } +} + +class TestToolTwo extends ToolBase { + public internalName = "test-tool-two"; + public category: ToolCategory = "mongodb"; + public operationType: OperationType = "delete"; + protected internalDescription = "A test tool two for verification tests"; + protected argsShape = {}; + protected async execute(): Promise { + return Promise.resolve({ + content: [ + { + type: "text", + text: "Test tool executed successfully", + }, + ], + }); + } + protected resolveTelemetryMetadata(): TelemetryToolMetadata { + return {}; + } +} describe("Server integration test", () => { describeWithMongoDB( @@ -97,4 +160,122 @@ describe("Server integration test", () => { }), } ); + + describeWithMongoDB( + "Tool with overridden metadata", + (integration) => { + validateToolMetadata(integration, "new-connect", "new connect tool description", "connect", [ + { + name: "connectionString", + description: "MongoDB connection string (in the mongodb:// or mongodb+srv:// format)", + type: "string", + required: true, + }, + ]); + + validateThrowsForInvalidArguments(integration, "new-connect", [{}, { connectionString: 123 }]); + + it("should not have overridden connect tool registered", async () => { + const { tools } = await integration.mcpClient().listTools(); + const tool = tools.find((tool) => tool.name === "connect"); + expect(tool).toBeUndefined(); + }); + }, + { + getUserConfig() { + return { + ...defaultTestConfig, + toolMetadataOverrides: { + connect: { + name: "new-connect", + description: "new connect tool description", + }, + }, + }; + }, + } + ); + + describe("when toolMetadataOverrides leads to tool name collision", () => { + it.each([ + { + config: { + toolMetadataOverrides: { + "list-databases": { name: "my-tool" }, + "list-collections": { name: "my-tool" }, + }, + }, + additionalTools: [], + }, + { + config: { + toolMetadataOverrides: { + "list-databases": { name: "list-collections" }, + }, + }, + additionalTools: [], + }, + { + config: { + toolMetadataOverrides: { + "test-tool-one": { + name: "connect", + }, + }, + }, + additionalTools: [TestToolOne, TestToolTwo], + }, + { + config: { + toolMetadataOverrides: { + "test-tool-one": { + name: "test-tool-two", + }, + }, + }, + additionalTools: [TestToolOne, TestToolTwo], + }, + ] as { config: Partial; additionalTools: (new () => ToolBase)[] }[])( + "should throw an error when tool names collide due to overrides - %", + async ({ config, additionalTools }) => { + const userConfig: UserConfig = { + ...defaultTestConfig, + ...config, + }; + const logger = new CompositeLogger(); + const deviceId = DeviceId.create(logger); + const connectionManager = new MCPConnectionManager(userConfig, logger, deviceId); + const exportsManager = ExportsManager.init(userConfig, logger); + + const session = new Session({ + userConfig: userConfig, + logger, + exportsManager, + connectionManager, + keychain: Keychain.root, + vectorSearchEmbeddingsManager: new VectorSearchEmbeddingsManager(userConfig, connectionManager), + atlasLocalClient: await defaultCreateAtlasLocalClient(), + }); + + const telemetry = Telemetry.create(session, userConfig, deviceId); + const mcpServerInstance = new McpServer({ name: "test", version: "1.0" }); + const elicitation = new Elicitation({ server: mcpServerInstance.server }); + + const server = new Server({ + session, + userConfig: userConfig, + telemetry, + mcpServer: mcpServerInstance, + elicitation, + connectionErrorHandler, + tools: [...AllTools, ...additionalTools], + }); + + const transport = new InMemoryTransport(); + + // We expect this to fail with our new guardrail + await expect(server.connect(transport)).rejects.toThrow(/Tool name collision detected/); + } + ); + }); }); diff --git a/tests/integration/tools/mongodb/connect/connect.test.ts b/tests/integration/tools/mongodb/connect/connect.test.ts index c197c63a9..cfb35010c 100644 --- a/tests/integration/tools/mongodb/connect/connect.test.ts +++ b/tests/integration/tools/mongodb/connect/connect.test.ts @@ -90,6 +90,70 @@ describeWithMongoDB( } ); +describeWithMongoDB( + "SwitchConnection tool with overridden metadata", + (integration) => { + beforeEach(async () => { + await integration.mcpServer().session.connectToMongoDB({ + connectionString: integration.connectionString(), + }); + }); + + validateToolMetadata(integration, "new-switch-connection", "new description", "connect", [ + { + name: "connectionString", + description: "MongoDB connection string to switch to (in the mongodb:// or mongodb+srv:// format)", + type: "string", + required: false, + }, + ]); + + validateThrowsForInvalidArguments(integration, "new-switch-connection", [{ connectionString: 123 }]); + + it("should not contain overridden tools", async () => { + const { tools } = await integration.mcpClient().listTools(); + const tool = tools.find((tool) => tool.name === "switch-connection"); + expect(tool).toBeUndefined(); + }); + + it("should not contain connect tool", async () => { + const { tools } = await integration.mcpClient().listTools(); + const tool = tools.find((tool) => tool.name === "connect"); + expect(tool).toBeUndefined(); + }); + + it("connects to the configured connection", async () => { + const response = await integration.mcpClient().callTool({ name: "new-switch-connection" }); + const content = getResponseContent(response.content); + expect(content).toContain("Successfully connected"); + }); + + it("connects to the provided connection", async () => { + const newConnectionString = `${integration.connectionString()}?appName=foo-bar`; + const response = await integration.mcpClient().callTool({ + name: "new-switch-connection", + arguments: { + connectionString: newConnectionString, + }, + }); + const content = getResponseContent(response.content); + expect(content).toContain("Successfully connected"); + }); + }, + { + getUserConfig: (mdbIntegration) => ({ + ...defaultTestConfig, + connectionString: mdbIntegration.connectionString(), + toolMetadataOverrides: { + "switch-connection": { + name: "new-switch-connection", + description: "new description", + }, + }, + }), + } +); + describeWithMongoDB( "SwitchConnection tool when server is configured to connect with complex connection", (integration) => { @@ -234,3 +298,113 @@ describeWithMongoDB( }), } ); + +describeWithMongoDB( + "Connect tool with overridden metadata", + (integration) => { + validateToolMetadata(integration, "new-connect", "new connect tool description", "connect", [ + { + name: "connectionString", + description: "MongoDB connection string (in the mongodb:// or mongodb+srv:// format)", + type: "string", + required: true, + }, + ]); + + validateThrowsForInvalidArguments(integration, "new-connect", [{}, { connectionString: 123 }]); + + it("should not have switch-connection tool registered", async () => { + const { tools } = await integration.mcpClient().listTools(); + const tool = tools.find((tool) => tool.name === "switch-connection"); + expect(tool).toBeUndefined(); + }); + + it("should not have overridden connect tool registered", async () => { + const { tools } = await integration.mcpClient().listTools(); + const tool = tools.find((tool) => tool.name === "connect"); + expect(tool).toBeUndefined(); + }); + + it("should be able to work the same as connect tool", async () => { + const response = await integration.mcpClient().callTool({ + name: "new-connect", + arguments: { + connectionString: integration.connectionString(), + }, + }); + const content = getResponseContent(response.content); + expect(content).toContain("Successfully connected"); + }); + }, + { + getUserConfig() { + return { + ...defaultTestConfig, + toolMetadataOverrides: { + connect: { + name: "new-connect", + description: "new connect tool description", + }, + }, + }; + }, + } +); + +describeWithMongoDB( + "with both connect and switch-connection metadata overridden", + (integration) => { + it("should be able to connect and switch connection using overridden tool metadata", async () => { + // Original switch-connection and connect are not there + let { tools } = await integration.mcpClient().listTools(); + let switchConnection = tools.find((tool) => tool.name === "switch-connection"); + let connect = tools.find((tool) => tool.name === "connect"); + expect(switchConnection).toBeUndefined(); + expect(connect).toBeUndefined(); + + // Establish connection + let response = await integration.mcpClient().callTool({ + name: "new-connect", + arguments: { + connectionString: integration.connectionString(), + }, + }); + let content = getResponseContent(response.content); + expect(content).toContain("Successfully connected"); + + // Now check again + ({ tools } = await integration.mcpClient().listTools()); + switchConnection = tools.find((tool) => tool.name === "switch-connection"); + connect = tools.find((tool) => tool.name === "connect"); + expect(switchConnection).toBeUndefined(); + expect(connect).toBeUndefined(); + + // Switch using new tool + response = await integration.mcpClient().callTool({ + name: "new-switch-connection", + arguments: { + connectionString: `${integration.connectionString()}?appName=foo-bar`, + }, + }); + content = getResponseContent(response.content); + expect(content).toContain("Successfully connected"); + }); + }, + { + getUserConfig() { + return { + ...defaultTestConfig, + toolMetadataOverrides: { + connect: { + name: "new-connect", + description: "new connect tool description", + }, + "switch-connection": { + name: "new-switch-connection", + description: "new switch connection tool description", + }, + }, + }; + }, + } +); diff --git a/tests/integration/tools/mongodb/create/createIndex.test.ts b/tests/integration/tools/mongodb/create/createIndex.test.ts index ee344b7d5..2e0135c3d 100644 --- a/tests/integration/tools/mongodb/create/createIndex.test.ts +++ b/tests/integration/tools/mongodb/create/createIndex.test.ts @@ -510,7 +510,7 @@ describeWithMongoDB( expect(indexes).toHaveLength(1); expect(indexes[0]?.name).toEqual("vector_1_vector"); expect(indexes[0]?.type).toEqual("vectorSearch"); - expect(indexes[0]?.status).toEqual("PENDING"); + expect(indexes[0]?.status).toEqual(expect.stringMatching(/PENDING|BUILDING/)); expect(indexes[0]?.queryable).toEqual(false); expect(indexes[0]?.latestDefinition).toEqual({ fields: [ diff --git a/tests/integration/tools/mongodb/mongodbTool.test.ts b/tests/integration/tools/mongodb/mongodbTool.test.ts index 56fd86bd3..c9fc9a223 100644 --- a/tests/integration/tools/mongodb/mongodbTool.test.ts +++ b/tests/integration/tools/mongodb/mongodbTool.test.ts @@ -54,9 +54,9 @@ const injectedErrorHandler: ConnectionErrorHandler = (error) => { }; class RandomTool extends MongoDBToolBase { - name = "Random"; + internalName = "Random"; operationType: OperationType = "read"; - protected description = "This is a tool."; + protected internalDescription = "This is a tool."; protected argsShape = {}; public async execute(): Promise { await this.ensureConnected(); @@ -65,9 +65,9 @@ class RandomTool extends MongoDBToolBase { } class UnusableVoyageTool extends MongoDBToolBase { - name = "UnusableVoyageTool"; + internalName = "UnusableVoyageTool"; operationType: OperationType = "read"; - protected description = "This is a Voyage tool."; + protected internalDescription = "This is a Voyage tool."; protected argsShape = {}; override verifyAllowed(): boolean { diff --git a/tests/unit/common/config.test.ts b/tests/unit/common/config.test.ts index 372445c79..8c6a5ff08 100644 --- a/tests/unit/common/config.test.ts +++ b/tests/unit/common/config.test.ts @@ -1,3 +1,4 @@ +import path from "path"; import { describe, it, expect, vi, beforeEach, afterEach, type MockedFunction } from "vitest"; import { type UserConfig, UserConfigSchema } from "../../../src/common/config/userConfig.js"; import { type CreateUserConfigHelpers, createUserConfig } from "../../../src/common/config/createUserConfig.js"; @@ -25,81 +26,54 @@ function createEnvironment(): { }; } +const CONFIG_FIXTURES = { + VALID: path.resolve(import.meta.dirname, "..", "..", "fixtures", "valid-config.json"), + WITH_INVALID_VALUE: path.resolve(import.meta.dirname, "..", "..", "fixtures", "config-with-invalid-value.json"), +}; + +// Expected hardcoded values (what we had before) +const expectedDefaults = { + apiBaseUrl: "https://cloud.mongodb.com/", + logPath: getLogPath(), + exportsPath: getExportsPath(), + exportTimeoutMs: 5 * 60 * 1000, // 5 minutes + exportCleanupIntervalMs: 2 * 60 * 1000, // 2 minutes + disabledTools: [], + telemetry: "enabled", + readOnly: false, + indexCheck: false, + confirmationRequiredTools: [ + "atlas-create-access-list", + "atlas-create-db-user", + "drop-database", + "drop-collection", + "delete-many", + "drop-index", + ], + transport: "stdio", + httpPort: 3000, + httpHost: "127.0.0.1", + loggers: ["disk", "mcp"], + idleTimeoutMs: 10 * 60 * 1000, // 10 minutes + notificationTimeoutMs: 9 * 60 * 1000, // 9 minutes + httpHeaders: {}, + maxDocumentsPerQuery: 100, + maxBytesPerQuery: 16 * 1024 * 1024, // ~16 mb + atlasTemporaryDatabaseUserLifetimeMs: 4 * 60 * 60 * 1000, // 4 hours + voyageApiKey: "", + vectorSearchDimensions: 1024, + vectorSearchSimilarityFunction: "euclidean", + disableEmbeddingsValidation: false, + previewFeatures: [], + toolMetadataOverrides: {}, +}; + describe("config", () => { it("should generate defaults from UserConfigSchema that match expected values", () => { - // Expected hardcoded values (what we had before) - const expectedDefaults = { - apiBaseUrl: "https://cloud.mongodb.com/", - logPath: getLogPath(), - exportsPath: getExportsPath(), - exportTimeoutMs: 5 * 60 * 1000, // 5 minutes - exportCleanupIntervalMs: 2 * 60 * 1000, // 2 minutes - disabledTools: [], - telemetry: "enabled", - readOnly: false, - indexCheck: false, - confirmationRequiredTools: [ - "atlas-create-access-list", - "atlas-create-db-user", - "drop-database", - "drop-collection", - "delete-many", - "drop-index", - ], - transport: "stdio", - httpPort: 3000, - httpHost: "127.0.0.1", - loggers: ["disk", "mcp"], - idleTimeoutMs: 10 * 60 * 1000, // 10 minutes - notificationTimeoutMs: 9 * 60 * 1000, // 9 minutes - httpHeaders: {}, - maxDocumentsPerQuery: 100, - maxBytesPerQuery: 16 * 1024 * 1024, // ~16 mb - atlasTemporaryDatabaseUserLifetimeMs: 4 * 60 * 60 * 1000, // 4 hours - voyageApiKey: "", - vectorSearchDimensions: 1024, - vectorSearchSimilarityFunction: "euclidean", - disableEmbeddingsValidation: false, - previewFeatures: [], - }; expect(UserConfigSchema.parse({})).toStrictEqual(expectedDefaults); }); it("should generate defaults when no config sources are populated", () => { - const expectedDefaults = { - apiBaseUrl: "https://cloud.mongodb.com/", - logPath: getLogPath(), - exportsPath: getExportsPath(), - exportTimeoutMs: 5 * 60 * 1000, // 5 minutes - exportCleanupIntervalMs: 2 * 60 * 1000, // 2 minutes - disabledTools: [], - telemetry: "enabled", - readOnly: false, - indexCheck: false, - confirmationRequiredTools: [ - "atlas-create-access-list", - "atlas-create-db-user", - "drop-database", - "drop-collection", - "delete-many", - "drop-index", - ], - transport: "stdio", - httpPort: 3000, - httpHost: "127.0.0.1", - loggers: ["disk", "mcp"], - idleTimeoutMs: 10 * 60 * 1000, // 10 minutes - notificationTimeoutMs: 9 * 60 * 1000, // 9 minutes - httpHeaders: {}, - maxDocumentsPerQuery: 100, - maxBytesPerQuery: 16 * 1024 * 1024, // ~16 mb - atlasTemporaryDatabaseUserLifetimeMs: 4 * 60 * 60 * 1000, // 4 hours - voyageApiKey: "", - vectorSearchDimensions: 1024, - vectorSearchSimilarityFunction: "euclidean", - disableEmbeddingsValidation: false, - previewFeatures: [], - }; expect(createUserConfig()).toStrictEqual(expectedDefaults); }); @@ -489,6 +463,87 @@ describe("config", () => { }); }); + describe("loading a config file", () => { + let warn: MockedFunction; + let error: MockedFunction; + let exit: MockedFunction; + + beforeEach(() => { + warn = vi.fn(); + error = vi.fn(); + exit = vi.fn(); + }); + + describe("through env variable MDB_MCP_CONFIG", () => { + const { setVariable, clearVariables } = createEnvironment(); + afterEach(() => { + clearVariables(); + }); + + it("should load a valid config file without troubles", () => { + setVariable("MDB_MCP_CONFIG", CONFIG_FIXTURES.VALID); + const config = createUserConfig({ + onWarning: warn, + onError: error, + closeProcess: exit, + }); + expect(warn).not.toBeCalled(); + expect(error).not.toBeCalled(); + expect(exit).not.toBeCalled(); + + expect(config.connectionString).toBe("mongodb://valid-json-localhost:1000"); + expect(config.loggers).toStrictEqual(["stderr"]); + }); + + it("should attempt loading config file with wrong value and exit", () => { + setVariable("MDB_MCP_CONFIG", CONFIG_FIXTURES.WITH_INVALID_VALUE); + createUserConfig({ + onWarning: warn, + onError: error, + closeProcess: exit, + }); + expect(warn).not.toBeCalled(); + expect(error).toBeCalledWith( + expect.stringContaining("Invalid configuration for the following fields:") + ); + expect(error).toBeCalledWith(expect.stringContaining("loggers - Duplicate loggers found in config")); + expect(exit).toBeCalledWith(1); + }); + }); + + describe("through cli argument --config", () => { + it("should load a valid config file without troubles", () => { + const config = createUserConfig({ + onWarning: warn, + onError: error, + closeProcess: exit, + cliArguments: ["--config", CONFIG_FIXTURES.VALID], + }); + expect(warn).not.toBeCalled(); + expect(error).not.toBeCalled(); + expect(exit).not.toBeCalled(); + + expect(config.connectionString).toBe("mongodb://valid-json-localhost:1000"); + expect(config.loggers).toStrictEqual(["stderr"]); + }); + + it("should attempt loading config file with wrong value and exit", () => { + createUserConfig({ + onWarning: warn, + onError: error, + closeProcess: exit, + cliArguments: ["--config", CONFIG_FIXTURES.WITH_INVALID_VALUE], + }); + expect(warn).not.toBeCalled(); + expect(error).toBeCalledWith( + expect.stringContaining("Invalid configuration for the following fields:") + ); + expect(error).toBeCalledWith(expect.stringContaining("loggers - Duplicate loggers found in config")); + expect(exit).toBeCalledWith(1); + }); + }); + }); + describe("precedence rules", () => { const { setVariable, clearVariables } = createEnvironment(); @@ -499,30 +554,34 @@ describe("config", () => { it("positional argument takes precedence over all", () => { setVariable("MDB_MCP_CONNECTION_STRING", "mongodb://crazyhost1"); const actual = createUserConfig({ - cliArguments: ["mongodb://crazyhost2", "--connectionString", "mongodb://localhost"], + cliArguments: [ + "mongodb://crazyhost2", + "--config", + CONFIG_FIXTURES.VALID, + "--connectionString", + "mongodb://localhost", + ], }); expect(actual.connectionString).toBe("mongodb://crazyhost2/?directConnection=true"); }); - it("cli arguments take precedence over env vars", () => { - setVariable("MDB_MCP_CONNECTION_STRING", "mongodb://crazyhost"); + it("any cli argument takes precedence over env vars, config and defaults", () => { + setVariable("MDB_MCP_CONNECTION_STRING", "mongodb://dummyhost"); const actual = createUserConfig({ - cliArguments: ["--connectionString", "mongodb://localhost"], + cliArguments: ["--config", CONFIG_FIXTURES.VALID, "--connectionString", "mongodb://host-from-cli"], }); - expect(actual.connectionString).toBe("mongodb://localhost"); + expect(actual.connectionString).toBe("mongodb://host-from-cli"); }); - it("any cli argument takes precedence over defaults", () => { - const actual = createUserConfig({ - cliArguments: ["--connectionString", "mongodb://localhost"], - }); - expect(actual.connectionString).toBe("mongodb://localhost"); + it("any env var takes precedence over config and defaults", () => { + setVariable("MDB_MCP_CONNECTION_STRING", "mongodb://dummyhost"); + const actual = createUserConfig({ cliArguments: ["--config", CONFIG_FIXTURES.VALID] }); + expect(actual.connectionString).toBe("mongodb://dummyhost"); }); - it("any env var takes precedence over defaults", () => { - setVariable("MDB_MCP_CONNECTION_STRING", "mongodb://localhost"); - const actual = createUserConfig(); - expect(actual.connectionString).toBe("mongodb://localhost"); + it("config file takes precedence over defaults", () => { + const actual = createUserConfig({ cliArguments: ["--config", CONFIG_FIXTURES.VALID] }); + expect(actual.connectionString).toBe("mongodb://valid-json-localhost:1000"); }); }); diff --git a/tests/unit/toolBase.test.ts b/tests/unit/toolBase.test.ts index b88bebae6..547ae6fbf 100644 --- a/tests/unit/toolBase.test.ts +++ b/tests/unit/toolBase.test.ts @@ -262,10 +262,10 @@ describe("ToolBase", () => { }); class TestTool extends ToolBase { - public name = "test-tool"; + public internalName = "test-tool"; public category: ToolCategory = "mongodb"; public operationType: OperationType = "delete"; - protected description = "A test tool for verification tests"; + protected internalDescription = "A test tool for verification tests"; protected argsShape = { param1: z.string().describe("Test parameter 1"), param2: z.number().optional().describe("Test parameter 2"),