diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index ca63cfae1..25439dfe1 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -35,6 +35,11 @@ jobs: run: | echo "NEW_VERSION=$(npm version ${{ inputs.version }} --no-git-tag-version)" >> $GITHUB_OUTPUT npm run build:update-package-version + + - name: Update server.json version and arguments + run: | + npm run generate:arguments + - name: Create release PR uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # 7.0.8 id: create-pr diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 978198b87..8cbbda6f0 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -80,6 +80,11 @@ jobs: if: needs.check.outputs.VERSION_EXISTS == 'false' steps: - uses: GitHubSecurityLab/actions-permissions/monitor@v1 + - uses: mongodb-js/devtools-shared/actions/setup-bot-token@main + id: app-token + with: + app-id: ${{ vars.DEVTOOLS_BOT_APP_ID }} + private-key: ${{ secrets.DEVTOOLS_BOT_PRIVATE_KEY }} - uses: actions/checkout@v5 - uses: actions/setup-node@v6 with: @@ -95,8 +100,19 @@ jobs: run: npm publish --tag ${{ needs.check.outputs.RELEASE_CHANNEL }} env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + - name: Publish git release env: GH_TOKEN: ${{ github.token }} run: | gh release create ${{ needs.check.outputs.VERSION }} --title "${{ needs.check.outputs.VERSION }}" --generate-notes --target ${{ github.sha }} ${{ (needs.check.outputs.RELEASE_CHANNEL != 'latest' && '--prerelease') || ''}} + + - name: Install MCP Publisher + run: | + curl -L "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/').tar.gz" | tar xz mcp-publisher + + - name: Login to MCP Registry + run: ./mcp-publisher login github --token ${{ steps.app-token.outputs.token }} + + - name: Publish to MCP Registry + run: ./mcp-publisher publish diff --git a/Dockerfile b/Dockerfile index 6d692a4dd..6c206d524 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,3 +9,4 @@ ENTRYPOINT ["mongodb-mcp-server"] LABEL maintainer="MongoDB Inc " LABEL description="MongoDB MCP Server" LABEL version=${VERSION} +LABEL io.modelcontextprotocol.server.name="io.github.mongodb-js/mongodb-mcp-server" diff --git a/package.json b/package.json index 309cb643f..9ce335d40 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "description": "MongoDB Model Context Protocol Server", "version": "1.1.0", "type": "module", + "mcpName": "io.github.mongodb-js/mongodb-mcp-server", "exports": { ".": { "import": { @@ -51,7 +52,8 @@ "fix": "npm run fix:lint && npm run reformat", "fix:lint": "eslint . --fix", "reformat": "prettier --write .", - "generate": "./scripts/generate.sh", + "generate": "./scripts/generate.sh && npm run generate:arguments", + "generate:arguments": "tsx scripts/generateArguments.ts", "test": "vitest --project eslint-rules --project unit-and-integration --coverage", "pretest:accuracy": "npm run build", "test:accuracy": "sh ./scripts/accuracy/runAccuracyTests.sh", diff --git a/scripts/generateArguments.ts b/scripts/generateArguments.ts new file mode 100644 index 000000000..a5a4c64d7 --- /dev/null +++ b/scripts/generateArguments.ts @@ -0,0 +1,233 @@ +#!/usr/bin/env tsx + +/** + * This script generates argument definitions and updates: + * - server.json arrays + * - TODO: README.md configuration table + * + * It uses the Zod schema and OPTIONS defined in src/common/config.ts + */ + +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"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +function camelCaseToSnakeCase(str: string): string { + return str.replace(/[A-Z]/g, (letter) => `_${letter}`).toUpperCase(); +} + +// List of configuration keys 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([ + "connectionString", + "username", + "password", + "apiClientId", + "apiClientSecret", + "tlsCAFile", + "tlsCertificateKeyFile", + "tlsCertificateKeyFilePassword", + "tlsCRLFile", + "sslCAFile", + "sslPEMKeyFile", + "sslPEMKeyPassword", + "sslCRLFile", + "voyageApiKey", +]); + +interface EnvironmentVariable { + name: string; + description: string; + isRequired: boolean; + format: string; + isSecret: boolean; + configKey: string; + defaultValue?: unknown; +} + +interface ConfigMetadata { + description: string; + defaultValue?: unknown; +} + +function extractZodDescriptions(): Record { + const result: Record = {}; + + // Get the shape of the Zod schema + const shape = (UserConfigSchema as ZodObject).shape; + + for (const [key, fieldSchema] of Object.entries(shape)) { + const schema = fieldSchema; + // Extract description from Zod schema + const description = schema.description || `Configuration option: ${key}`; + + // 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; + } + + result[key] = { + description, + defaultValue, + }; + } + + return result; +} + +function generateEnvironmentVariables( + options: typeof OPTIONS, + zodMetadata: Record +): EnvironmentVariable[] { + const envVars: EnvironmentVariable[] = []; + const processedKeys = new Set(); + + // Helper to add env var + const addEnvVar = (key: string, type: "string" | "number" | "boolean" | "array"): void => { + if (processedKeys.has(key)) return; + processedKeys.add(key); + + const envVarName = `MDB_MCP_${camelCaseToSnakeCase(key)}`; + + // Get description and default value from Zod metadata + const metadata = zodMetadata[key] || { + description: `Configuration option: ${key}`, + }; + + // Determine format based on type + let format = type; + if (type === "array") { + format = "string"; // Arrays are passed as comma-separated strings + } + + envVars.push({ + name: envVarName, + description: metadata.description, + isRequired: false, + format: format, + isSecret: SECRET_CONFIG_KEYS.has(key), + configKey: key, + defaultValue: metadata.defaultValue, + }); + }; + + // Process all string options + for (const key of options.string) { + addEnvVar(key, "string"); + } + + // Process all number options + for (const key of options.number) { + addEnvVar(key, "number"); + } + + // Process all boolean options + for (const key of options.boolean) { + addEnvVar(key, "boolean"); + } + + // Process all array options + for (const key of options.array) { + addEnvVar(key, "array"); + } + + // Sort by name for consistent output + return envVars.sort((a, b) => a.name.localeCompare(b.name)); +} + +function generatePackageArguments(envVars: EnvironmentVariable[]): unknown[] { + const packageArguments: unknown[] = []; + + // Generate positional arguments from the same config options (only documented ones) + const documentedVars = envVars.filter((v) => !v.description.startsWith("Configuration option:")); + + // Generate named arguments from the same config options + for (const argument of documentedVars) { + const arg: Record = { + type: "named", + name: "--" + argument.configKey, + description: argument.description, + isRequired: argument.isRequired, + }; + + // Add format if it's not string (string is the default) + if (argument.format !== "string") { + arg.format = argument.format; + } + + packageArguments.push(arg); + } + + return packageArguments; +} + +function updateServerJsonEnvVars(envVars: EnvironmentVariable[]): void { + const serverJsonPath = join(__dirname, "..", "server.json"); + const packageJsonPath = join(__dirname, "..", "package.json"); + + const content = readFileSync(serverJsonPath, "utf-8"); + const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8")) as { version: string }; + const serverJson = JSON.parse(content) as { + version?: string; + packages: { + registryType?: string; + identifier?: string; + environmentVariables: EnvironmentVariable[]; + packageArguments?: unknown[]; + version?: string; + }[]; + }; + + // Get version from package.json + const version = packageJson.version; + + // Generate environment variables array (only documented ones) + const documentedVars = envVars.filter((v) => !v.description.startsWith("Configuration option:")); + const envVarsArray = documentedVars.map((v) => ({ + name: v.name, + description: v.description, + isRequired: v.isRequired, + format: v.format, + isSecret: v.isSecret, + })); + + // Generate package arguments (named arguments in camelCase) + const packageArguments = generatePackageArguments(envVars); + + // Update version at root level + serverJson.version = process.env.VERSION || version; + + // 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.packageArguments = packageArguments; + pkg.version = version; + + // Update OCI identifier version tag if this is an OCI package + if (pkg.registryType === "oci" && pkg.identifier) { + // Replace the version tag in the OCI identifier (e.g., docker.io/mongodb/mongodb-mcp-server:1.0.0) + pkg.identifier = pkg.identifier.replace(/:[^:]+$/, `:${version}`); + } + } + } + + writeFileSync(serverJsonPath, JSON.stringify(serverJson, null, 2) + "\n", "utf-8"); + console.log(`✓ Updated server.json (version ${version})`); +} + +function main(): void { + const zodMetadata = extractZodDescriptions(); + + const envVars = generateEnvironmentVariables(OPTIONS, zodMetadata); + updateServerJsonEnvVars(envVars); +} + +main(); diff --git a/server.json b/server.json new file mode 100644 index 000000000..7ac3f9f2c --- /dev/null +++ b/server.json @@ -0,0 +1,644 @@ +{ + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-10-17/server.schema.json", + "name": "io.github.mongodb-js/mongodb-mcp-server", + "description": "MongoDB Model Context Protocol Server", + "repository": { + "url": "https://github.com/mongodb-js/mongodb-mcp-server", + "source": "github" + }, + "version": "1.1.0", + "packages": [ + { + "registryType": "npm", + "identifier": "mongodb-mcp-server", + "version": "1.1.0", + "transport": { + "type": "stdio" + }, + "environmentVariables": [ + { + "name": "MDB_MCP_API_CLIENT_ID", + "description": "Atlas API client ID for authentication. Required for running Atlas tools.", + "isRequired": false, + "format": "string", + "isSecret": true + }, + { + "name": "MDB_MCP_API_CLIENT_SECRET", + "description": "Atlas API client secret for authentication. Required for running Atlas tools.", + "isRequired": false, + "format": "string", + "isSecret": true + }, + { + "name": "MDB_MCP_ATLAS_TEMPORARY_DATABASE_USER_LIFETIME_MS", + "description": "Time in milliseconds that temporary database users created when connecting to MongoDB Atlas clusters will remain active before being automatically deleted.", + "isRequired": false, + "format": "string", + "isSecret": false + }, + { + "name": "MDB_MCP_CONFIRMATION_REQUIRED_TOOLS", + "description": "An array of tool names that require user confirmation before execution. Requires the client to support elicitation.", + "isRequired": false, + "format": "string", + "isSecret": false + }, + { + "name": "MDB_MCP_CONNECTION_STRING", + "description": "MongoDB connection string for direct database connections. Optional, if not set, you'll need to call the connect tool before interacting with MongoDB data.", + "isRequired": false, + "format": "string", + "isSecret": true + }, + { + "name": "MDB_MCP_DISABLE_EMBEDDINGS_VALIDATION", + "description": "When set to true, disables validation of embeddings dimensions.", + "isRequired": false, + "format": "boolean", + "isSecret": false + }, + { + "name": "MDB_MCP_DISABLED_TOOLS", + "description": "An array of tool names, operation types, and/or categories of tools that will be disabled.", + "isRequired": false, + "format": "string", + "isSecret": false + }, + { + "name": "MDB_MCP_EXPORT_CLEANUP_INTERVAL_MS", + "description": "Time in milliseconds between export cleanup cycles that remove expired export files.", + "isRequired": false, + "format": "string", + "isSecret": false + }, + { + "name": "MDB_MCP_EXPORT_TIMEOUT_MS", + "description": "Time in milliseconds after which an export is considered expired and eligible for cleanup.", + "isRequired": false, + "format": "string", + "isSecret": false + }, + { + "name": "MDB_MCP_EXPORTS_PATH", + "description": "Folder to store exported data files.", + "isRequired": false, + "format": "string", + "isSecret": false + }, + { + "name": "MDB_MCP_HTTP_HOST", + "description": "Host to bind the http server.", + "isRequired": false, + "format": "string", + "isSecret": false + }, + { + "name": "MDB_MCP_HTTP_PORT", + "description": "Port number.", + "isRequired": false, + "format": "string", + "isSecret": false + }, + { + "name": "MDB_MCP_IDLE_TIMEOUT_MS", + "description": "Idle timeout for a client to disconnect (only applies to http transport).", + "isRequired": false, + "format": "string", + "isSecret": false + }, + { + "name": "MDB_MCP_INDEX_CHECK", + "description": "When set to true, enforces that query operations must use an index, rejecting queries that perform a collection scan.", + "isRequired": false, + "format": "boolean", + "isSecret": false + }, + { + "name": "MDB_MCP_LOG_PATH", + "description": "Folder to store logs.", + "isRequired": false, + "format": "string", + "isSecret": false + }, + { + "name": "MDB_MCP_LOGGERS", + "description": "Comma separated values, possible values are 'mcp', 'disk' and 'stderr'.", + "isRequired": false, + "format": "string", + "isSecret": false + }, + { + "name": "MDB_MCP_MAX_BYTES_PER_QUERY", + "description": "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.", + "isRequired": false, + "format": "number", + "isSecret": false + }, + { + "name": "MDB_MCP_MAX_DOCUMENTS_PER_QUERY", + "description": "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.", + "isRequired": false, + "format": "number", + "isSecret": false + }, + { + "name": "MDB_MCP_NOTIFICATION_TIMEOUT_MS", + "description": "Notification timeout for a client to be aware of disconnect (only applies to http transport).", + "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.", + "isRequired": false, + "format": "boolean", + "isSecret": false + }, + { + "name": "MDB_MCP_TELEMETRY", + "description": "When set to disabled, disables telemetry collection.", + "isRequired": false, + "format": "string", + "isSecret": false + }, + { + "name": "MDB_MCP_TRANSPORT", + "description": "Either 'stdio' or 'http'.", + "isRequired": false, + "format": "string", + "isSecret": false + }, + { + "name": "MDB_MCP_VOYAGE_API_KEY", + "description": "API key for Voyage AI embeddings service (required for vector search operations with text-to-embedding conversion).", + "isRequired": false, + "format": "string", + "isSecret": true + } + ], + "packageArguments": [ + { + "type": "named", + "name": "--apiClientId", + "description": "Atlas API client ID for authentication. Required for running Atlas tools.", + "isRequired": false + }, + { + "type": "named", + "name": "--apiClientSecret", + "description": "Atlas API client secret for authentication. Required for running Atlas tools.", + "isRequired": false + }, + { + "type": "named", + "name": "--atlasTemporaryDatabaseUserLifetimeMs", + "description": "Time in milliseconds that temporary database users created when connecting to MongoDB Atlas clusters will remain active before being automatically deleted.", + "isRequired": false + }, + { + "type": "named", + "name": "--confirmationRequiredTools", + "description": "An array of tool names that require user confirmation before execution. Requires the client to support elicitation.", + "isRequired": false + }, + { + "type": "named", + "name": "--connectionString", + "description": "MongoDB connection string for direct database connections. Optional, if not set, you'll need to call the connect tool before interacting with MongoDB data.", + "isRequired": false + }, + { + "type": "named", + "name": "--disableEmbeddingsValidation", + "description": "When set to true, disables validation of embeddings dimensions.", + "isRequired": false, + "format": "boolean" + }, + { + "type": "named", + "name": "--disabledTools", + "description": "An array of tool names, operation types, and/or categories of tools that will be disabled.", + "isRequired": false + }, + { + "type": "named", + "name": "--exportCleanupIntervalMs", + "description": "Time in milliseconds between export cleanup cycles that remove expired export files.", + "isRequired": false + }, + { + "type": "named", + "name": "--exportTimeoutMs", + "description": "Time in milliseconds after which an export is considered expired and eligible for cleanup.", + "isRequired": false + }, + { + "type": "named", + "name": "--exportsPath", + "description": "Folder to store exported data files.", + "isRequired": false + }, + { + "type": "named", + "name": "--httpHost", + "description": "Host to bind the http server.", + "isRequired": false + }, + { + "type": "named", + "name": "--httpPort", + "description": "Port number.", + "isRequired": false + }, + { + "type": "named", + "name": "--idleTimeoutMs", + "description": "Idle timeout for a client to disconnect (only applies to http transport).", + "isRequired": false + }, + { + "type": "named", + "name": "--indexCheck", + "description": "When set to true, enforces that query operations must use an index, rejecting queries that perform a collection scan.", + "isRequired": false, + "format": "boolean" + }, + { + "type": "named", + "name": "--logPath", + "description": "Folder to store logs.", + "isRequired": false + }, + { + "type": "named", + "name": "--loggers", + "description": "Comma separated values, possible values are 'mcp', 'disk' and 'stderr'.", + "isRequired": false + }, + { + "type": "named", + "name": "--maxBytesPerQuery", + "description": "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.", + "isRequired": false, + "format": "number" + }, + { + "type": "named", + "name": "--maxDocumentsPerQuery", + "description": "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.", + "isRequired": false, + "format": "number" + }, + { + "type": "named", + "name": "--notificationTimeoutMs", + "description": "Notification timeout for a client to be aware of disconnect (only applies to http transport).", + "isRequired": false + }, + { + "type": "named", + "name": "--readOnly", + "description": "When set to true, only allows read, connect, and metadata operation types, disabling create/update/delete operations.", + "isRequired": false, + "format": "boolean" + }, + { + "type": "named", + "name": "--telemetry", + "description": "When set to disabled, disables telemetry collection.", + "isRequired": false + }, + { + "type": "named", + "name": "--transport", + "description": "Either 'stdio' or 'http'.", + "isRequired": false + }, + { + "type": "named", + "name": "--voyageApiKey", + "description": "API key for Voyage AI embeddings service (required for vector search operations with text-to-embedding conversion).", + "isRequired": false + } + ] + }, + { + "registryType": "oci", + "identifier": "docker.io/mongodb/mongodb-mcp-server:1.1.0", + "transport": { + "type": "stdio" + }, + "environmentVariables": [ + { + "name": "MDB_MCP_API_CLIENT_ID", + "description": "Atlas API client ID for authentication. Required for running Atlas tools.", + "isRequired": false, + "format": "string", + "isSecret": true + }, + { + "name": "MDB_MCP_API_CLIENT_SECRET", + "description": "Atlas API client secret for authentication. Required for running Atlas tools.", + "isRequired": false, + "format": "string", + "isSecret": true + }, + { + "name": "MDB_MCP_ATLAS_TEMPORARY_DATABASE_USER_LIFETIME_MS", + "description": "Time in milliseconds that temporary database users created when connecting to MongoDB Atlas clusters will remain active before being automatically deleted.", + "isRequired": false, + "format": "string", + "isSecret": false + }, + { + "name": "MDB_MCP_CONFIRMATION_REQUIRED_TOOLS", + "description": "An array of tool names that require user confirmation before execution. Requires the client to support elicitation.", + "isRequired": false, + "format": "string", + "isSecret": false + }, + { + "name": "MDB_MCP_CONNECTION_STRING", + "description": "MongoDB connection string for direct database connections. Optional, if not set, you'll need to call the connect tool before interacting with MongoDB data.", + "isRequired": false, + "format": "string", + "isSecret": true + }, + { + "name": "MDB_MCP_DISABLE_EMBEDDINGS_VALIDATION", + "description": "When set to true, disables validation of embeddings dimensions.", + "isRequired": false, + "format": "boolean", + "isSecret": false + }, + { + "name": "MDB_MCP_DISABLED_TOOLS", + "description": "An array of tool names, operation types, and/or categories of tools that will be disabled.", + "isRequired": false, + "format": "string", + "isSecret": false + }, + { + "name": "MDB_MCP_EXPORT_CLEANUP_INTERVAL_MS", + "description": "Time in milliseconds between export cleanup cycles that remove expired export files.", + "isRequired": false, + "format": "string", + "isSecret": false + }, + { + "name": "MDB_MCP_EXPORT_TIMEOUT_MS", + "description": "Time in milliseconds after which an export is considered expired and eligible for cleanup.", + "isRequired": false, + "format": "string", + "isSecret": false + }, + { + "name": "MDB_MCP_EXPORTS_PATH", + "description": "Folder to store exported data files.", + "isRequired": false, + "format": "string", + "isSecret": false + }, + { + "name": "MDB_MCP_HTTP_HOST", + "description": "Host to bind the http server.", + "isRequired": false, + "format": "string", + "isSecret": false + }, + { + "name": "MDB_MCP_HTTP_PORT", + "description": "Port number.", + "isRequired": false, + "format": "string", + "isSecret": false + }, + { + "name": "MDB_MCP_IDLE_TIMEOUT_MS", + "description": "Idle timeout for a client to disconnect (only applies to http transport).", + "isRequired": false, + "format": "string", + "isSecret": false + }, + { + "name": "MDB_MCP_INDEX_CHECK", + "description": "When set to true, enforces that query operations must use an index, rejecting queries that perform a collection scan.", + "isRequired": false, + "format": "boolean", + "isSecret": false + }, + { + "name": "MDB_MCP_LOG_PATH", + "description": "Folder to store logs.", + "isRequired": false, + "format": "string", + "isSecret": false + }, + { + "name": "MDB_MCP_LOGGERS", + "description": "Comma separated values, possible values are 'mcp', 'disk' and 'stderr'.", + "isRequired": false, + "format": "string", + "isSecret": false + }, + { + "name": "MDB_MCP_MAX_BYTES_PER_QUERY", + "description": "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.", + "isRequired": false, + "format": "number", + "isSecret": false + }, + { + "name": "MDB_MCP_MAX_DOCUMENTS_PER_QUERY", + "description": "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.", + "isRequired": false, + "format": "number", + "isSecret": false + }, + { + "name": "MDB_MCP_NOTIFICATION_TIMEOUT_MS", + "description": "Notification timeout for a client to be aware of disconnect (only applies to http transport).", + "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.", + "isRequired": false, + "format": "boolean", + "isSecret": false + }, + { + "name": "MDB_MCP_TELEMETRY", + "description": "When set to disabled, disables telemetry collection.", + "isRequired": false, + "format": "string", + "isSecret": false + }, + { + "name": "MDB_MCP_TRANSPORT", + "description": "Either 'stdio' or 'http'.", + "isRequired": false, + "format": "string", + "isSecret": false + }, + { + "name": "MDB_MCP_VOYAGE_API_KEY", + "description": "API key for Voyage AI embeddings service (required for vector search operations with text-to-embedding conversion).", + "isRequired": false, + "format": "string", + "isSecret": true + } + ], + "packageArguments": [ + { + "type": "named", + "name": "--apiClientId", + "description": "Atlas API client ID for authentication. Required for running Atlas tools.", + "isRequired": false + }, + { + "type": "named", + "name": "--apiClientSecret", + "description": "Atlas API client secret for authentication. Required for running Atlas tools.", + "isRequired": false + }, + { + "type": "named", + "name": "--atlasTemporaryDatabaseUserLifetimeMs", + "description": "Time in milliseconds that temporary database users created when connecting to MongoDB Atlas clusters will remain active before being automatically deleted.", + "isRequired": false + }, + { + "type": "named", + "name": "--confirmationRequiredTools", + "description": "An array of tool names that require user confirmation before execution. Requires the client to support elicitation.", + "isRequired": false + }, + { + "type": "named", + "name": "--connectionString", + "description": "MongoDB connection string for direct database connections. Optional, if not set, you'll need to call the connect tool before interacting with MongoDB data.", + "isRequired": false + }, + { + "type": "named", + "name": "--disableEmbeddingsValidation", + "description": "When set to true, disables validation of embeddings dimensions.", + "isRequired": false, + "format": "boolean" + }, + { + "type": "named", + "name": "--disabledTools", + "description": "An array of tool names, operation types, and/or categories of tools that will be disabled.", + "isRequired": false + }, + { + "type": "named", + "name": "--exportCleanupIntervalMs", + "description": "Time in milliseconds between export cleanup cycles that remove expired export files.", + "isRequired": false + }, + { + "type": "named", + "name": "--exportTimeoutMs", + "description": "Time in milliseconds after which an export is considered expired and eligible for cleanup.", + "isRequired": false + }, + { + "type": "named", + "name": "--exportsPath", + "description": "Folder to store exported data files.", + "isRequired": false + }, + { + "type": "named", + "name": "--httpHost", + "description": "Host to bind the http server.", + "isRequired": false + }, + { + "type": "named", + "name": "--httpPort", + "description": "Port number.", + "isRequired": false + }, + { + "type": "named", + "name": "--idleTimeoutMs", + "description": "Idle timeout for a client to disconnect (only applies to http transport).", + "isRequired": false + }, + { + "type": "named", + "name": "--indexCheck", + "description": "When set to true, enforces that query operations must use an index, rejecting queries that perform a collection scan.", + "isRequired": false, + "format": "boolean" + }, + { + "type": "named", + "name": "--logPath", + "description": "Folder to store logs.", + "isRequired": false + }, + { + "type": "named", + "name": "--loggers", + "description": "Comma separated values, possible values are 'mcp', 'disk' and 'stderr'.", + "isRequired": false + }, + { + "type": "named", + "name": "--maxBytesPerQuery", + "description": "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.", + "isRequired": false, + "format": "number" + }, + { + "type": "named", + "name": "--maxDocumentsPerQuery", + "description": "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.", + "isRequired": false, + "format": "number" + }, + { + "type": "named", + "name": "--notificationTimeoutMs", + "description": "Notification timeout for a client to be aware of disconnect (only applies to http transport).", + "isRequired": false + }, + { + "type": "named", + "name": "--readOnly", + "description": "When set to true, only allows read, connect, and metadata operation types, disabling create/update/delete operations.", + "isRequired": false, + "format": "boolean" + }, + { + "type": "named", + "name": "--telemetry", + "description": "When set to disabled, disables telemetry collection.", + "isRequired": false + }, + { + "type": "named", + "name": "--transport", + "description": "Either 'stdio' or 'http'.", + "isRequired": false + }, + { + "type": "named", + "name": "--voyageApiKey", + "description": "API key for Voyage AI embeddings service (required for vector search operations with text-to-embedding conversion).", + "isRequired": false + } + ], + "version": "1.1.0" + } + ] +} diff --git a/src/common/config.ts b/src/common/config.ts index 68c6ebc17..9565e1d07 100644 --- a/src/common/config.ts +++ b/src/common/config.ts @@ -5,11 +5,13 @@ 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 levenshtein from "ts-levenshtein"; +import * as levenshteinModule from "ts-levenshtein"; import type { Similarity } from "./search/vectorSearchEmbeddingsManager.js"; +import { z } from "zod"; +const levenshtein = levenshteinModule.default; // From: https://github.com/mongodb-js/mongosh/blob/main/packages/cli-repl/src/arg-parser.ts -const OPTIONS = { +export const OPTIONS = { number: ["maxDocumentsPerQuery", "maxBytesPerQuery"], string: [ "apiBaseUrl", @@ -157,38 +159,132 @@ function isConnectionSpecifier(arg: string | undefined): boolean { ); } -// If we decide to support non-string config options, we'll need to extend the mechanism for parsing -// env variables. -export interface UserConfig extends CliOptions { - apiBaseUrl: string; - apiClientId?: string; - apiClientSecret?: string; - telemetry: "enabled" | "disabled"; - logPath: string; - exportsPath: string; - exportTimeoutMs: number; - exportCleanupIntervalMs: number; - connectionString?: string; - // TODO: Use a type tracking all tool names. - disabledTools: Array; - confirmationRequiredTools: Array; - readOnly?: boolean; - indexCheck?: boolean; - transport: "stdio" | "http"; - httpPort: number; - httpHost: string; - httpHeaders: Record; - loggers: Array<"stderr" | "disk" | "mcp">; - idleTimeoutMs: number; - notificationTimeoutMs: number; - maxDocumentsPerQuery: number; - maxBytesPerQuery: number; - atlasTemporaryDatabaseUserLifetimeMs: number; - voyageApiKey: string; - disableEmbeddingsValidation: boolean; - vectorSearchDimensions: number; - vectorSearchSimilarityFunction: Similarity; -} +export const UserConfigSchema = z.object({ + apiBaseUrl: z.string().default("https://cloud.mongodb.com/"), + apiClientId: z + .string() + .optional() + .describe("Atlas API client ID for authentication. Required for running Atlas tools."), + apiClientSecret: z + .string() + .optional() + .describe("Atlas API client secret for authentication. Required for running Atlas tools."), + connectionString: z + .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"])) + .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()) + .default([]) + .describe("An array of tool names, operation types, and/or categories of tools that will be disabled."), + confirmationRequiredTools: z + .array(z.string()) + .default([ + "atlas-create-access-list", + "atlas-create-db-user", + "drop-database", + "drop-collection", + "delete-many", + "drop-index", + ]) + .describe( + "An array of tool names that require user confirmation before execution. Requires the client to support elicitation." + ), + readOnly: z + .boolean() + .default(false) + .describe( + "When set to true, only allows read, connect, and metadata operation types, disabling create/update/delete operations." + ), + indexCheck: z + .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 + .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 + .number() + .default(3000) + .describe("Port number for the HTTP server (only used when transport is 'http')."), + httpHost: z + .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()) + .default({}) + .describe( + "Header that the HTTP server will validate when making requests (only used when transport is 'http')." + ), + idleTimeoutMs: z + .number() + .default(600_000) + .describe("Idle timeout for a client to disconnect (only applies to http transport)."), + notificationTimeoutMs: z + .number() + .default(540_000) + .describe("Notification timeout for a client to be aware of disconnect (only applies to http transport)."), + maxBytesPerQuery: z + .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 + .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 + .number() + .default(300_000) + .describe("Time in milliseconds after which an export is considered expired and eligible for cleanup."), + exportCleanupIntervalMs: z + .number() + .default(120_000) + .describe("Time in milliseconds between export cleanup cycles that remove expired export files."), + atlasTemporaryDatabaseUserLifetimeMs: z + .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 + .string() + .default("") + .describe( + "API key for Voyage AI embeddings service (required for vector search operations with text-to-embedding conversion)." + ), + disableEmbeddingsValidation: z + .boolean() + .optional() + .describe("When set to true, disables validation of embeddings dimensions."), + vectorSearchDimensions: z + .number() + .default(1024) + .describe("Default number of dimensions for vector search embeddings."), + vectorSearchSimilarityFunction: z + .custom() + .optional() + .default("euclidean") + .describe("Default similarity function for vector search: 'euclidean', 'cosine', or 'dotProduct'."), +}); + +export type UserConfig = z.infer & CliOptions; export const defaultUserConfig: UserConfig = { apiBaseUrl: "https://cloud.mongodb.com/",