From 40e1668a0c61379f4d9c3b0b8998fe99880c2252 Mon Sep 17 00:00:00 2001 From: gagik Date: Fri, 31 Oct 2025 11:01:08 +0100 Subject: [PATCH 01/12] chore: automatic README generation --- README.md | 51 +++++++------ eslint-rules/enforce-zod-v4.js | 54 +++++++++++++ eslint.config.js | 7 ++ scripts/generateArguments.ts | 135 ++++++++++++++++++++++++++------- server.json | 50 +++++++++--- src/common/config.ts | 126 +++++++++++++++++++----------- 6 files changed, 314 insertions(+), 109 deletions(-) create mode 100644 eslint-rules/enforce-zod-v4.js diff --git a/README.md b/README.md index 870ced72e..9b7af8f81 100644 --- a/README.md +++ b/README.md @@ -347,31 +347,32 @@ The MongoDB MCP Server can be configured using multiple methods, with the follow ### 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. | -| `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. | -| `loggers` | `MDB_MCP_LOGGERS` | disk,mcp | Comma separated values, possible values are `mcp`, `disk` and `stderr`. See [Logger Options](#logger-options) for details. | -| `logPath` | `MDB_MCP_LOG_PATH` | see note\* | Folder to store logs. | -| `disabledTools` | `MDB_MCP_DISABLED_TOOLS` | | An array of tool names, operation types, and/or categories of tools that will be disabled. | -| `confirmationRequiredTools` | `MDB_MCP_CONFIRMATION_REQUIRED_TOOLS` | create-access-list,create-db-user,drop-database,drop-collection,delete-many | An array of tool names that require user confirmation before execution. **Requires the client to support [elicitation](https://modelcontextprotocol.io/specification/draft/client/elicitation)**. | -| `readOnly` | `MDB_MCP_READ_ONLY` | false | When set to true, only allows read, connect, and metadata operation types, disabling create/update/delete operations. | -| `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. | -| `telemetry` | `MDB_MCP_TELEMETRY` | enabled | When set to disabled, disables telemetry collection. | -| `transport` | `MDB_MCP_TRANSPORT` | stdio | Either 'stdio' or 'http'. | -| `httpPort` | `MDB_MCP_HTTP_PORT` | 3000 | Port number. | -| `httpHost` | `MDB_MCP_HTTP_HOST` | 127.0.0.1 | Host to bind the http server. | -| `idleTimeoutMs` | `MDB_MCP_IDLE_TIMEOUT_MS` | 600000 | Idle timeout for a client to disconnect (only applies to http transport). | -| `maxBytesPerQuery` | `MDB_MCP_MAX_BYTES_PER_QUERY` | 16777216 (16MiB) | 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 diconnect (only applies to http transport). | -| `exportsPath` | `MDB_MCP_EXPORTS_PATH` | see note\* | Folder to store exported data files. | -| `exportTimeoutMs` | `MDB_MCP_EXPORT_TIMEOUT_MS` | 300000 | Time in milliseconds after which an export is considered expired and eligible for cleanup. | -| `exportCleanupIntervalMs` | `MDB_MCP_EXPORT_CLEANUP_INTERVAL_MS` | 120000 | Time in milliseconds between export cleanup cycles that remove expired export files. | -| `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. | -| ([preview](#opting-into-preview-features)) `voyageApiKey` | `MDB_VOYAGE_API_KEY` | | API key for communicating with Voyage AI. Used for generating embeddings for Vector search. **This feature is in preview and requires opting into the `vectorSearch` preview feature**. | -| `previewFeatures` | `MDB_MCP_PREVIEW_FEATURES` | `[]` | An array of preview features to opt into. | +| 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 diff --git a/eslint-rules/enforce-zod-v4.js b/eslint-rules/enforce-zod-v4.js new file mode 100644 index 000000000..6d9accba9 --- /dev/null +++ b/eslint-rules/enforce-zod-v4.js @@ -0,0 +1,54 @@ +"use strict"; +import path from "path"; + +// The file that is allowed to import from zod/v4 +const configFilePath = path.resolve(import.meta.dirname, "../src/common/config.ts"); + +// Ref: https://eslint.org/docs/latest/extend/custom-rules +export default { + meta: { + type: "problem", + docs: { + description: "Only allow importing 'zod/v4' in config.ts, all other imports are allowed elsewhere.", + recommended: true, + }, + fixable: null, + messages: { + enforceZodV4: + "Only 'zod/v4' imports are allowed in config.ts. Found import from '{{importPath}}'. Use 'zod/v4' instead.", + }, + }, + create(context) { + const currentFilePath = path.resolve(context.getFilename()); + + // Only enforce rule in config.ts + if (currentFilePath !== configFilePath) { + return {}; + } + + return { + ImportDeclaration(node) { + const importPath = node.source.value; + + // Check if this is a zod import + if (typeof importPath !== "string") { + return; + } + + // If importing from 'zod' or any 'zod/...' except 'zod/v4', only allow 'zod/v4' in config.ts + const isZodImport = importPath === "zod" || importPath.startsWith("zod/"); + const isZodV4Import = importPath === "zod/v4"; + + if (isZodImport && !isZodV4Import) { + context.report({ + node, + messageId: "enforceZodV4", + data: { + importPath, + }, + }); + } + }, + }; + }, +}; diff --git a/eslint.config.js b/eslint.config.js index ce643a09c..f9d4f308c 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -6,6 +6,7 @@ import tseslint from "typescript-eslint"; import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended"; import vitestPlugin from "@vitest/eslint-plugin"; import noConfigImports from "./eslint-rules/no-config-imports.js"; +import enforceZodV4 from "./eslint-rules/enforce-zod-v4.js"; const testFiles = ["tests/**/*.test.ts", "tests/**/*.ts"]; @@ -72,9 +73,15 @@ export default defineConfig([ "no-config-imports": noConfigImports, }, }, + "enforce-zod-v4": { + rules: { + "enforce-zod-v4": enforceZodV4, + }, + }, }, rules: { "no-config-imports/no-config-imports": "error", + "enforce-zod-v4/enforce-zod-v4": "error", }, }, globalIgnores([ diff --git a/scripts/generateArguments.ts b/scripts/generateArguments.ts index 6107d9951..e5f8c203c 100644 --- a/scripts/generateArguments.ts +++ b/scripts/generateArguments.ts @@ -3,7 +3,7 @@ /** * This script generates argument definitions and updates: * - server.json arrays - * - TODO: README.md configuration table + * - README.md configuration table * * It uses the Zod schema and OPTIONS defined in src/common/config.ts */ @@ -11,8 +11,9 @@ import { readFileSync, writeFileSync } from "fs"; import { join, dirname } from "path"; import { fileURLToPath } from "url"; -import { OPTIONS, UserConfigSchema } from "../src/common/config.js"; -import type { ZodObject, ZodRawShape } from "zod"; +import { OPTIONS, UserConfigSchema, defaultUserConfig, configRegistry } from "../src/common/config.js"; +import assert from "assert"; +import { execSync } from "child_process"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); @@ -21,14 +22,12 @@ function camelCaseToSnakeCase(str: string): string { return str.replace(/[A-Z]/g, (letter) => `_${letter}`).toUpperCase(); } -// List of configuration keys that contain sensitive/secret information +// List of mongosh OPTIONS that contain sensitive/secret information // These should be redacted in logs and marked as secret in environment variable definitions -const SECRET_CONFIG_KEYS = new Set([ +const SECRET_OPTIONS_KEYS = new Set([ "connectionString", "username", "password", - "apiClientId", - "apiClientSecret", "tlsCAFile", "tlsCertificateKeyFile", "tlsCertificateKeyFilePassword", @@ -37,10 +36,9 @@ const SECRET_CONFIG_KEYS = new Set([ "sslPEMKeyFile", "sslPEMKeyPassword", "sslCRLFile", - "voyageApiKey", ]); -interface EnvironmentVariable { +interface ArgumentInfo { name: string; description: string; isRequired: boolean; @@ -48,45 +46,64 @@ interface EnvironmentVariable { isSecret: boolean; configKey: string; defaultValue?: unknown; + defaultValueDescription?: string; } interface ConfigMetadata { description: string; defaultValue?: unknown; + defaultValueDescription?: string; + isSecret?: boolean; } function extractZodDescriptions(): Record { const result: Record = {}; // Get the shape of the Zod schema - const shape = (UserConfigSchema as ZodObject).shape; + const shape = UserConfigSchema.shape; for (const [key, fieldSchema] of Object.entries(shape)) { const schema = fieldSchema; // Extract description from Zod schema - const description = schema.description || `Configuration option: ${key}`; + let description = schema.description || `Configuration option: ${key}`; + + if ("innerType" in schema.def) { + if (schema.def.innerType.def.type === "array") { + assert( + description.startsWith("An array of"), + `Field description for field "${key}" with array type does not start with 'An array of'` + ); + description = description.replace("An array of", "Comma separated values of"); + } + } // Extract default value if present let defaultValue: unknown = undefined; - if (schema._def && "defaultValue" in schema._def) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access - defaultValue = schema._def.defaultValue() as unknown; + let defaultValueDescription: string | undefined = undefined; + let isSecret: boolean | undefined = undefined; + if (schema.def && "defaultValue" in schema.def) { + defaultValue = schema.def.defaultValue; + } + // Get metadata from custom registry + const registryMeta = configRegistry.get(schema); + if (registryMeta) { + defaultValueDescription = registryMeta.defaultValueDescription; + isSecret = registryMeta.isSecret; } result[key] = { description, defaultValue, + defaultValueDescription, + isSecret, }; } return result; } -function generateEnvironmentVariables( - options: typeof OPTIONS, - zodMetadata: Record -): EnvironmentVariable[] { - const envVars: EnvironmentVariable[] = []; +function getArgumentInfo(options: typeof OPTIONS, zodMetadata: Record): ArgumentInfo[] { + const argumentInfos: ArgumentInfo[] = []; const processedKeys = new Set(); // Helper to add env var @@ -107,14 +124,15 @@ function generateEnvironmentVariables( format = "string"; // Arrays are passed as comma-separated strings } - envVars.push({ + argumentInfos.push({ name: envVarName, description: metadata.description, isRequired: false, format: format, - isSecret: SECRET_CONFIG_KEYS.has(key), + isSecret: metadata.isSecret ?? SECRET_OPTIONS_KEYS.has(key), configKey: key, defaultValue: metadata.defaultValue, + defaultValueDescription: metadata.defaultValueDescription, }); }; @@ -139,10 +157,10 @@ function generateEnvironmentVariables( } // Sort by name for consistent output - return envVars.sort((a, b) => a.name.localeCompare(b.name)); + return argumentInfos.sort((a, b) => a.name.localeCompare(b.name)); } -function generatePackageArguments(envVars: EnvironmentVariable[]): unknown[] { +function generatePackageArguments(envVars: ArgumentInfo[]): unknown[] { const packageArguments: unknown[] = []; // Generate positional arguments from the same config options (only documented ones) @@ -168,7 +186,7 @@ function generatePackageArguments(envVars: EnvironmentVariable[]): unknown[] { return packageArguments; } -function updateServerJsonEnvVars(envVars: EnvironmentVariable[]): void { +function updateServerJsonEnvVars(envVars: ArgumentInfo[]): void { const serverJsonPath = join(__dirname, "..", "server.json"); const packageJsonPath = join(__dirname, "..", "package.json"); @@ -179,7 +197,7 @@ function updateServerJsonEnvVars(envVars: EnvironmentVariable[]): void { packages: { registryType?: string; identifier: string; - environmentVariables: EnvironmentVariable[]; + environmentVariables: ArgumentInfo[]; packageArguments?: unknown[]; version?: string; }[]; @@ -207,7 +225,7 @@ function updateServerJsonEnvVars(envVars: EnvironmentVariable[]): void { // Update environmentVariables, packageArguments, and version for all packages if (serverJson.packages && Array.isArray(serverJson.packages)) { for (const pkg of serverJson.packages) { - pkg.environmentVariables = envVarsArray as EnvironmentVariable[]; + pkg.environmentVariables = envVarsArray as ArgumentInfo[]; pkg.packageArguments = packageArguments; // For OCI packages, update the version tag in the identifier and not a version field @@ -224,11 +242,72 @@ function updateServerJsonEnvVars(envVars: EnvironmentVariable[]): void { console.log(`✓ Updated server.json (version ${version})`); } +function generateReadmeConfigTable(argumentInfos: ArgumentInfo[]): string { + const rows = [ + "| CLI Option | Environment Variable | Default | Description |", + "| -------------------------------------- | --------------------------------------------------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |", + ]; + + // Filter to only include options that are in the Zod schema (documented options) + const documentedVars = argumentInfos.filter((v) => !v.description.startsWith("Configuration option:")); + + for (const argumentInfo of documentedVars) { + const cliOption = `\`${argumentInfo.configKey}\``; + const envVarName = `\`${argumentInfo.name}\``; + + // Get default value from Zod schema or fallback to defaultUserConfig + const config = defaultUserConfig as unknown as Record; + const defaultValue = argumentInfo.defaultValue ?? config[argumentInfo.configKey]; + + let defaultValueString = argumentInfo.defaultValueDescription ?? "``"; + if (!argumentInfo.defaultValueDescription && defaultValue !== undefined && defaultValue !== null) { + if (Array.isArray(defaultValue)) { + defaultValueString = `\`"${defaultValue.join(",")}"\``; + } else if (typeof defaultValue === "number") { + defaultValueString = `\`${defaultValue}\``; + } else if (typeof defaultValue === "boolean") { + defaultValueString = `\`${defaultValue}\``; + } else if (typeof defaultValue === "string") { + defaultValueString = `\`"${defaultValue}"\``; + } else { + throw new Error(`Unsupported default value type: ${typeof defaultValue}`); + } + } + + const desc = argumentInfo.description.replace(/\|/g, "\\|"); // Escape pipes in description + rows.push( + `| ${cliOption.padEnd(38)} | ${envVarName.padEnd(51)} | ${defaultValueString.padEnd(75)} | ${desc.padEnd(199)} |` + ); + } + + return rows.join("\n"); +} + +function updateReadmeConfigTable(envVars: ArgumentInfo[]): void { + const readmePath = join(__dirname, "..", "README.md"); + let content = readFileSync(readmePath, "utf-8"); + + const newTable = generateReadmeConfigTable(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####`; + + content = content.replace(tableRegex, replacement); + + writeFileSync(readmePath, content, "utf-8"); + console.log("✓ Updated README.md configuration table"); + + // Run prettier on the README.md file + execSync("npx prettier --write README.md", { cwd: join(__dirname, "..") }); +} + function main(): void { const zodMetadata = extractZodDescriptions(); - const envVars = generateEnvironmentVariables(OPTIONS, zodMetadata); - updateServerJsonEnvVars(envVars); + const argumentInfo = getArgumentInfo(OPTIONS, zodMetadata); + updateServerJsonEnvVars(argumentInfo); + updateReadmeConfigTable(argumentInfo); } main(); diff --git a/server.json b/server.json index bb6cef1c7..bc0c7ada2 100644 --- a/server.json +++ b/server.json @@ -39,7 +39,7 @@ }, { "name": "MDB_MCP_CONFIRMATION_REQUIRED_TOOLS", - "description": "An array of tool names that require user confirmation before execution. Requires the client to support elicitation.", + "description": "Comma separated values of tool names that require user confirmation before execution. Requires the client to support elicitation.", "isRequired": false, "format": "string", "isSecret": false @@ -60,7 +60,7 @@ }, { "name": "MDB_MCP_DISABLED_TOOLS", - "description": "An array of tool names, operation types, and/or categories of tools that will be disabled.", + "description": "Comma separated values of tool names, operation types, and/or categories of tools that will be disabled.", "isRequired": false, "format": "string", "isSecret": false @@ -123,7 +123,7 @@ }, { "name": "MDB_MCP_LOGGERS", - "description": "Comma separated values, possible values are 'mcp', 'disk' and 'stderr'.", + "description": "Comma separated values of logger types.", "isRequired": false, "format": "string", "isSecret": false @@ -149,6 +149,13 @@ "format": "string", "isSecret": false }, + { + "name": "MDB_MCP_PREVIEW_FEATURES", + "description": "Comma separated values of preview features that are enabled.", + "isRequired": false, + "format": "string", + "isSecret": false + }, { "name": "MDB_MCP_READ_ONLY", "description": "When set to true, only allows read, connect, and metadata operation types, disabling create/update/delete operations.", @@ -200,7 +207,7 @@ { "type": "named", "name": "--confirmationRequiredTools", - "description": "An array of tool names that require user confirmation before execution. Requires the client to support elicitation.", + "description": "Comma separated values of tool names that require user confirmation before execution. Requires the client to support elicitation.", "isRequired": false }, { @@ -219,7 +226,7 @@ { "type": "named", "name": "--disabledTools", - "description": "An array of tool names, operation types, and/or categories of tools that will be disabled.", + "description": "Comma separated values of tool names, operation types, and/or categories of tools that will be disabled.", "isRequired": false }, { @@ -274,7 +281,7 @@ { "type": "named", "name": "--loggers", - "description": "Comma separated values, possible values are 'mcp', 'disk' and 'stderr'.", + "description": "Comma separated values of logger types.", "isRequired": false }, { @@ -297,6 +304,12 @@ "description": "Notification timeout for a client to be aware of disconnect (only applies to http transport).", "isRequired": false }, + { + "type": "named", + "name": "--previewFeatures", + "description": "Comma separated values of preview features that are enabled.", + "isRequired": false + }, { "type": "named", "name": "--readOnly", @@ -354,7 +367,7 @@ }, { "name": "MDB_MCP_CONFIRMATION_REQUIRED_TOOLS", - "description": "An array of tool names that require user confirmation before execution. Requires the client to support elicitation.", + "description": "Comma separated values of tool names that require user confirmation before execution. Requires the client to support elicitation.", "isRequired": false, "format": "string", "isSecret": false @@ -375,7 +388,7 @@ }, { "name": "MDB_MCP_DISABLED_TOOLS", - "description": "An array of tool names, operation types, and/or categories of tools that will be disabled.", + "description": "Comma separated values of tool names, operation types, and/or categories of tools that will be disabled.", "isRequired": false, "format": "string", "isSecret": false @@ -438,7 +451,7 @@ }, { "name": "MDB_MCP_LOGGERS", - "description": "Comma separated values, possible values are 'mcp', 'disk' and 'stderr'.", + "description": "Comma separated values of logger types.", "isRequired": false, "format": "string", "isSecret": false @@ -464,6 +477,13 @@ "format": "string", "isSecret": false }, + { + "name": "MDB_MCP_PREVIEW_FEATURES", + "description": "Comma separated values of preview features that are enabled.", + "isRequired": false, + "format": "string", + "isSecret": false + }, { "name": "MDB_MCP_READ_ONLY", "description": "When set to true, only allows read, connect, and metadata operation types, disabling create/update/delete operations.", @@ -515,7 +535,7 @@ { "type": "named", "name": "--confirmationRequiredTools", - "description": "An array of tool names that require user confirmation before execution. Requires the client to support elicitation.", + "description": "Comma separated values of tool names that require user confirmation before execution. Requires the client to support elicitation.", "isRequired": false }, { @@ -534,7 +554,7 @@ { "type": "named", "name": "--disabledTools", - "description": "An array of tool names, operation types, and/or categories of tools that will be disabled.", + "description": "Comma separated values of tool names, operation types, and/or categories of tools that will be disabled.", "isRequired": false }, { @@ -589,7 +609,7 @@ { "type": "named", "name": "--loggers", - "description": "Comma separated values, possible values are 'mcp', 'disk' and 'stderr'.", + "description": "Comma separated values of logger types.", "isRequired": false }, { @@ -612,6 +632,12 @@ "description": "Notification timeout for a client to be aware of disconnect (only applies to http transport).", "isRequired": false }, + { + "type": "named", + "name": "--previewFeatures", + "description": "Comma separated values of preview features that are enabled.", + "isRequired": false + }, { "type": "named", "name": "--readOnly", diff --git a/src/common/config.ts b/src/common/config.ts index 03bcddf8c..ef522114c 100644 --- a/src/common/config.ts +++ b/src/common/config.ts @@ -7,12 +7,9 @@ import { Keychain } from "./keychain.js"; import type { Secret } from "./keychain.js"; import * as levenshteinModule from "ts-levenshtein"; import type { Similarity } from "./search/vectorSearchEmbeddingsManager.js"; -import { z } from "zod"; +import { z as z4 } from "zod/v4"; const levenshtein = levenshteinModule.default; -const previewFeatures = z.enum(["vectorSearch"]); -export type PreviewFeature = z.infer; - // From: https://github.com/mongodb-js/mongosh/blob/main/packages/cli-repl/src/arg-parser.ts export const OPTIONS = { number: ["maxDocumentsPerQuery", "maxBytesPerQuery"], @@ -162,33 +159,65 @@ function isConnectionSpecifier(arg: string | undefined): boolean { ); } -export const UserConfigSchema = z.object({ - apiBaseUrl: z.string().default("https://cloud.mongodb.com/"), - apiClientId: z +/** + * Metadata for config schema fields. + */ +interface ConfigFieldMeta { + /** + * Custom description for the default value, used when generating documentation. + * Example: "see below*" or "`\"disk,mcp\"` see below*" + */ + defaultValueDescription?: string; + /** + * Marks the field as containing sensitive/secret information, used for MCP Registry. + * Secret fields will be redacted in logs and marked as secret in environment variable definitions. + */ + isSecret?: boolean; + + [key: string]: unknown; +} + +/** + * Custom registry for storing metadata specific to config schema fields. + */ +export const configRegistry = z4.registry(); + +export const UserConfigSchema = z4.object({ + apiBaseUrl: z4.string().default("https://cloud.mongodb.com/"), + apiClientId: z4 .string() .optional() - .describe("Atlas API client ID for authentication. Required for running Atlas tools."), - apiClientSecret: z + .describe("Atlas API client ID for authentication. Required for running Atlas tools.") + .register(configRegistry, { isSecret: true }), + apiClientSecret: z4 .string() .optional() - .describe("Atlas API client secret for authentication. Required for running Atlas tools."), - connectionString: z + .describe("Atlas API client secret for authentication. Required for running Atlas tools.") + .register(configRegistry, { isSecret: true }), + connectionString: z4 .string() .optional() .describe( "MongoDB connection string for direct database connections. Optional, if not set, you'll need to call the connect tool before interacting with MongoDB data." - ), - loggers: z - .array(z.enum(["stderr", "disk", "mcp"])) + ) + .register(configRegistry, { isSecret: true }), + loggers: z4 + .array(z4.enum(["stderr", "disk", "mcp"])) .default(["disk", "mcp"]) - .describe("Comma separated values, possible values are 'mcp', 'disk' and 'stderr'."), - logPath: z.string().describe("Folder to store logs."), - disabledTools: z - .array(z.string()) + .describe("An array of logger types.") + .register(configRegistry, { + defaultValueDescription: '`"disk,mcp"` see below*', + }), + logPath: z4 + .string() + .describe("Folder to store logs.") + .register(configRegistry, { defaultValueDescription: "see below*" }), + disabledTools: z4 + .array(z4.string()) .default([]) .describe("An array of tool names, operation types, and/or categories of tools that will be disabled."), - confirmationRequiredTools: z - .array(z.string()) + confirmationRequiredTools: z4 + .array(z4.string()) .default([ "atlas-create-access-list", "atlas-create-db-user", @@ -200,95 +229,104 @@ export const UserConfigSchema = z.object({ .describe( "An array of tool names that require user confirmation before execution. Requires the client to support elicitation." ), - readOnly: z + readOnly: z4 .boolean() .default(false) .describe( "When set to true, only allows read, connect, and metadata operation types, disabling create/update/delete operations." ), - indexCheck: z + indexCheck: z4 .boolean() .default(false) .describe( "When set to true, enforces that query operations must use an index, rejecting queries that perform a collection scan." ), - telemetry: z + telemetry: z4 .enum(["enabled", "disabled"]) .default("enabled") .describe("When set to disabled, disables telemetry collection."), - transport: z.enum(["stdio", "http"]).default("stdio").describe("Either 'stdio' or 'http'."), - httpPort: z + transport: z4.enum(["stdio", "http"]).default("stdio").describe("Either 'stdio' or 'http'."), + httpPort: z4 .number() .default(3000) .describe("Port number for the HTTP server (only used when transport is 'http')."), - httpHost: z + httpHost: z4 .string() .default("127.0.0.1") .describe("Host address to bind the HTTP server to (only used when transport is 'http')."), - httpHeaders: z - .record(z.string()) + httpHeaders: z4 + .object({}) + .passthrough() .default({}) .describe( "Header that the HTTP server will validate when making requests (only used when transport is 'http')." ), - idleTimeoutMs: z + idleTimeoutMs: z4 .number() .default(600_000) .describe("Idle timeout for a client to disconnect (only applies to http transport)."), - notificationTimeoutMs: z + notificationTimeoutMs: z4 .number() .default(540_000) .describe("Notification timeout for a client to be aware of disconnect (only applies to http transport)."), - maxBytesPerQuery: z + maxBytesPerQuery: z4 .number() .default(16_777_216) .describe( "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: z + maxDocumentsPerQuery: z4 .number() .default(100) .describe( "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." ), - exportsPath: z.string().describe("Folder to store exported data files."), - exportTimeoutMs: z + exportsPath: z4 + .string() + .describe("Folder to store exported data files.") + .register(configRegistry, { defaultValueDescription: "see below*" }), + exportTimeoutMs: z4 .number() .default(300_000) .describe("Time in milliseconds after which an export is considered expired and eligible for cleanup."), - exportCleanupIntervalMs: z + exportCleanupIntervalMs: z4 .number() .default(120_000) .describe("Time in milliseconds between export cleanup cycles that remove expired export files."), - atlasTemporaryDatabaseUserLifetimeMs: z + atlasTemporaryDatabaseUserLifetimeMs: z4 .number() .default(14_400_000) .describe( "Time in milliseconds that temporary database users created when connecting to MongoDB Atlas clusters will remain active before being automatically deleted." ), - voyageApiKey: z + voyageApiKey: z4 .string() .default("") .describe( "API key for Voyage AI embeddings service (required for vector search operations with text-to-embedding conversion)." - ), - disableEmbeddingsValidation: z + ) + .register(configRegistry, { isSecret: true }), + disableEmbeddingsValidation: z4 .boolean() .optional() .describe("When set to true, disables validation of embeddings dimensions."), - vectorSearchDimensions: z + vectorSearchDimensions: z4 .number() .default(1024) .describe("Default number of dimensions for vector search embeddings."), - vectorSearchSimilarityFunction: z + vectorSearchSimilarityFunction: z4 .custom() .optional() .default("euclidean") .describe("Default similarity function for vector search: 'euclidean', 'cosine', or 'dotProduct'."), - previewFeatures: z.array(previewFeatures).default([]).describe("An array of preview features that are enabled."), + previewFeatures: z4 + .array(z4.enum(["vectorSearch"])) + .default([]) + .describe("An array of preview features that are enabled."), }); -export type UserConfig = z.infer & CliOptions; +export type PreviewFeature = z4.infer["previewFeatures"][number]; +export type UserConfig = z4.infer & CliOptions; export const defaultUserConfig: UserConfig = { apiBaseUrl: "https://cloud.mongodb.com/", @@ -338,7 +376,7 @@ function getLocalDataPath(): string { } export type DriverOptions = ConnectionInfo["driverOptions"]; -export const defaultDriverOptions: DriverOptions = { +export const defaultDriverOptions: Partial = { readConcern: { level: "local", }, From edc4b35f3f3c4792f2524907017c4953e9e586a9 Mon Sep 17 00:00:00 2001 From: gagik Date: Fri, 31 Oct 2025 11:03:40 +0100 Subject: [PATCH 02/12] chore: add comment about v4 --- eslint-rules/enforce-zod-v4.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/eslint-rules/enforce-zod-v4.js b/eslint-rules/enforce-zod-v4.js index 6d9accba9..cf3e67757 100644 --- a/eslint-rules/enforce-zod-v4.js +++ b/eslint-rules/enforce-zod-v4.js @@ -9,7 +9,8 @@ export default { meta: { type: "problem", docs: { - description: "Only allow importing 'zod/v4' in config.ts, all other imports are allowed elsewhere.", + description: + "Only allow importing 'zod/v4' in config.ts, all other imports are allowed elsewhere. We should only adopt zod v4 for tools and resources once https://github.com/modelcontextprotocol/typescript-sdk/issues/555 is resolved.", recommended: true, }, fixable: null, From c2ed66d69dd36b1804eeecdd4d583d81a9c09358 Mon Sep 17 00:00:00 2001 From: gagik Date: Fri, 31 Oct 2025 11:04:35 +0100 Subject: [PATCH 03/12] chore: fixup descriptions --- src/common/config.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/common/config.ts b/src/common/config.ts index ef522114c..94a38d8af 100644 --- a/src/common/config.ts +++ b/src/common/config.ts @@ -165,12 +165,11 @@ function isConnectionSpecifier(arg: string | undefined): boolean { interface ConfigFieldMeta { /** * Custom description for the default value, used when generating documentation. - * Example: "see below*" or "`\"disk,mcp\"` see below*" */ defaultValueDescription?: string; /** * Marks the field as containing sensitive/secret information, used for MCP Registry. - * Secret fields will be redacted in logs and marked as secret in environment variable definitions. + * Secret fields will be marked as secret in environment variable definitions. */ isSecret?: boolean; From afb93e835253f6334257a27bb4d65a0271d2a778 Mon Sep 17 00:00:00 2001 From: gagik Date: Fri, 31 Oct 2025 11:05:44 +0100 Subject: [PATCH 04/12] chore: use switch --- scripts/generateArguments.ts | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/scripts/generateArguments.ts b/scripts/generateArguments.ts index e5f8c203c..414d06ba5 100644 --- a/scripts/generateArguments.ts +++ b/scripts/generateArguments.ts @@ -263,14 +263,21 @@ function generateReadmeConfigTable(argumentInfos: ArgumentInfo[]): string { if (!argumentInfo.defaultValueDescription && defaultValue !== undefined && defaultValue !== null) { if (Array.isArray(defaultValue)) { defaultValueString = `\`"${defaultValue.join(",")}"\``; - } else if (typeof defaultValue === "number") { - defaultValueString = `\`${defaultValue}\``; - } else if (typeof defaultValue === "boolean") { - defaultValueString = `\`${defaultValue}\``; - } else if (typeof defaultValue === "string") { - defaultValueString = `\`"${defaultValue}"\``; } else { - throw new Error(`Unsupported default value type: ${typeof defaultValue}`); + // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check + switch (typeof defaultValue) { + case "number": + defaultValueString = `\`${defaultValue}\``; + break; + case "boolean": + defaultValueString = `\`${defaultValue}\``; + break; + case "string": + defaultValueString = `\`"${defaultValue}"\``; + break; + default: + throw new Error(`Unsupported default value type: ${typeof defaultValue}`); + } } } From 3f6de0fcf188479d5bdf3259c3f2a68c31c22c19 Mon Sep 17 00:00:00 2001 From: gagik Date: Fri, 31 Oct 2025 11:07:27 +0100 Subject: [PATCH 05/12] chore: unnecessary type change --- src/common/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/config.ts b/src/common/config.ts index 94a38d8af..902fbcc2a 100644 --- a/src/common/config.ts +++ b/src/common/config.ts @@ -375,7 +375,7 @@ function getLocalDataPath(): string { } export type DriverOptions = ConnectionInfo["driverOptions"]; -export const defaultDriverOptions: Partial = { +export const defaultDriverOptions: DriverOptions = { readConcern: { level: "local", }, From 7fda570af0b2232ef06e14a27f665cb3f48fdede Mon Sep 17 00:00:00 2001 From: gagik Date: Fri, 31 Oct 2025 11:18:38 +0100 Subject: [PATCH 06/12] chore: get default user config from schema --- README.md | 2 +- src/common/config.ts | 67 +++++++++----------------------- tests/unit/common/config.test.ts | 43 ++++++++++++++++++++ 3 files changed, 62 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index 9b7af8f81..100e4770c 100644 --- a/README.md +++ b/README.md @@ -354,7 +354,7 @@ The MongoDB MCP Server can be configured using multiple methods, with the follow | `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. | +| `disableEmbeddingsValidation` | `MDB_MCP_DISABLE_EMBEDDINGS_VALIDATION` | `` | 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. | diff --git a/src/common/config.ts b/src/common/config.ts index 902fbcc2a..9d4f6d139 100644 --- a/src/common/config.ts +++ b/src/common/config.ts @@ -181,6 +181,21 @@ interface ConfigFieldMeta { */ export const configRegistry = z4.registry(); +function getLocalDataPath(): string { + return process.platform === "win32" + ? path.join(process.env.LOCALAPPDATA || process.env.APPDATA || os.homedir(), "mongodb") + : path.join(os.homedir(), ".mongodb"); +} + +export function getLogPath(): string { + const logPath = path.join(getLocalDataPath(), "mongodb-mcp", ".app-logs"); + return logPath; +} + +export function getExportsPath(): string { + return path.join(getLocalDataPath(), "mongodb-mcp", "exports"); +} + export const UserConfigSchema = z4.object({ apiBaseUrl: z4.string().default("https://cloud.mongodb.com/"), apiClientId: z4 @@ -209,6 +224,7 @@ export const UserConfigSchema = z4.object({ }), logPath: z4 .string() + .default(getLogPath()) .describe("Folder to store logs.") .register(configRegistry, { defaultValueDescription: "see below*" }), disabledTools: z4 @@ -282,6 +298,7 @@ export const UserConfigSchema = z4.object({ ), exportsPath: z4 .string() + .default(getExportsPath()) .describe("Folder to store exported data files.") .register(configRegistry, { defaultValueDescription: "see below*" }), exportTimeoutMs: z4 @@ -327,40 +344,7 @@ export const UserConfigSchema = z4.object({ export type PreviewFeature = z4.infer["previewFeatures"][number]; export type UserConfig = z4.infer & CliOptions; -export const defaultUserConfig: UserConfig = { - 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, // By default, we only fetch a maximum 100 documents per query / aggregation - maxBytesPerQuery: 16 * 1024 * 1024, // By default, we only return ~16 mb of data per query / aggregation - atlasTemporaryDatabaseUserLifetimeMs: 4 * 60 * 60 * 1000, // 4 hours - voyageApiKey: "", - disableEmbeddingsValidation: false, - vectorSearchDimensions: 1024, - vectorSearchSimilarityFunction: "euclidean", - previewFeatures: [], -}; +export const defaultUserConfig: UserConfig = UserConfigSchema.parse({}); export const config = setupUserConfig({ defaults: defaultUserConfig, @@ -368,12 +352,6 @@ export const config = setupUserConfig({ env: process.env, }); -function getLocalDataPath(): string { - return process.platform === "win32" - ? path.join(process.env.LOCALAPPDATA || process.env.APPDATA || os.homedir(), "mongodb") - : path.join(os.homedir(), ".mongodb"); -} - export type DriverOptions = ConnectionInfo["driverOptions"]; export const defaultDriverOptions: DriverOptions = { readConcern: { @@ -388,15 +366,6 @@ export const defaultDriverOptions: DriverOptions = { applyProxyToOIDC: true, }; -function getLogPath(): string { - const logPath = path.join(getLocalDataPath(), "mongodb-mcp", ".app-logs"); - return logPath; -} - -function getExportsPath(): string { - return path.join(getLocalDataPath(), "mongodb-mcp", "exports"); -} - // Gets the config supplied by the user as environment variables. The variable names // are prefixed with `MDB_MCP_` and the keys match the UserConfig keys, but are converted // to SNAKE_UPPER_CASE. diff --git a/tests/unit/common/config.test.ts b/tests/unit/common/config.test.ts index 78a0382ef..54db144d4 100644 --- a/tests/unit/common/config.test.ts +++ b/tests/unit/common/config.test.ts @@ -5,12 +5,55 @@ import { defaultUserConfig, registerKnownSecretsInRootKeychain, warnAboutDeprecatedOrUnknownCliArgs, + getLogPath, + getExportsPath, } from "../../../src/common/config.js"; import type { CliOptions } from "@mongosh/arg-parser"; import { Keychain } from "../../../src/common/keychain.js"; import type { Secret } from "../../../src/common/keychain.js"; 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/", + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + logPath: getLogPath() as string, + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + exportsPath: getExportsPath() as string, + 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", + previewFeatures: [], + }; + + expect(defaultUserConfig).toStrictEqual(expectedDefaults); + }); + describe("env var parsing", () => { describe("mongodb urls", () => { it("should not try to parse a multiple-host urls", () => { From bb16f57d56cc63b1fe61870a11b7be682dd4f3e2 Mon Sep 17 00:00:00 2001 From: gagik Date: Fri, 31 Oct 2025 13:10:04 +0100 Subject: [PATCH 07/12] chore: validate config arguments using Zod This removes a lot of boilerplate validation code and makes the UserConfigSchema the main entry point for our parsing. There's actually a couple more follow-ups I want to do with i.e. storing secrets but don't want to make this too big. --- eslint-rules/enforce-zod-v4.js | 8 +- scripts/generateArguments.ts | 10 +- src/common/argsParserOptions.ts | 109 ++++++ src/common/config.ts | 354 ++++-------------- src/common/configUtils.ts | 96 +++++ src/common/schemas.ts | 7 + .../search/vectorSearchEmbeddingsManager.ts | 5 +- src/tools/mongodb/create/createIndex.ts | 3 +- tests/unit/common/config.test.ts | 121 +++--- tests/unit/common/roles.test.ts | 6 +- 10 files changed, 351 insertions(+), 368 deletions(-) create mode 100644 src/common/argsParserOptions.ts create mode 100644 src/common/configUtils.ts create mode 100644 src/common/schemas.ts diff --git a/eslint-rules/enforce-zod-v4.js b/eslint-rules/enforce-zod-v4.js index cf3e67757..de6ef8302 100644 --- a/eslint-rules/enforce-zod-v4.js +++ b/eslint-rules/enforce-zod-v4.js @@ -3,6 +3,7 @@ import path from "path"; // The file that is allowed to import from zod/v4 const configFilePath = path.resolve(import.meta.dirname, "../src/common/config.ts"); +const schemasFilePath = path.resolve(import.meta.dirname, "../src/common/schemas.ts"); // Ref: https://eslint.org/docs/latest/extend/custom-rules export default { @@ -22,8 +23,7 @@ export default { create(context) { const currentFilePath = path.resolve(context.getFilename()); - // Only enforce rule in config.ts - if (currentFilePath !== configFilePath) { + if (currentFilePath === configFilePath || currentFilePath === schemasFilePath) { return {}; } @@ -36,11 +36,9 @@ export default { return; } - // If importing from 'zod' or any 'zod/...' except 'zod/v4', only allow 'zod/v4' in config.ts - const isZodImport = importPath === "zod" || importPath.startsWith("zod/"); const isZodV4Import = importPath === "zod/v4"; - if (isZodImport && !isZodV4Import) { + if (isZodV4Import) { context.report({ node, messageId: "enforceZodV4", diff --git a/scripts/generateArguments.ts b/scripts/generateArguments.ts index 414d06ba5..ee3695c4f 100644 --- a/scripts/generateArguments.ts +++ b/scripts/generateArguments.ts @@ -11,9 +11,10 @@ import { readFileSync, writeFileSync } from "fs"; import { join, dirname } from "path"; import { fileURLToPath } from "url"; -import { OPTIONS, UserConfigSchema, defaultUserConfig, configRegistry } from "../src/common/config.js"; +import { UserConfigSchema, configRegistry } from "../src/common/config.js"; import assert from "assert"; import { execSync } from "child_process"; +import { OPTIONS } from "../src/common/argsParserOptions.js"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); @@ -68,7 +69,8 @@ function extractZodDescriptions(): Record { let description = schema.description || `Configuration option: ${key}`; if ("innerType" in schema.def) { - if (schema.def.innerType.def.type === "array") { + // "pipe" is used for our comma-separated arrays + if (schema.def.innerType.def.type === "pipe") { assert( description.startsWith("An array of"), `Field description for field "${key}" with array type does not start with 'An array of'` @@ -255,9 +257,7 @@ function generateReadmeConfigTable(argumentInfos: ArgumentInfo[]): string { const cliOption = `\`${argumentInfo.configKey}\``; const envVarName = `\`${argumentInfo.name}\``; - // Get default value from Zod schema or fallback to defaultUserConfig - const config = defaultUserConfig as unknown as Record; - const defaultValue = argumentInfo.defaultValue ?? config[argumentInfo.configKey]; + const defaultValue = argumentInfo.defaultValue; let defaultValueString = argumentInfo.defaultValueDescription ?? "``"; if (!argumentInfo.defaultValueDescription && defaultValue !== undefined && defaultValue !== null) { diff --git a/src/common/argsParserOptions.ts b/src/common/argsParserOptions.ts new file mode 100644 index 000000000..8decc318a --- /dev/null +++ b/src/common/argsParserOptions.ts @@ -0,0 +1,109 @@ +type ArgsParserOptions = { + string: string[]; + number: string[]; + boolean: string[]; + array: string[]; + alias: Record; + configuration: Record; +}; + +// TODO: Export this from arg-parser or find a better way to do this +// From: https://github.com/mongodb-js/mongosh/blob/main/packages/cli-repl/src/arg-parser.ts +export const OPTIONS = { + number: ["maxDocumentsPerQuery", "maxBytesPerQuery"], + string: [ + "apiBaseUrl", + "apiClientId", + "apiClientSecret", + "connectionString", + "httpHost", + "httpPort", + "idleTimeoutMs", + "logPath", + "notificationTimeoutMs", + "telemetry", + "transport", + "apiVersion", + "authenticationDatabase", + "authenticationMechanism", + "browser", + "db", + "gssapiHostName", + "gssapiServiceName", + "host", + "oidcFlows", + "oidcRedirectUri", + "password", + "port", + "sslCAFile", + "sslCRLFile", + "sslCertificateSelector", + "sslDisabledProtocols", + "sslPEMKeyFile", + "sslPEMKeyPassword", + "sspiHostnameCanonicalization", + "sspiRealmOverride", + "tlsCAFile", + "tlsCRLFile", + "tlsCertificateKeyFile", + "tlsCertificateKeyFilePassword", + "tlsCertificateSelector", + "tlsDisabledProtocols", + "username", + "atlasTemporaryDatabaseUserLifetimeMs", + "exportsPath", + "exportTimeoutMs", + "exportCleanupIntervalMs", + "voyageApiKey", + ], + boolean: [ + "apiDeprecationErrors", + "apiStrict", + "disableEmbeddingsValidation", + "help", + "indexCheck", + "ipv6", + "nodb", + "oidcIdTokenAsAccessToken", + "oidcNoNonce", + "oidcTrustedEndpoint", + "readOnly", + "retryWrites", + "ssl", + "sslAllowInvalidCertificates", + "sslAllowInvalidHostnames", + "sslFIPSMode", + "tls", + "tlsAllowInvalidCertificates", + "tlsAllowInvalidHostnames", + "tlsFIPSMode", + "version", + ], + array: ["disabledTools", "loggers", "confirmationRequiredTools", "previewFeatures"], + alias: { + h: "help", + p: "password", + u: "username", + "build-info": "buildInfo", + browser: "browser", + oidcDumpTokens: "oidcDumpTokens", + oidcRedirectUrl: "oidcRedirectUri", + oidcIDTokenAsAccessToken: "oidcIdTokenAsAccessToken", + }, + configuration: { + "camel-case-expansion": false, + "unknown-options-as-args": true, + "parse-positional-numbers": false, + "parse-numbers": false, + "greedy-arrays": true, + "short-option-groups": false, + }, +} as Readonly; + +export const ALL_CONFIG_KEYS = new Set( + (OPTIONS.string as readonly string[]) + .concat(OPTIONS.number) + .concat(OPTIONS.array) + .concat(OPTIONS.boolean) + .concat(Object.keys(OPTIONS.alias)) +); diff --git a/src/common/config.ts b/src/common/config.ts index 9d4f6d139..1e0a9cbf2 100644 --- a/src/common/config.ts +++ b/src/common/config.ts @@ -1,201 +1,23 @@ -import path from "path"; -import os from "os"; import argv from "yargs-parser"; import type { CliOptions, ConnectionInfo } from "@mongosh/arg-parser"; import { generateConnectionInfoFromCliArgs } from "@mongosh/arg-parser"; import { Keychain } from "./keychain.js"; import type { Secret } from "./keychain.js"; -import * as levenshteinModule from "ts-levenshtein"; -import type { Similarity } from "./search/vectorSearchEmbeddingsManager.js"; -import { z as z4 } from "zod/v4"; -const levenshtein = levenshteinModule.default; - -// From: https://github.com/mongodb-js/mongosh/blob/main/packages/cli-repl/src/arg-parser.ts -export const OPTIONS = { - number: ["maxDocumentsPerQuery", "maxBytesPerQuery"], - string: [ - "apiBaseUrl", - "apiClientId", - "apiClientSecret", - "connectionString", - "httpHost", - "httpPort", - "idleTimeoutMs", - "logPath", - "notificationTimeoutMs", - "telemetry", - "transport", - "apiVersion", - "authenticationDatabase", - "authenticationMechanism", - "browser", - "db", - "gssapiHostName", - "gssapiServiceName", - "host", - "oidcFlows", - "oidcRedirectUri", - "password", - "port", - "sslCAFile", - "sslCRLFile", - "sslCertificateSelector", - "sslDisabledProtocols", - "sslPEMKeyFile", - "sslPEMKeyPassword", - "sspiHostnameCanonicalization", - "sspiRealmOverride", - "tlsCAFile", - "tlsCRLFile", - "tlsCertificateKeyFile", - "tlsCertificateKeyFilePassword", - "tlsCertificateSelector", - "tlsDisabledProtocols", - "username", - "atlasTemporaryDatabaseUserLifetimeMs", - "exportsPath", - "exportTimeoutMs", - "exportCleanupIntervalMs", - "voyageApiKey", - ], - boolean: [ - "apiDeprecationErrors", - "apiStrict", - "disableEmbeddingsValidation", - "help", - "indexCheck", - "ipv6", - "nodb", - "oidcIdTokenAsAccessToken", - "oidcNoNonce", - "oidcTrustedEndpoint", - "readOnly", - "retryWrites", - "ssl", - "sslAllowInvalidCertificates", - "sslAllowInvalidHostnames", - "sslFIPSMode", - "tls", - "tlsAllowInvalidCertificates", - "tlsAllowInvalidHostnames", - "tlsFIPSMode", - "version", - ], - array: ["disabledTools", "loggers", "confirmationRequiredTools", "previewFeatures"], - alias: { - h: "help", - p: "password", - u: "username", - "build-info": "buildInfo", - browser: "browser", - oidcDumpTokens: "oidcDumpTokens", - oidcRedirectUrl: "oidcRedirectUri", - oidcIDTokenAsAccessToken: "oidcIdTokenAsAccessToken", - }, - configuration: { - "camel-case-expansion": false, - "unknown-options-as-args": true, - "parse-positional-numbers": false, - "parse-numbers": false, - "greedy-arrays": true, - "short-option-groups": false, - }, -} as Readonly; - -interface Options { - string: string[]; - number: string[]; - boolean: string[]; - array: string[]; - alias: Record; - configuration: Record; -} - -export const ALL_CONFIG_KEYS = new Set( - (OPTIONS.string as readonly string[]) - .concat(OPTIONS.number) - .concat(OPTIONS.array) - .concat(OPTIONS.boolean) - .concat(Object.keys(OPTIONS.alias)) -); - -function validateConfigKey(key: string): { valid: boolean; suggestion?: string } { - if (ALL_CONFIG_KEYS.has(key)) { - return { valid: true }; - } - - let minLev = Number.MAX_VALUE; - let suggestion = ""; - - // find the closest match for a suggestion - for (const validKey of ALL_CONFIG_KEYS) { - // check if there is an exact case-insensitive match - if (validKey.toLowerCase() === key.toLowerCase()) { - return { valid: false, suggestion: validKey }; - } - - // else, infer something using levenshtein so we suggest a valid key - const lev = levenshtein.get(key, validKey); - if (lev < minLev) { - minLev = lev; - suggestion = validKey; - } - } - - if (minLev <= 2) { - // accept up to 2 typos - return { valid: false, suggestion }; - } - - return { valid: false }; -} - -function isConnectionSpecifier(arg: string | undefined): boolean { - return ( - arg !== undefined && - (arg.startsWith("mongodb://") || - arg.startsWith("mongodb+srv://") || - !(arg.endsWith(".js") || arg.endsWith(".mongodb"))) - ); -} -/** - * Metadata for config schema fields. - */ -interface ConfigFieldMeta { - /** - * Custom description for the default value, used when generating documentation. - */ - defaultValueDescription?: string; - /** - * Marks the field as containing sensitive/secret information, used for MCP Registry. - * Secret fields will be marked as secret in environment variable definitions. - */ - isSecret?: boolean; - - [key: string]: unknown; -} +import { z as z4 } from "zod/v4"; +import { + commaSeparatedToArray, + type ConfigFieldMeta, + getExportsPath, + getLogPath, + isConnectionSpecifier, + validateConfigKey, +} from "./configUtils.js"; +import { OPTIONS } from "./argsParserOptions.js"; +import { similarityEnumV4 } from "./schemas.js"; -/** - * Custom registry for storing metadata specific to config schema fields. - */ export const configRegistry = z4.registry(); -function getLocalDataPath(): string { - return process.platform === "win32" - ? path.join(process.env.LOCALAPPDATA || process.env.APPDATA || os.homedir(), "mongodb") - : path.join(os.homedir(), ".mongodb"); -} - -export function getLogPath(): string { - const logPath = path.join(getLocalDataPath(), "mongodb-mcp", ".app-logs"); - return logPath; -} - -export function getExportsPath(): string { - return path.join(getLocalDataPath(), "mongodb-mcp", "exports"); -} - export const UserConfigSchema = z4.object({ apiBaseUrl: z4.string().default("https://cloud.mongodb.com/"), apiClientId: z4 @@ -216,7 +38,16 @@ export const UserConfigSchema = z4.object({ ) .register(configRegistry, { isSecret: true }), loggers: z4 - .array(z4.enum(["stderr", "disk", "mcp"])) + .preprocess( + (val: string | string[] | undefined) => commaSeparatedToArray(val), + z4.array(z4.enum(["stderr", "disk", "mcp"])) + ) + .check( + z4.minLength(1, "Cannot be an empty array"), + z4.refine((val) => new Set(val).size === val.length, { + message: "Duplicate loggers found in config", + }) + ) .default(["disk", "mcp"]) .describe("An array of logger types.") .register(configRegistry, { @@ -228,11 +59,11 @@ export const UserConfigSchema = z4.object({ .describe("Folder to store logs.") .register(configRegistry, { defaultValueDescription: "see below*" }), disabledTools: z4 - .array(z4.string()) + .preprocess((val: string | string[] | undefined) => commaSeparatedToArray(val), z4.array(z4.string())) .default([]) .describe("An array of tool names, operation types, and/or categories of tools that will be disabled."), confirmationRequiredTools: z4 - .array(z4.string()) + .preprocess((val: string | string[] | undefined) => commaSeparatedToArray(val), z4.array(z4.string())) .default([ "atlas-create-access-list", "atlas-create-db-user", @@ -261,8 +92,11 @@ export const UserConfigSchema = z4.object({ .default("enabled") .describe("When set to disabled, disables telemetry collection."), transport: z4.enum(["stdio", "http"]).default("stdio").describe("Either 'stdio' or 'http'."), - httpPort: z4 + httpPort: z4.coerce .number() + .int() + .min(1, "Invalid httpPort: must be at least 1") + .max(65535, "Invalid httpPort: must be at most 65535") .default(3000) .describe("Port number for the HTTP server (only used when transport is 'http')."), httpHost: z4 @@ -276,21 +110,21 @@ export const UserConfigSchema = z4.object({ .describe( "Header that the HTTP server will validate when making requests (only used when transport is 'http')." ), - idleTimeoutMs: z4 + idleTimeoutMs: z4.coerce .number() .default(600_000) .describe("Idle timeout for a client to disconnect (only applies to http transport)."), - notificationTimeoutMs: z4 + notificationTimeoutMs: z4.coerce .number() .default(540_000) .describe("Notification timeout for a client to be aware of disconnect (only applies to http transport)."), - maxBytesPerQuery: z4 + maxBytesPerQuery: z4.coerce .number() .default(16_777_216) .describe( "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: z4 + maxDocumentsPerQuery: z4.coerce .number() .default(100) .describe( @@ -301,15 +135,15 @@ export const UserConfigSchema = z4.object({ .default(getExportsPath()) .describe("Folder to store exported data files.") .register(configRegistry, { defaultValueDescription: "see below*" }), - exportTimeoutMs: z4 + exportTimeoutMs: z4.coerce .number() .default(300_000) .describe("Time in milliseconds after which an export is considered expired and eligible for cleanup."), - exportCleanupIntervalMs: z4 + exportCleanupIntervalMs: z4.coerce .number() .default(120_000) .describe("Time in milliseconds between export cleanup cycles that remove expired export files."), - atlasTemporaryDatabaseUserLifetimeMs: z4 + atlasTemporaryDatabaseUserLifetimeMs: z4.coerce .number() .default(14_400_000) .describe( @@ -326,28 +160,29 @@ export const UserConfigSchema = z4.object({ .boolean() .optional() .describe("When set to true, disables validation of embeddings dimensions."), - vectorSearchDimensions: z4 + vectorSearchDimensions: z4.coerce .number() .default(1024) .describe("Default number of dimensions for vector search embeddings."), - vectorSearchSimilarityFunction: z4 - .custom() + vectorSearchSimilarityFunction: similarityEnumV4 .optional() .default("euclidean") .describe("Default similarity function for vector search: 'euclidean', 'cosine', or 'dotProduct'."), previewFeatures: z4 - .array(z4.enum(["vectorSearch"])) + .preprocess( + (val: string | string[] | undefined) => commaSeparatedToArray(val), + z4.array(z4.enum(["vectorSearch"])) + ) .default([]) .describe("An array of preview features that are enabled."), }); +export type Similarity = z4.infer["vectorSearchSimilarityFunction"]; export type PreviewFeature = z4.infer["previewFeatures"][number]; export type UserConfig = z4.infer & CliOptions; - -export const defaultUserConfig: UserConfig = UserConfigSchema.parse({}); +export type Logger = UserConfig["loggers"][number]; export const config = setupUserConfig({ - defaults: defaultUserConfig, cli: process.argv, env: process.env, }); @@ -372,7 +207,11 @@ export const defaultDriverOptions: DriverOptions = { function parseEnvConfig(env: Record): Partial { const CONFIG_WITH_URLS: Set = new Set<(typeof OPTIONS)["string"][number]>(["connectionString"]); - function setValue(obj: Record, path: string[], value: string): void { + function setValue( + obj: Record | undefined>, + path: string[], + value: string + ): void { const currentField = path.shift(); if (!currentField) { return; @@ -409,10 +248,10 @@ function parseEnvConfig(env: Record): Partial { obj[currentField] = {}; } - setValue(obj[currentField] as Record, path, value); + setValue(obj[currentField] as Record, path, value); } - const result: Record = {}; + const result: Record = {}; const mcpVariables = Object.entries(env).filter( ([key, value]) => value !== undefined && key.startsWith("MDB_MCP_") ) as [string, string][]; @@ -437,12 +276,14 @@ function SNAKE_CASE_toCamelCase(str: string): string { // We will consolidate them in a way where the mongosh format takes precedence. // We will warn users that previous configuration is deprecated in favour of // whatever is in mongosh. -function parseCliConfig(args: string[]): CliOptions { +function parseCliConfig(args: string[]): Partial> { const programArgs = args.slice(2); - const parsed = argv(programArgs, OPTIONS as unknown as argv.Options) as unknown as CliOptions & - UserConfig & { - _?: string[]; - }; + const parsed = argv(programArgs, OPTIONS as unknown as argv.Options) as unknown as Record< + keyof CliOptions, + string | number | undefined + > & { + _?: string[]; + }; const positionalArguments = parsed._ ?? []; @@ -512,29 +353,6 @@ export function warnAboutDeprecatedOrUnknownCliArgs( } } -function commaSeparatedToArray(str: string | string[] | undefined): T { - if (str === undefined) { - return [] as unknown as T; - } - - if (!Array.isArray(str)) { - return [str] as T; - } - - if (str.length === 0) { - return str as T; - } - - if (str.length === 1) { - return str[0] - ?.split(",") - .map((e) => e.trim()) - .filter((e) => e.length > 0) as T; - } - - return str as T; -} - export function registerKnownSecretsInRootKeychain(userConfig: Partial): void { const keychain = Keychain.root; @@ -558,59 +376,25 @@ export function registerKnownSecretsInRootKeychain(userConfig: Partial; - defaults: UserConfig; -}): UserConfig { - const userConfig = { - ...defaults, +export function setupUserConfig({ cli, env }: { cli: string[]; env: Record }): UserConfig { + const rawConfig = { ...parseEnvConfig(env), ...parseCliConfig(cli), - } satisfies UserConfig; - - userConfig.disabledTools = commaSeparatedToArray(userConfig.disabledTools); - userConfig.loggers = commaSeparatedToArray(userConfig.loggers); - userConfig.confirmationRequiredTools = commaSeparatedToArray(userConfig.confirmationRequiredTools); - - if (userConfig.connectionString && userConfig.connectionSpecifier) { - const connectionInfo = generateConnectionInfoFromCliArgs(userConfig); - userConfig.connectionString = connectionInfo.connectionString; - } - - const transport = userConfig.transport as string; - if (transport !== "http" && transport !== "stdio") { - throw new Error(`Invalid transport: ${transport}`); - } - - const telemetry = userConfig.telemetry as string; - if (telemetry !== "enabled" && telemetry !== "disabled") { - throw new Error(`Invalid telemetry: ${telemetry}`); - } - - const httpPort = +userConfig.httpPort; - if (httpPort < 1 || httpPort > 65535 || isNaN(httpPort)) { - throw new Error(`Invalid httpPort: ${userConfig.httpPort}`); - } - - if (userConfig.loggers.length === 0) { - throw new Error("No loggers found in config"); - } + }; - const loggerTypes = new Set(userConfig.loggers); - if (loggerTypes.size !== userConfig.loggers.length) { - throw new Error("Duplicate loggers found in config"); + if (rawConfig.connectionString && rawConfig.connectionSpecifier) { + const connectionInfo = generateConnectionInfoFromCliArgs(rawConfig as UserConfig); + rawConfig.connectionString = connectionInfo.connectionString; } - for (const loggerType of userConfig.loggers as string[]) { - if (loggerType !== "mcp" && loggerType !== "disk" && loggerType !== "stderr") { - throw new Error(`Invalid logger: ${loggerType}`); - } + const parseResult = UserConfigSchema.safeParse(rawConfig); + if (parseResult.error) { + throw new Error( + `Invalid configuration for the following fields:\n${parseResult.error.issues.map((issue) => `${issue.path.join(".")} - ${issue.message}`).join("\n")}` + ); } + // We don't have as schema defined for all args-parser arguments so we need to merge the raw config with the parsed config. + const userConfig = { ...rawConfig, ...parseResult.data } as UserConfig; registerKnownSecretsInRootKeychain(userConfig); return userConfig; diff --git a/src/common/configUtils.ts b/src/common/configUtils.ts new file mode 100644 index 000000000..472851522 --- /dev/null +++ b/src/common/configUtils.ts @@ -0,0 +1,96 @@ +import path from "path"; +import os from "os"; +import { ALL_CONFIG_KEYS } from "./argsParserOptions.js"; +import * as levenshteinModule from "ts-levenshtein"; +const levenshtein = levenshteinModule.default; + +export function validateConfigKey(key: string): { valid: boolean; suggestion?: string } { + if (ALL_CONFIG_KEYS.has(key)) { + return { valid: true }; + } + + let minLev = Number.MAX_VALUE; + let suggestion = ""; + + // find the closest match for a suggestion + for (const validKey of ALL_CONFIG_KEYS) { + // check if there is an exact case-insensitive match + if (validKey.toLowerCase() === key.toLowerCase()) { + return { valid: false, suggestion: validKey }; + } + + // else, infer something using levenshtein so we suggest a valid key + const lev = levenshtein.get(key, validKey); + if (lev < minLev) { + minLev = lev; + suggestion = validKey; + } + } + + if (minLev <= 2) { + // accept up to 2 typos + return { valid: false, suggestion }; + } + + return { valid: false }; +} + +export function isConnectionSpecifier(arg: string | undefined): boolean { + return ( + arg !== undefined && + (arg.startsWith("mongodb://") || + arg.startsWith("mongodb+srv://") || + !(arg.endsWith(".js") || arg.endsWith(".mongodb"))) + ); +} + +/** + * Metadata for config schema fields. + */ +export type ConfigFieldMeta = { + /** + * Custom description for the default value, used when generating documentation. + */ + defaultValueDescription?: string; + /** + * Marks the field as containing sensitive/secret information, used for MCP Registry. + * Secret fields will be marked as secret in environment variable definitions. + */ + isSecret?: boolean; + + [key: string]: unknown; +}; + +export function getLocalDataPath(): string { + return process.platform === "win32" + ? path.join(process.env.LOCALAPPDATA || process.env.APPDATA || os.homedir(), "mongodb") + : path.join(os.homedir(), ".mongodb"); +} + +export function getLogPath(): string { + const logPath = path.join(getLocalDataPath(), "mongodb-mcp", ".app-logs"); + return logPath; +} + +export function getExportsPath(): string { + return path.join(getLocalDataPath(), "mongodb-mcp", "exports"); +} + +export function commaSeparatedToArray(str: string | string[] | undefined): T | undefined { + if (str === undefined) { + return undefined; + } + + if (!Array.isArray(str)) { + return [str] as T; + } + + if (str.length === 1) { + return str[0] + ?.split(",") + .map((e) => e.trim()) + .filter((e) => e.length > 0) as T; + } + + return str as T; +} diff --git a/src/common/schemas.ts b/src/common/schemas.ts new file mode 100644 index 000000000..fef0ba6dc --- /dev/null +++ b/src/common/schemas.ts @@ -0,0 +1,7 @@ +import z4 from "zod/v4"; +import z3 from "zod/v3"; + +const similarityValues = ["cosine", "euclidean", "dotProduct"] as const; + +export const similarityEnumV4 = z4.enum(similarityValues); +export const similarityEnum = z3.enum(similarityValues); diff --git a/src/common/search/vectorSearchEmbeddingsManager.ts b/src/common/search/vectorSearchEmbeddingsManager.ts index 1af3a8a6c..20fc15426 100644 --- a/src/common/search/vectorSearchEmbeddingsManager.ts +++ b/src/common/search/vectorSearchEmbeddingsManager.ts @@ -1,6 +1,6 @@ import type { NodeDriverServiceProvider } from "@mongosh/service-provider-node-driver"; import { BSON, type Document } from "bson"; -import type { UserConfig } from "../config.js"; +import type { Similarity, UserConfig } from "../config.js"; import type { ConnectionManager } from "../connectionManager.js"; import z from "zod"; import { ErrorCodes, MongoDBError } from "../errors.js"; @@ -8,9 +8,6 @@ import { getEmbeddingsProvider } from "./embeddingsProvider.js"; import type { EmbeddingParameters, SupportedEmbeddingParameters } from "./embeddingsProvider.js"; import { formatUntrustedData } from "../../tools/tool.js"; -export const similarityEnum = z.enum(["cosine", "euclidean", "dotProduct"]); -export type Similarity = z.infer; - export const quantizationEnum = z.enum(["none", "scalar", "binary"]); export type Quantization = z.infer; diff --git a/src/tools/mongodb/create/createIndex.ts b/src/tools/mongodb/create/createIndex.ts index 252d8c4c4..0532453c2 100644 --- a/src/tools/mongodb/create/createIndex.ts +++ b/src/tools/mongodb/create/createIndex.ts @@ -3,7 +3,8 @@ import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import { DbOperationArgs, MongoDBToolBase } from "../mongodbTool.js"; import { type ToolArgs, type OperationType } from "../../tool.js"; import type { IndexDirection } from "mongodb"; -import { quantizationEnum, similarityEnum } from "../../../common/search/vectorSearchEmbeddingsManager.js"; +import { quantizationEnum } from "../../../common/search/vectorSearchEmbeddingsManager.js"; +import { similarityEnum } from "../../../common/schemas.js"; export class CreateIndexTool extends MongoDBToolBase { private vectorSearchIndexDefinition = z.object({ diff --git a/tests/unit/common/config.test.ts b/tests/unit/common/config.test.ts index 54db144d4..89eb6af4a 100644 --- a/tests/unit/common/config.test.ts +++ b/tests/unit/common/config.test.ts @@ -2,12 +2,11 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import type { UserConfig } from "../../../src/common/config.js"; import { setupUserConfig, - defaultUserConfig, registerKnownSecretsInRootKeychain, warnAboutDeprecatedOrUnknownCliArgs, - getLogPath, - getExportsPath, + UserConfigSchema, } from "../../../src/common/config.js"; +import { getLogPath, getExportsPath } from "../../../src/common/configUtils.js"; import type { CliOptions } from "@mongosh/arg-parser"; import { Keychain } from "../../../src/common/keychain.js"; import type { Secret } from "../../../src/common/keychain.js"; @@ -17,10 +16,8 @@ describe("config", () => { // Expected hardcoded values (what we had before) const expectedDefaults = { apiBaseUrl: "https://cloud.mongodb.com/", - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - logPath: getLogPath() as string, - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - exportsPath: getExportsPath() as string, + logPath: getLogPath(), + exportsPath: getExportsPath(), exportTimeoutMs: 5 * 60 * 1000, // 5 minutes exportCleanupIntervalMs: 2 * 60 * 1000, // 2 minutes disabledTools: [], @@ -51,7 +48,7 @@ describe("config", () => { previewFeatures: [], }; - expect(defaultUserConfig).toStrictEqual(expectedDefaults); + expect(UserConfigSchema.parse({})).toStrictEqual(expectedDefaults); }); describe("env var parsing", () => { @@ -62,7 +59,6 @@ describe("config", () => { MDB_MCP_CONNECTION_STRING: "mongodb://user:password@host1,host2,host3/", }, cli: [], - defaults: defaultUserConfig, }); expect(actual.connectionString).toEqual("mongodb://user:password@host1,host2,host3/"); @@ -98,7 +94,6 @@ describe("config", () => { env: { [envVar]: String(value), }, - defaults: defaultUserConfig, }); expect(actual[property]).toBe(value); @@ -119,7 +114,6 @@ describe("config", () => { env: { [envVar]: "disk,mcp", }, - defaults: defaultUserConfig, }); expect(actual[config]).toEqual(["disk", "mcp"]); @@ -133,7 +127,6 @@ describe("config", () => { const actual = setupUserConfig({ cli: ["myself", "--", "--connectionString", "mongodb://user:password@host1,host2,host3/"], env: {}, - defaults: defaultUserConfig, }); expect(actual.connectionString).toEqual("mongodb://user:password@host1,host2,host3/"); @@ -163,11 +156,11 @@ describe("config", () => { }, { cli: ["--httpPort", "8080"], - expected: { httpPort: "8080" }, + expected: { httpPort: 8080 }, }, { cli: ["--idleTimeoutMs", "42"], - expected: { idleTimeoutMs: "42" }, + expected: { idleTimeoutMs: 42 }, }, { cli: ["--logPath", "/var/"], @@ -175,11 +168,11 @@ describe("config", () => { }, { cli: ["--notificationTimeoutMs", "42"], - expected: { notificationTimeoutMs: "42" }, + expected: { notificationTimeoutMs: 42 }, }, { cli: ["--atlasTemporaryDatabaseUserLifetimeMs", "12345"], - expected: { atlasTemporaryDatabaseUserLifetimeMs: "12345" }, + expected: { atlasTemporaryDatabaseUserLifetimeMs: 12345 }, }, { cli: ["--telemetry", "enabled"], @@ -227,11 +220,19 @@ describe("config", () => { }, { cli: ["--oidcRedirectUri", "https://oidc"], - expected: { oidcRedirectUri: "https://oidc" }, + expected: { oidcRedirectUri: "https://oidc", oidcRedirectUrl: "https://oidc" }, + }, + { + cli: ["--oidcRedirectUrl", "https://oidc"], + expected: { oidcRedirectUrl: "https://oidc", oidcRedirectUri: "https://oidc" }, }, { cli: ["--password", "123456"], - expected: { password: "123456" }, + expected: { password: "123456", p: "123456" }, + }, + { + cli: ["-p", "123456"], + expected: { password: "123456", p: "123456" }, }, { cli: ["--port", "27017"], @@ -295,7 +296,11 @@ describe("config", () => { }, { cli: ["--username", "admin"], - expected: { username: "admin" }, + expected: { username: "admin", u: "admin" }, + }, + { + cli: ["-u", "admin"], + expected: { username: "admin", u: "admin" }, }, ] as { cli: string[]; expected: Partial }[]; @@ -304,12 +309,12 @@ describe("config", () => { const actual = setupUserConfig({ cli: ["myself", "--", ...cli], env: {}, - defaults: defaultUserConfig, }); - for (const [key, value] of Object.entries(expected)) { - expect(actual[key as keyof UserConfig]).toBe(value); - } + expect(actual).toStrictEqual({ + ...UserConfigSchema.parse({}), + ...expected, + }); }); } }); @@ -403,7 +408,6 @@ describe("config", () => { const actual = setupUserConfig({ cli: ["myself", "--", ...cli], env: {}, - defaults: defaultUserConfig, }); for (const [key, value] of Object.entries(expected)) { @@ -430,7 +434,6 @@ describe("config", () => { const actual = setupUserConfig({ cli: ["myself", "--", ...cli], env: {}, - defaults: defaultUserConfig, }); for (const [key, value] of Object.entries(expected)) { @@ -446,7 +449,6 @@ describe("config", () => { const actual = setupUserConfig({ cli: ["myself", "--", "--connectionString", "mongodb://localhost"], env: { MDB_MCP_CONNECTION_STRING: "mongodb://crazyhost" }, - defaults: defaultUserConfig, }); expect(actual.connectionString).toBe("mongodb://localhost"); @@ -456,10 +458,6 @@ describe("config", () => { const actual = setupUserConfig({ cli: ["myself", "--", "--connectionString", "mongodb://localhost"], env: {}, - defaults: { - ...defaultUserConfig, - connectionString: "mongodb://crazyhost", - }, }); expect(actual.connectionString).toBe("mongodb://localhost"); @@ -469,10 +467,6 @@ describe("config", () => { const actual = setupUserConfig({ cli: [], env: { MDB_MCP_CONNECTION_STRING: "mongodb://localhost" }, - defaults: { - ...defaultUserConfig, - connectionString: "mongodb://crazyhost", - }, }); expect(actual.connectionString).toBe("mongodb://localhost"); @@ -484,7 +478,6 @@ describe("config", () => { const actual = setupUserConfig({ cli: ["myself", "--", "mongodb://localhost", "--connectionString", "toRemove"], env: {}, - defaults: defaultUserConfig, }); // the shell specifies directConnection=true and serverSelectionTimeoutMS=2000 by default @@ -501,7 +494,6 @@ describe("config", () => { const actual = setupUserConfig({ cli: ["myself", "--", "--transport", "http"], env: {}, - defaults: defaultUserConfig, }); expect(actual.transport).toEqual("http"); @@ -511,7 +503,6 @@ describe("config", () => { const actual = setupUserConfig({ cli: ["myself", "--", "--transport", "stdio"], env: {}, - defaults: defaultUserConfig, }); expect(actual.transport).toEqual("stdio"); @@ -522,9 +513,10 @@ describe("config", () => { setupUserConfig({ cli: ["myself", "--", "--transport", "sse"], env: {}, - defaults: defaultUserConfig, }) - ).toThrowError("Invalid transport: sse"); + ).toThrowError( + 'Invalid configuration for the following fields:\ntransport - Invalid option: expected one of "stdio"|"http"' + ); }); it("should not support arbitrary values", () => { @@ -534,9 +526,10 @@ describe("config", () => { setupUserConfig({ cli: ["myself", "--", "--transport", value], env: {}, - defaults: defaultUserConfig, }) - ).toThrowError(`Invalid transport: ${value}`); + ).toThrowError( + `Invalid configuration for the following fields:\ntransport - Invalid option: expected one of "stdio"|"http"` + ); }); }); @@ -545,7 +538,6 @@ describe("config", () => { const actual = setupUserConfig({ cli: ["myself", "--", "--telemetry", "enabled"], env: {}, - defaults: defaultUserConfig, }); expect(actual.telemetry).toEqual("enabled"); @@ -555,7 +547,6 @@ describe("config", () => { const actual = setupUserConfig({ cli: ["myself", "--", "--telemetry", "disabled"], env: {}, - defaults: defaultUserConfig, }); expect(actual.telemetry).toEqual("disabled"); @@ -566,9 +557,10 @@ describe("config", () => { setupUserConfig({ cli: ["myself", "--", "--telemetry", "true"], env: {}, - defaults: defaultUserConfig, }) - ).toThrowError("Invalid telemetry: true"); + ).toThrowError( + 'Invalid configuration for the following fields:\ntelemetry - Invalid option: expected one of "enabled"|"disabled"' + ); }); it("should not support the boolean false value", () => { @@ -576,9 +568,10 @@ describe("config", () => { setupUserConfig({ cli: ["myself", "--", "--telemetry", "false"], env: {}, - defaults: defaultUserConfig, }) - ).toThrowError("Invalid telemetry: false"); + ).toThrowError( + 'Invalid configuration for the following fields:\ntelemetry - Invalid option: expected one of "enabled"|"disabled"' + ); }); it("should not support arbitrary values", () => { @@ -588,9 +581,10 @@ describe("config", () => { setupUserConfig({ cli: ["myself", "--", "--telemetry", value], env: {}, - defaults: defaultUserConfig, }) - ).toThrowError(`Invalid telemetry: ${value}`); + ).toThrowError( + `Invalid configuration for the following fields:\ntelemetry - Invalid option: expected one of "enabled"|"disabled"` + ); }); }); @@ -600,9 +594,10 @@ describe("config", () => { setupUserConfig({ cli: ["myself", "--", "--httpPort", "0"], env: {}, - defaults: defaultUserConfig, }) - ).toThrowError("Invalid httpPort: 0"); + ).toThrowError( + "Invalid configuration for the following fields:\nhttpPort - Invalid httpPort: must be at least 1" + ); }); it("must be below 65535 (OS limit)", () => { @@ -610,9 +605,10 @@ describe("config", () => { setupUserConfig({ cli: ["myself", "--", "--httpPort", "89527345"], env: {}, - defaults: defaultUserConfig, }) - ).toThrowError("Invalid httpPort: 89527345"); + ).toThrowError( + "Invalid configuration for the following fields:\nhttpPort - Invalid httpPort: must be at most 65535" + ); }); it("should not support non numeric values", () => { @@ -620,19 +616,19 @@ describe("config", () => { setupUserConfig({ cli: ["myself", "--", "--httpPort", "portAventura"], env: {}, - defaults: defaultUserConfig, }) - ).toThrowError("Invalid httpPort: portAventura"); + ).toThrowError( + "Invalid configuration for the following fields:\nhttpPort - Invalid input: expected number, received NaN" + ); }); it("should support numeric values", () => { const actual = setupUserConfig({ cli: ["myself", "--", "--httpPort", "8888"], env: {}, - defaults: defaultUserConfig, }); - expect(actual.httpPort).toEqual("8888"); + expect(actual.httpPort).toEqual(8888); }); }); @@ -642,9 +638,8 @@ describe("config", () => { setupUserConfig({ cli: ["myself", "--", "--loggers", ""], env: {}, - defaults: defaultUserConfig, }) - ).toThrowError("No loggers found in config"); + ).toThrowError("Invalid configuration for the following fields:\nloggers - Cannot be an empty array"); }); it("must not allow duplicates", () => { @@ -652,16 +647,16 @@ describe("config", () => { setupUserConfig({ cli: ["myself", "--", "--loggers", "disk,disk,disk"], env: {}, - defaults: defaultUserConfig, }) - ).toThrowError("Duplicate loggers found in config"); + ).toThrowError( + "Invalid configuration for the following fields:\nloggers - Duplicate loggers found in config" + ); }); it("allows mcp logger", () => { const actual = setupUserConfig({ cli: ["myself", "--", "--loggers", "mcp"], env: {}, - defaults: defaultUserConfig, }); expect(actual.loggers).toEqual(["mcp"]); @@ -671,7 +666,6 @@ describe("config", () => { const actual = setupUserConfig({ cli: ["myself", "--", "--loggers", "disk"], env: {}, - defaults: defaultUserConfig, }); expect(actual.loggers).toEqual(["disk"]); @@ -681,7 +675,6 @@ describe("config", () => { const actual = setupUserConfig({ cli: ["myself", "--", "--loggers", "stderr"], env: {}, - defaults: defaultUserConfig, }); expect(actual.loggers).toEqual(["stderr"]); diff --git a/tests/unit/common/roles.test.ts b/tests/unit/common/roles.test.ts index 058e605ab..e9eac0f24 100644 --- a/tests/unit/common/roles.test.ts +++ b/tests/unit/common/roles.test.ts @@ -1,11 +1,9 @@ import { describe, it, expect } from "vitest"; import { getDefaultRoleFromConfig } from "../../../src/common/atlas/roles.js"; -import { defaultUserConfig, type UserConfig } from "../../../src/common/config.js"; +import { UserConfigSchema, type UserConfig } from "../../../src/common/config.js"; describe("getDefaultRoleFromConfig", () => { - const defaultConfig: UserConfig = { - ...defaultUserConfig, - }; + const defaultConfig: UserConfig = UserConfigSchema.parse({}); const readOnlyConfig: UserConfig = { ...defaultConfig, From c98c875303cc62ea928b0494a71d087276436f58 Mon Sep 17 00:00:00 2001 From: gagik Date: Fri, 31 Oct 2025 13:14:49 +0100 Subject: [PATCH 08/12] chore: bring back disableEmbeddingsValidation default --- README.md | 2 +- src/common/config.ts | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 100e4770c..9b7af8f81 100644 --- a/README.md +++ b/README.md @@ -354,7 +354,7 @@ The MongoDB MCP Server can be configured using multiple methods, with the follow | `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` | `` | When set to true, disables validation of embeddings dimensions. | +| `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. | diff --git a/src/common/config.ts b/src/common/config.ts index 1e0a9cbf2..224c53e34 100644 --- a/src/common/config.ts +++ b/src/common/config.ts @@ -158,14 +158,13 @@ export const UserConfigSchema = z4.object({ .register(configRegistry, { isSecret: true }), disableEmbeddingsValidation: z4 .boolean() - .optional() + .default(false) .describe("When set to true, disables validation of embeddings dimensions."), vectorSearchDimensions: z4.coerce .number() .default(1024) .describe("Default number of dimensions for vector search embeddings."), vectorSearchSimilarityFunction: similarityEnumV4 - .optional() .default("euclidean") .describe("Default similarity function for vector search: 'euclidean', 'cosine', or 'dotProduct'."), previewFeatures: z4 From 5e865b5e96684fba86fa6eab471609d0556da261 Mon Sep 17 00:00:00 2001 From: gagik Date: Fri, 31 Oct 2025 13:27:55 +0100 Subject: [PATCH 09/12] chore: format, skip check on CJS compile --- src/lib.ts | 2 +- tests/integration/build.test.ts | 1 - tsconfig.cjs.json | 1 + 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib.ts b/src/lib.ts index c81472e0b..a9d5cfed6 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -1,6 +1,6 @@ export { Server, type ServerOptions } from "./server.js"; export { Session, type SessionOptions } from "./common/session.js"; -export { defaultUserConfig, type UserConfig, ALL_CONFIG_KEYS as configurableProperties } from "./common/config.js"; +export { type UserConfig } from "./common/config.js"; export { LoggerBase, type LogPayload, type LoggerType, type LogLevel } from "./common/logger.js"; export { StreamableHttpRunner } from "./transports/streamableHttp.js"; export { diff --git a/tests/integration/build.test.ts b/tests/integration/build.test.ts index 7453cb3d9..064af001a 100644 --- a/tests/integration/build.test.ts +++ b/tests/integration/build.test.ts @@ -49,7 +49,6 @@ describe("Build Test", () => { "Session", "StreamableHttpRunner", "Telemetry", - "defaultUserConfig", ]) ); }); diff --git a/tsconfig.cjs.json b/tsconfig.cjs.json index ad8b38322..b31f849e5 100644 --- a/tsconfig.cjs.json +++ b/tsconfig.cjs.json @@ -1,6 +1,7 @@ { "extends": "./tsconfig.build.json", "compilerOptions": { + "noCheck": true, "module": "commonjs", "moduleResolution": "node", "outDir": "./dist/cjs" From 2813618ab97370e4564c5b53044345b6f1c5fef4 Mon Sep 17 00:00:00 2001 From: gagik Date: Fri, 31 Oct 2025 13:35:02 +0100 Subject: [PATCH 10/12] chore: remove export, add disableEmbeddingsValidation to test --- src/common/config.ts | 1 - tests/unit/common/config.test.ts | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/config.ts b/src/common/config.ts index 224c53e34..0c5c7a5bc 100644 --- a/src/common/config.ts +++ b/src/common/config.ts @@ -179,7 +179,6 @@ export const UserConfigSchema = z4.object({ export type Similarity = z4.infer["vectorSearchSimilarityFunction"]; export type PreviewFeature = z4.infer["previewFeatures"][number]; export type UserConfig = z4.infer & CliOptions; -export type Logger = UserConfig["loggers"][number]; export const config = setupUserConfig({ cli: process.argv, diff --git a/tests/unit/common/config.test.ts b/tests/unit/common/config.test.ts index 89eb6af4a..5c671ca7d 100644 --- a/tests/unit/common/config.test.ts +++ b/tests/unit/common/config.test.ts @@ -45,6 +45,7 @@ describe("config", () => { voyageApiKey: "", vectorSearchDimensions: 1024, vectorSearchSimilarityFunction: "euclidean", + disableEmbeddingsValidation: false, previewFeatures: [], }; From 80e02c00728847710a651d6a8137f608a1ef5375 Mon Sep 17 00:00:00 2001 From: gagik Date: Fri, 31 Oct 2025 13:51:09 +0100 Subject: [PATCH 11/12] chore: fix build with CJS --- src/common/config.ts | 10 ++++------ src/common/schemas.ts | 9 ++++----- src/common/search/vectorSearchEmbeddingsManager.ts | 3 ++- src/tools/mongodb/create/createIndex.ts | 5 +++-- src/tools/tool.ts | 3 ++- tests/unit/toolBase.test.ts | 3 ++- tsconfig.cjs.json | 1 - 7 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/common/config.ts b/src/common/config.ts index 0c5c7a5bc..e7ece0223 100644 --- a/src/common/config.ts +++ b/src/common/config.ts @@ -3,7 +3,6 @@ import type { CliOptions, ConnectionInfo } from "@mongosh/arg-parser"; import { generateConnectionInfoFromCliArgs } from "@mongosh/arg-parser"; import { Keychain } from "./keychain.js"; import type { Secret } from "./keychain.js"; - import { z as z4 } from "zod/v4"; import { commaSeparatedToArray, @@ -14,7 +13,7 @@ import { validateConfigKey, } from "./configUtils.js"; import { OPTIONS } from "./argsParserOptions.js"; -import { similarityEnumV4 } from "./schemas.js"; +import { similarityValues, previewFeatureValues } from "./schemas.js"; export const configRegistry = z4.registry(); @@ -164,20 +163,19 @@ export const UserConfigSchema = z4.object({ .number() .default(1024) .describe("Default number of dimensions for vector search embeddings."), - vectorSearchSimilarityFunction: similarityEnumV4 + vectorSearchSimilarityFunction: z4 + .enum(similarityValues) .default("euclidean") .describe("Default similarity function for vector search: 'euclidean', 'cosine', or 'dotProduct'."), previewFeatures: z4 .preprocess( (val: string | string[] | undefined) => commaSeparatedToArray(val), - z4.array(z4.enum(["vectorSearch"])) + z4.array(z4.enum(previewFeatureValues)) ) .default([]) .describe("An array of preview features that are enabled."), }); -export type Similarity = z4.infer["vectorSearchSimilarityFunction"]; -export type PreviewFeature = z4.infer["previewFeatures"][number]; export type UserConfig = z4.infer & CliOptions; export const config = setupUserConfig({ diff --git a/src/common/schemas.ts b/src/common/schemas.ts index fef0ba6dc..6375d25c6 100644 --- a/src/common/schemas.ts +++ b/src/common/schemas.ts @@ -1,7 +1,6 @@ -import z4 from "zod/v4"; -import z3 from "zod/v3"; +export const previewFeatureValues = ["vectorSearch"] as const; +export type PreviewFeature = (typeof previewFeatureValues)[number]; -const similarityValues = ["cosine", "euclidean", "dotProduct"] as const; +export const similarityValues = ["cosine", "euclidean", "dotProduct"] as const; -export const similarityEnumV4 = z4.enum(similarityValues); -export const similarityEnum = z3.enum(similarityValues); +export type Similarity = (typeof similarityValues)[number]; diff --git a/src/common/search/vectorSearchEmbeddingsManager.ts b/src/common/search/vectorSearchEmbeddingsManager.ts index 20fc15426..e570f064b 100644 --- a/src/common/search/vectorSearchEmbeddingsManager.ts +++ b/src/common/search/vectorSearchEmbeddingsManager.ts @@ -1,12 +1,13 @@ import type { NodeDriverServiceProvider } from "@mongosh/service-provider-node-driver"; import { BSON, type Document } from "bson"; -import type { Similarity, UserConfig } from "../config.js"; +import type { UserConfig } from "../config.js"; import type { ConnectionManager } from "../connectionManager.js"; import z from "zod"; import { ErrorCodes, MongoDBError } from "../errors.js"; import { getEmbeddingsProvider } from "./embeddingsProvider.js"; import type { EmbeddingParameters, SupportedEmbeddingParameters } from "./embeddingsProvider.js"; import { formatUntrustedData } from "../../tools/tool.js"; +import type { Similarity } from "../schemas.js"; export const quantizationEnum = z.enum(["none", "scalar", "binary"]); export type Quantization = z.infer; diff --git a/src/tools/mongodb/create/createIndex.ts b/src/tools/mongodb/create/createIndex.ts index 0532453c2..b9cc517fc 100644 --- a/src/tools/mongodb/create/createIndex.ts +++ b/src/tools/mongodb/create/createIndex.ts @@ -4,7 +4,7 @@ import { DbOperationArgs, MongoDBToolBase } from "../mongodbTool.js"; import { type ToolArgs, type OperationType } from "../../tool.js"; import type { IndexDirection } from "mongodb"; import { quantizationEnum } from "../../../common/search/vectorSearchEmbeddingsManager.js"; -import { similarityEnum } from "../../../common/schemas.js"; +import { similarityValues, type Similarity } from "../../../common/schemas.js"; export class CreateIndexTool extends MongoDBToolBase { private vectorSearchIndexDefinition = z.object({ @@ -39,7 +39,8 @@ export class CreateIndexTool extends MongoDBToolBase { .describe( "Number of vector dimensions that MongoDB Vector Search enforces at index-time and query-time" ), - similarity: similarityEnum + similarity: z + .enum(similarityValues) .default(this.config.vectorSearchSimilarityFunction) .describe( "Vector similarity function to use to search for top K-nearest neighbors. You can set this field only for vector-type fields." diff --git a/src/tools/tool.ts b/src/tools/tool.ts index ec9f01a61..f8d594f78 100644 --- a/src/tools/tool.ts +++ b/src/tools/tool.ts @@ -6,9 +6,10 @@ import type { Session } from "../common/session.js"; import { LogId } from "../common/logger.js"; import type { Telemetry } from "../telemetry/telemetry.js"; import { type ToolEvent } from "../telemetry/types.js"; -import type { PreviewFeature, UserConfig } from "../common/config.js"; +import type { UserConfig } from "../common/config.js"; import type { Server } from "../server.js"; import type { Elicitation } from "../elicitation.js"; +import type { PreviewFeature } from "../common/schemas.js"; export type ToolArgs = z.objectOutputType; export type ToolCallbackArgs = Parameters>; diff --git a/tests/unit/toolBase.test.ts b/tests/unit/toolBase.test.ts index 984aa5bfb..9f45eb551 100644 --- a/tests/unit/toolBase.test.ts +++ b/tests/unit/toolBase.test.ts @@ -3,11 +3,12 @@ import { z } from "zod"; import { ToolBase, type OperationType, type ToolCategory, type ToolConstructorParams } from "../../src/tools/tool.js"; import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import type { Session } from "../../src/common/session.js"; -import type { PreviewFeature, UserConfig } from "../../src/common/config.js"; +import type { UserConfig } from "../../src/common/config.js"; import type { Telemetry } from "../../src/telemetry/telemetry.js"; import type { Elicitation } from "../../src/elicitation.js"; import type { CompositeLogger } from "../../src/common/logger.js"; import type { TelemetryToolMetadata, ToolCallbackArgs } from "../../src/tools/tool.js"; +import type { PreviewFeature } from "../../src/common/schemas.js"; describe("ToolBase", () => { let mockSession: Session; diff --git a/tsconfig.cjs.json b/tsconfig.cjs.json index b31f849e5..ad8b38322 100644 --- a/tsconfig.cjs.json +++ b/tsconfig.cjs.json @@ -1,7 +1,6 @@ { "extends": "./tsconfig.build.json", "compilerOptions": { - "noCheck": true, "module": "commonjs", "moduleResolution": "node", "outDir": "./dist/cjs" From 5ba672190dfe3af1a7d554e2a65562b5b92adaa4 Mon Sep 17 00:00:00 2001 From: gagik Date: Fri, 31 Oct 2025 14:23:04 +0100 Subject: [PATCH 12/12] chore: remove unused import --- src/tools/mongodb/create/createIndex.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tools/mongodb/create/createIndex.ts b/src/tools/mongodb/create/createIndex.ts index b9cc517fc..68ad4d918 100644 --- a/src/tools/mongodb/create/createIndex.ts +++ b/src/tools/mongodb/create/createIndex.ts @@ -4,7 +4,7 @@ import { DbOperationArgs, MongoDBToolBase } from "../mongodbTool.js"; import { type ToolArgs, type OperationType } from "../../tool.js"; import type { IndexDirection } from "mongodb"; import { quantizationEnum } from "../../../common/search/vectorSearchEmbeddingsManager.js"; -import { similarityValues, type Similarity } from "../../../common/schemas.js"; +import { similarityValues } from "../../../common/schemas.js"; export class CreateIndexTool extends MongoDBToolBase { private vectorSearchIndexDefinition = z.object({