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..c631412f9 --- /dev/null +++ b/eslint-rules/enforce-zod-v4.js @@ -0,0 +1,53 @@ +"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. 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, + 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 allow zod v4 import 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; + } + + const isZodV4Import = importPath === "zod/v4"; + + if (isZodV4Import) { + context.report({ + node, + messageId: "enforceZodV4", + data: { + importPath, + }, + }); + } + }, + }; + }, +}; diff --git a/eslint-rules/enforce-zod-v4.test.js b/eslint-rules/enforce-zod-v4.test.js new file mode 100644 index 000000000..835fd86a2 --- /dev/null +++ b/eslint-rules/enforce-zod-v4.test.js @@ -0,0 +1,153 @@ +import path from "path"; +import { RuleTester } from "eslint"; +import { describe, it } from "vitest"; +import tsParser from "@typescript-eslint/parser"; +import rule from "./enforce-zod-v4.js"; + +const ROOT = process.cwd(); +const resolve = (p) => path.resolve(ROOT, p); + +const ruleTester = new RuleTester({ + languageOptions: { + parser: tsParser, + parserOptions: { ecmaVersion: 2022, sourceType: "module" }, + }, +}); + +describe("enforce-zod-v4", () => { + it("should allow zod/v4 imports in config.ts", () => { + ruleTester.run("enforce-zod-v4", rule, { + valid: [ + { + filename: resolve("src/common/config.ts"), + code: 'import { z } from "zod/v4";\n', + }, + { + filename: resolve("src/common/config.ts"), + code: 'import * as z from "zod/v4";\n', + }, + { + filename: resolve("src/common/config.ts"), + code: 'import type { ZodType } from "zod/v4";\n', + }, + ], + invalid: [], + }); + }); + + it("should allow regular zod imports in other files", () => { + ruleTester.run("enforce-zod-v4", rule, { + valid: [ + { + filename: resolve("src/tools/tool.ts"), + code: 'import { z } from "zod";\n', + }, + { + filename: resolve("src/resources/resource.ts"), + code: 'import * as z from "zod";\n', + }, + { + filename: resolve("src/some/module.ts"), + code: 'import type { ZodType } from "zod";\n', + }, + ], + invalid: [], + }); + }); + + it("should allow non-zod imports in any file", () => { + ruleTester.run("enforce-zod-v4", rule, { + valid: [ + { + filename: resolve("src/tools/tool.ts"), + code: 'import { something } from "some-package";\n', + }, + { + filename: resolve("src/common/config.ts"), + code: 'import path from "path";\n', + }, + { + filename: resolve("src/resources/resource.ts"), + code: 'import { Logger } from "./logger.js";\n', + }, + ], + invalid: [], + }); + }); + + it("should report error when zod/v4 is imported in files other than config.ts", () => { + ruleTester.run("enforce-zod-v4", rule, { + valid: [], + invalid: [ + { + filename: resolve("src/tools/tool.ts"), + code: 'import { z } from "zod/v4";\n', + errors: [ + { + messageId: "enforceZodV4", + data: { importPath: "zod/v4" }, + }, + ], + }, + { + filename: resolve("src/resources/resource.ts"), + code: 'import * as z from "zod/v4";\n', + errors: [ + { + messageId: "enforceZodV4", + data: { importPath: "zod/v4" }, + }, + ], + }, + { + filename: resolve("src/some/module.ts"), + code: 'import type { ZodType } from "zod/v4";\n', + errors: [ + { + messageId: "enforceZodV4", + data: { importPath: "zod/v4" }, + }, + ], + }, + { + filename: resolve("tests/unit/toolBase.test.ts"), + code: 'import { z } from "zod/v4";\n', + errors: [ + { + messageId: "enforceZodV4", + data: { importPath: "zod/v4" }, + }, + ], + }, + ], + }); + }); + + it("should handle multiple imports in a single file", () => { + ruleTester.run("enforce-zod-v4", rule, { + valid: [ + { + filename: resolve("src/common/config.ts"), + code: `import { z } from "zod/v4"; +import path from "path"; +import type { UserConfig } from "./types.js"; +`, + }, + ], + invalid: [ + { + filename: resolve("src/tools/tool.ts"), + code: `import { z } from "zod/v4"; +import path from "path"; +`, + errors: [ + { + messageId: "enforceZodV4", + data: { importPath: "zod/v4" }, + }, + ], + }, + ], + }); + }); +}); 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..414d06ba5 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,79 @@ 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 { + // 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}`); + } + } + } + + 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..902fbcc2a 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,64 @@ 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. + */ + 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; +} + +/** + * 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 +228,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/",