From 0f2a5942245e7debf087bce4a9f1ccc95ff57f56 Mon Sep 17 00:00:00 2001 From: gagik Date: Mon, 24 Nov 2025 14:46:55 +0100 Subject: [PATCH 01/21] feat: add ability to override parameters using HTTP headers MCP-293 --- package.json | 2 +- src/common/config/configOverrides.ts | 157 +++++++ src/common/config/configUtils.ts | 48 ++- src/common/config/userConfig.ts | 119 ++++-- src/transports/base.ts | 22 +- src/transports/streamableHttp.ts | 8 +- tests/integration/server.test.ts | 2 +- .../transports/configOverrides.test.ts | 392 ++++++++++++++++++ .../transports/createSessionConfig.test.ts | 184 ++++---- .../common/config/configOverrides.test.ts | 329 +++++++++++++++ 10 files changed, 1125 insertions(+), 138 deletions(-) create mode 100644 src/common/config/configOverrides.ts create mode 100644 tests/integration/transports/configOverrides.test.ts create mode 100644 tests/unit/common/config/configOverrides.test.ts diff --git a/package.json b/package.json index 4257c4d88..bc8a59155 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "generate": "npm run generate:api && npm run generate:arguments", "generate:api": "./scripts/generate.sh", "generate:arguments": "tsx scripts/generateArguments.ts", - "test": "vitest --project eslint-rules --project unit-and-integration --coverage", + "test": "vitest --project eslint-rules --project unit-and-integration --coverage --run", "pretest:accuracy": "npm run build", "test:accuracy": "sh ./scripts/accuracy/runAccuracyTests.sh", "test:long-running-tests": "vitest --project long-running-tests --coverage", diff --git a/src/common/config/configOverrides.ts b/src/common/config/configOverrides.ts new file mode 100644 index 000000000..fd3c61299 --- /dev/null +++ b/src/common/config/configOverrides.ts @@ -0,0 +1,157 @@ +import type { UserConfig } from "./userConfig.js"; +import { UserConfigSchema, configRegistry } from "./userConfig.js"; +import type { RequestContext } from "../../transports/base.js"; +import type { OverrideBehavior } from "./configUtils.js"; + +export const CONFIG_HEADER_PREFIX = "x-mongodb-mcp-"; +export const CONFIG_QUERY_PREFIX = "mongodbMcp"; + +/** + * Applies config overrides from request context (headers and query parameters). + * Query parameters take precedence over headers. + * + * @param baseConfig - The base user configuration + * @param request - The request context containing headers and query parameters + * @returns The configuration with overrides applied + */ +export function applyConfigOverrides({ + baseConfig, + request, +}: { + baseConfig: UserConfig; + request?: RequestContext; +}): UserConfig { + if (!request) { + return baseConfig; + } + + const result: UserConfig = { ...baseConfig }; + const overridesFromHeaders = extractConfigOverrides("header", request.headers); + const overridesFromQuery = extractConfigOverrides("query", request.query); + + // Merge overrides, with query params taking precedence + const allOverrides = { ...overridesFromHeaders, ...overridesFromQuery }; + + // Apply each override according to its behavior + for (const [key, overrideValue] of Object.entries(allOverrides)) { + assertValidConfigKey(key); + const behavior = getConfigMeta(key)?.overrideBehavior || "not-allowed"; + const baseValue = baseConfig[key as keyof UserConfig]; + const newValue = applyOverride(key, baseValue, overrideValue, behavior); + (result as any)[key] = newValue; + } + + return result; +} + +/** + * Extracts config overrides from HTTP headers or query parameters. + */ +function extractConfigOverrides( + mode: "header" | "query", + source: Record | undefined +): Partial> { + if (!source) { + return {}; + } + + const overrides: Partial> = {}; + + for (const [name, value] of Object.entries(source)) { + const configKey = nameToConfigKey(mode, name); + if (!configKey) { + continue; + } + assertValidConfigKey(configKey); + + const behavior = getConfigMeta(configKey)?.overrideBehavior || "not-allowed"; + if (behavior === "not-allowed") { + throw new Error(`Config key ${configKey} is not allowed to be overridden`); + } + + const parsedValue = parseConfigValue(configKey, value); + if (parsedValue !== undefined) { + overrides[configKey] = parsedValue; + } + } + + return overrides; +} + +function assertValidConfigKey(key: string): asserts key is keyof typeof UserConfigSchema.shape { + if (!(key in UserConfigSchema.shape)) { + throw new Error(`Invalid config key: ${key}`); + } +} + +/** + * Gets the schema metadata for a config key. + */ +export function getConfigMeta(key: keyof typeof UserConfigSchema.shape) { + return configRegistry.get(UserConfigSchema.shape[key]); +} + +/** + * Parses a string value to the appropriate type using the Zod schema. + */ +function parseConfigValue(key: keyof typeof UserConfigSchema.shape, value: unknown): unknown { + const fieldSchema = UserConfigSchema.shape[key as keyof typeof UserConfigSchema.shape]; + if (!fieldSchema) { + throw new Error(`Invalid config key: ${key}`); + } + + return fieldSchema.safeParse(value).data; +} + +/** + * Converts a header/query name to its config key format. + * Example: "x-mongodb-mcp-read-only" -> "readOnly" + * Example: "mongodbMcpReadOnly" -> "readOnly" + */ +export function nameToConfigKey(mode: "header" | "query", name: string): string | undefined { + const lowerCaseName = name.toLowerCase(); + + if (mode === "header" && lowerCaseName.startsWith(CONFIG_HEADER_PREFIX)) { + const normalized = lowerCaseName.substring(CONFIG_HEADER_PREFIX.length); + // Convert kebab-case to camelCase + return normalized.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()); + } + if (mode === "query" && name.startsWith(CONFIG_QUERY_PREFIX)) { + const withoutPrefix = name.substring(CONFIG_QUERY_PREFIX.length); + // Convert first letter to lowercase to get config key + return withoutPrefix.charAt(0).toLowerCase() + withoutPrefix.slice(1); + } + + return undefined; +} + +function applyOverride( + key: keyof typeof UserConfigSchema.shape, + baseValue: unknown, + overrideValue: unknown, + behavior: OverrideBehavior +): unknown { + if (typeof behavior === "function") { + const shouldApply = behavior(baseValue, overrideValue); + if (!shouldApply) { + throw new Error( + `Config override validation failed for ${key}: cannot override from ${JSON.stringify(baseValue)} to ${JSON.stringify(overrideValue)}` + ); + } + return overrideValue; + } + switch (behavior) { + case "override": + return overrideValue; + + case "merge": + if (Array.isArray(baseValue) && Array.isArray(overrideValue)) { + return [...baseValue, ...overrideValue]; + } + throw new Error("Cannot merge non-array values, did you mean to use the 'override' behavior?"); + + case "not-allowed": + default: + return baseValue; + } +} diff --git a/src/common/config/configUtils.ts b/src/common/config/configUtils.ts index 11db9ee42..66cc7688a 100644 --- a/src/common/config/configUtils.ts +++ b/src/common/config/configUtils.ts @@ -4,6 +4,22 @@ import { ALL_CONFIG_KEYS } from "./argsParserOptions.js"; import * as levenshteinModule from "ts-levenshtein"; const levenshtein = levenshteinModule.default; +/// Custom logic function to apply the override value. +/// Returns true if the override should be applied, false otherwise. +export type CustomOverrideLogic = (oldValue: unknown, newValue: unknown) => boolean; + +/** + * Defines how a config field can be overridden via HTTP headers or query parameters. + */ +export type OverrideBehavior = + /// Cannot be overridden via request + | "not-allowed" + /// Can be completely replaced + | "override" + /// Values are merged (for arrays) + | "merge" + | CustomOverrideLogic; + /** * Metadata for config schema fields. */ @@ -17,7 +33,11 @@ export type ConfigFieldMeta = { * Secret fields will be marked as secret in environment variable definitions. */ isSecret?: boolean; - + /** + * Defines how this config field can be overridden via HTTP headers or query parameters. + * Defaults to "not-allowed" for security. + */ + overrideBehavior?: OverrideBehavior; [key: string]: unknown; }; @@ -69,8 +89,11 @@ export function commaSeparatedToArray(str: string | string[] return undefined; } - if (!Array.isArray(str)) { - return [str] as T; + if (typeof str === "string") { + return str + .split(",") + .map((e) => e.trim()) + .filter((e) => e.length > 0) as T; } if (str.length === 1) { @@ -82,3 +105,22 @@ export function commaSeparatedToArray(str: string | string[] return str as T; } + +/** + * Preprocessor for boolean values that handles string "true"/"false" correctly. + * Zod's coerce.boolean() treats any non-empty string as true, which is not what we want. + */ +export function parseBoolean(val: unknown): unknown { + if (typeof val === "string") { + const lower = val.toLowerCase().trim(); + if (lower === "false" || lower === "0") return false; + if (lower === "true" || lower === "1") return true; + } + if (typeof val === "boolean") { + return val; + } + if (typeof val === "number") { + return val !== 0; + } + return false; +} diff --git a/src/common/config/userConfig.ts b/src/common/config/userConfig.ts index 938155ab4..afc29701f 100644 --- a/src/common/config/userConfig.ts +++ b/src/common/config/userConfig.ts @@ -1,6 +1,12 @@ import { z as z4 } from "zod/v4"; import { type CliOptions } from "@mongosh/arg-parser"; -import { type ConfigFieldMeta, commaSeparatedToArray, getExportsPath, getLogPath } from "./configUtils.js"; +import { + type ConfigFieldMeta, + commaSeparatedToArray, + getExportsPath, + getLogPath, + parseBoolean, +} from "./configUtils.js"; import { previewFeatureValues, similarityValues } from "../schemas.js"; // TODO: UserConfig should only be UserConfigSchema and not an intersection with @@ -11,24 +17,27 @@ export type UserConfig = z4.infer & CliOptions; export const configRegistry = z4.registry(); export const UserConfigSchema = z4.object({ - apiBaseUrl: z4.string().default("https://cloud.mongodb.com/"), + apiBaseUrl: z4 + .string() + .default("https://cloud.mongodb.com/") + .register(configRegistry, { overrideBehavior: "not-allowed" }), apiClientId: z4 .string() .optional() .describe("Atlas API client ID for authentication. Required for running Atlas tools.") - .register(configRegistry, { isSecret: true }), + .register(configRegistry, { isSecret: true, overrideBehavior: "not-allowed" }), apiClientSecret: z4 .string() .optional() .describe("Atlas API client secret for authentication. Required for running Atlas tools.") - .register(configRegistry, { isSecret: true }), + .register(configRegistry, { isSecret: true, overrideBehavior: "not-allowed" }), 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." ) - .register(configRegistry, { isSecret: true }), + .register(configRegistry, { isSecret: true, overrideBehavior: "override" }), loggers: z4 .preprocess( (val: string | string[] | undefined) => commaSeparatedToArray(val), @@ -44,16 +53,18 @@ export const UserConfigSchema = z4.object({ .describe("An array of logger types.") .register(configRegistry, { defaultValueDescription: '`"disk,mcp"` see below*', + overrideBehavior: "merge", }), logPath: z4 .string() .default(getLogPath()) .describe("Folder to store logs.") - .register(configRegistry, { defaultValueDescription: "see below*" }), + .register(configRegistry, { defaultValueDescription: "see below*", overrideBehavior: "not-allowed" }), disabledTools: z4 .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."), + .describe("An array of tool names, operation types, and/or categories of tools that will be disabled.") + .register(configRegistry, { overrideBehavior: "merge" }), confirmationRequiredTools: z4 .preprocess((val: string | string[] | undefined) => commaSeparatedToArray(val), z4.array(z4.string())) .default([ @@ -66,105 +77,151 @@ export const UserConfigSchema = z4.object({ ]) .describe( "An array of tool names that require user confirmation before execution. Requires the client to support elicitation." - ), + ) + .register(configRegistry, { overrideBehavior: "merge" }), readOnly: z4 - .boolean() + .preprocess(parseBoolean, z4.boolean()) .default(false) .describe( "When set to true, only allows read, connect, and metadata operation types, disabling create/update/delete operations." - ), + ) + .register(configRegistry, { + overrideBehavior: (oldValue, newValue) => { + // Only allow override if setting to true from false + if (oldValue === false && newValue === true) { + return true; + } + return false; + }, + }), indexCheck: z4 - .boolean() + .preprocess(parseBoolean, z4.boolean()) .default(false) .describe( "When set to true, enforces that query operations must use an index, rejecting queries that perform a collection scan." - ), + ) + .register(configRegistry, { + overrideBehavior: (oldValue, newValue) => { + // Only allow override if setting to true from false + if (oldValue === false && newValue === true) { + return true; + } + return false; + }, + }), telemetry: z4 .enum(["enabled", "disabled"]) .default("enabled") - .describe("When set to disabled, disables telemetry collection."), - transport: z4.enum(["stdio", "http"]).default("stdio").describe("Either 'stdio' or 'http'."), + .describe("When set to disabled, disables telemetry collection.") + .register(configRegistry, { overrideBehavior: "not-allowed" }), + transport: z4 + .enum(["stdio", "http"]) + .default("stdio") + .describe("Either 'stdio' or 'http'.") + .register(configRegistry, { overrideBehavior: "not-allowed" }), 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')."), + .describe("Port number for the HTTP server (only used when transport is 'http').") + .register(configRegistry, { overrideBehavior: "not-allowed" }), httpHost: z4 .string() .default("127.0.0.1") - .describe("Host address to bind the HTTP server to (only used when transport is 'http')."), + .describe("Host address to bind the HTTP server to (only used when transport is 'http').") + .register(configRegistry, { overrideBehavior: "not-allowed" }), httpHeaders: z4 .object({}) .passthrough() .default({}) .describe( "Header that the HTTP server will validate when making requests (only used when transport is 'http')." - ), + ) + .register(configRegistry, { overrideBehavior: "not-allowed" }), idleTimeoutMs: z4.coerce .number() .default(600_000) - .describe("Idle timeout for a client to disconnect (only applies to http transport)."), + .describe("Idle timeout for a client to disconnect (only applies to http transport).") + .register(configRegistry, { overrideBehavior: "override" }), notificationTimeoutMs: z4.coerce .number() .default(540_000) - .describe("Notification timeout for a client to be aware of disconnect (only applies to http transport)."), + .describe("Notification timeout for a client to be aware of disconnect (only applies to http transport).") + .register(configRegistry, { overrideBehavior: "override" }), 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." - ), + ) + .register(configRegistry, { overrideBehavior: "not-allowed" }), maxDocumentsPerQuery: z4.coerce .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." - ), + ) + .register(configRegistry, { overrideBehavior: "not-allowed" }), exportsPath: z4 .string() .default(getExportsPath()) .describe("Folder to store exported data files.") - .register(configRegistry, { defaultValueDescription: "see below*" }), + .register(configRegistry, { defaultValueDescription: "see below*", overrideBehavior: "not-allowed" }), exportTimeoutMs: z4.coerce .number() .default(300_000) - .describe("Time in milliseconds after which an export is considered expired and eligible for cleanup."), + .describe("Time in milliseconds after which an export is considered expired and eligible for cleanup.") + .register(configRegistry, { overrideBehavior: "override" }), exportCleanupIntervalMs: z4.coerce .number() .default(120_000) - .describe("Time in milliseconds between export cleanup cycles that remove expired export files."), + .describe("Time in milliseconds between export cleanup cycles that remove expired export files.") + .register(configRegistry, { overrideBehavior: "override" }), atlasTemporaryDatabaseUserLifetimeMs: z4.coerce .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." - ), + ) + .register(configRegistry, { overrideBehavior: "override" }), voyageApiKey: z4 .string() .default("") .describe( "API key for Voyage AI embeddings service (required for vector search operations with text-to-embedding conversion)." ) - .register(configRegistry, { isSecret: true }), + .register(configRegistry, { isSecret: true, overrideBehavior: "not-allowed" }), disableEmbeddingsValidation: z4 - .boolean() + .preprocess(parseBoolean, z4.boolean()) .default(false) - .describe("When set to true, disables validation of embeddings dimensions."), + .describe("When set to true, disables validation of embeddings dimensions.") + .register(configRegistry, { + overrideBehavior: (oldValue, newValue) => { + // Only allow override if setting to false from true (making more restrictive) + if (oldValue === true && newValue === false) { + return true; + } + return false; + }, + }), vectorSearchDimensions: z4.coerce .number() .default(1024) - .describe("Default number of dimensions for vector search embeddings."), + .describe("Default number of dimensions for vector search embeddings.") + .register(configRegistry, { overrideBehavior: "override" }), vectorSearchSimilarityFunction: z4 .enum(similarityValues) .default("euclidean") - .describe("Default similarity function for vector search: 'euclidean', 'cosine', or 'dotProduct'."), + .describe("Default similarity function for vector search: 'euclidean', 'cosine', or 'dotProduct'.") + .register(configRegistry, { overrideBehavior: "override" }), previewFeatures: z4 .preprocess( (val: string | string[] | undefined) => commaSeparatedToArray(val), z4.array(z4.enum(previewFeatureValues)) ) .default([]) - .describe("An array of preview features that are enabled."), + .describe("An array of preview features that are enabled.") + .register(configRegistry, { overrideBehavior: "merge" }), }); diff --git a/src/transports/base.ts b/src/transports/base.ts index f9b00f758..f4d173d5e 100644 --- a/src/transports/base.ts +++ b/src/transports/base.ts @@ -21,8 +21,17 @@ import { defaultCreateAtlasLocalClient } from "../common/atlasLocal.js"; import type { Client } from "@mongodb-js/atlas-local"; import { VectorSearchEmbeddingsManager } from "../common/search/vectorSearchEmbeddingsManager.js"; import type { ToolBase, ToolConstructorParams } from "../tools/tool.js"; +import { applyConfigOverrides } from "../common/config/configOverrides.js"; -type CreateSessionConfigFn = (userConfig: UserConfig) => Promise | UserConfig; +export type RequestContext = { + headers?: Record; + query?: Record; +}; + +type CreateSessionConfigFn = (context: { + userConfig: UserConfig; + request?: RequestContext; +}) => Promise | UserConfig; export type TransportRunnerConfig = { userConfig: UserConfig; @@ -90,10 +99,15 @@ export abstract class TransportRunnerBase { this.deviceId = DeviceId.create(this.logger); } - protected async setupServer(): Promise { + protected async setupServer(request?: RequestContext): Promise { + // Apply config overrides from request context (headers and query parameters) + let userConfig = applyConfigOverrides({ baseConfig: this.userConfig, request }); + // Call the config provider hook if provided, allowing consumers to - // fetch or modify configuration before session initialization - const userConfig = this.createSessionConfig ? await this.createSessionConfig(this.userConfig) : this.userConfig; + // fetch or modify configuration after applying request context overrides + if (this.createSessionConfig) { + userConfig = await this.createSessionConfig({ userConfig, request }); + } const mcpServer = new McpServer({ name: packageInfo.mcpServerName, diff --git a/src/transports/streamableHttp.ts b/src/transports/streamableHttp.ts index 0a20e59e8..3d3d59ca4 100644 --- a/src/transports/streamableHttp.ts +++ b/src/transports/streamableHttp.ts @@ -5,7 +5,7 @@ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/ import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js"; import { LogId } from "../common/logger.js"; import { SessionStore } from "../common/sessionStore.js"; -import { TransportRunnerBase, type TransportRunnerConfig } from "./base.js"; +import { TransportRunnerBase, type TransportRunnerConfig, type RequestContext } from "./base.js"; const JSON_RPC_ERROR_CODE_PROCESSING_REQUEST_FAILED = -32000; const JSON_RPC_ERROR_CODE_SESSION_ID_REQUIRED = -32001; @@ -111,7 +111,11 @@ export class StreamableHttpRunner extends TransportRunnerBase { return; } - const server = await this.setupServer(); + const request: RequestContext = { + headers: req.headers as Record, + query: req.query as Record, + }; + const server = await this.setupServer(request); let keepAliveLoop: NodeJS.Timeout; const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: (): string => randomUUID().toString(), diff --git a/tests/integration/server.test.ts b/tests/integration/server.test.ts index 2c362a963..a1ccba1fb 100644 --- a/tests/integration/server.test.ts +++ b/tests/integration/server.test.ts @@ -82,7 +82,7 @@ describe("Server integration test", () => { expect(tools.tools.some((tool) => tool.name === "atlas-list-projects")).toBe(true); // Check that non-read tools are NOT available - expect(tools.tools.some((tool) => tool.name === "insert-one")).toBe(false); + expect(tools.tools.some((tool) => tool.name === "insert-many")).toBe(false); expect(tools.tools.some((tool) => tool.name === "update-many")).toBe(false); expect(tools.tools.some((tool) => tool.name === "delete-one")).toBe(false); expect(tools.tools.some((tool) => tool.name === "drop-collection")).toBe(false); diff --git a/tests/integration/transports/configOverrides.test.ts b/tests/integration/transports/configOverrides.test.ts new file mode 100644 index 000000000..69cfcd58d --- /dev/null +++ b/tests/integration/transports/configOverrides.test.ts @@ -0,0 +1,392 @@ +import { StreamableHttpRunner } from "../../../src/transports/streamableHttp.js"; +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; +import { describe, expect, it, afterEach, beforeEach } from "vitest"; +import { defaultTestConfig } from "../helpers.js"; + +describe("Config Overrides via HTTP", () => { + let runner: StreamableHttpRunner; + let client: Client; + let transport: StreamableHTTPClientTransport; + + // Helper function to setup and start runner with config + async function startRunner(config: any, createSessionConfig?: any) { + runner = new StreamableHttpRunner({ userConfig: config, createSessionConfig }); + await runner.start(); + } + + // Helper function to connect client with headers + async function connectClient(headers: Record = {}) { + transport = new StreamableHTTPClientTransport(new URL(`${runner.serverAddress}/mcp`), { + requestInit: { headers }, + }); + await client.connect(transport); + } + + beforeEach(() => { + client = new Client({ + name: "test-client", + version: "1.0.0", + }); + }); + + afterEach(async () => { + if (client) { + await client.close(); + } + if (transport) { + await transport.close(); + } + if (runner) { + await runner.close(); + } + }); + + describe("override behavior", () => { + it("should override readOnly config via header (false to true)", async () => { + await startRunner({ + ...defaultTestConfig, + httpPort: 0, + readOnly: false, + }); + + await connectClient({ + ["x-mongodb-mcp-read-only"]: "true", + }); + + const response = await client.listTools(); + + expect(response).toBeDefined(); + expect(response.tools).toBeDefined(); + + // Verify read-only mode is applied - insert-many should not be available + const writeTools = response.tools.filter((tool) => tool.name === "insert-many"); + expect(writeTools.length).toBe(0); + + // Verify read tools are available + const readTools = response.tools.filter((tool) => tool.name === "find"); + expect(readTools.length).toBe(1); + }); + + it("should NOT override readOnly from true to false", async () => { + await startRunner({ + ...defaultTestConfig, + httpPort: 0, + readOnly: true, + }); + + try { + await connectClient({ + ["x-mongodb-mcp-read-only"]: "false", + }); + expect.fail("Expected an error to be thrown"); + } catch (error) { + if (!(error instanceof Error)) { + throw new Error("Expected an error to be thrown"); + } + expect(error.message).toContain("Error POSTing to endpoint (HTTP 400)"); + expect(error.message).toContain(`Config override validation failed for readOnly`); + } + }); + + it("should override connectionString via header", async () => { + await startRunner({ + ...defaultTestConfig, + httpPort: 0, + connectionString: undefined, + }); + + await connectClient({ + ["x-mongodb-mcp-connection-string"]: "mongodb://override:27017", + }); + + const response = await client.listTools(); + + expect(response).toBeDefined(); + }); + }); + + describe("merge behavior", () => { + it("should merge disabledTools via header", async () => { + await startRunner({ + ...defaultTestConfig, + httpPort: 0, + disabledTools: ["insert-many"], + }); + + await connectClient({ + ["x-mongodb-mcp-disabled-tools"]: "find,aggregate", + }); + + const response = await client.listTools(); + + expect(response).toBeDefined(); + expect(response.tools).toBeDefined(); + + // Verify all three tools are disabled + const insertTool = response.tools.find( + (tool) => tool.name === "insert-many" || tool.name === "find" || tool.name === "aggregate" + ); + expect(response.tools).is.not.empty; + expect(insertTool).toBeUndefined(); + }); + }); + + describe("not-allowed behavior", () => { + it.each([ + { + configKey: "apiBaseUrl", + headerName: "x-mongodb-mcp-api-base-url", + headerValue: "https://malicious.com/", + }, + { + configKey: "apiClientId", + headerName: "x-mongodb-mcp-api-client-id", + headerValue: "malicious-id", + }, + { + configKey: "apiClientSecret", + headerName: "x-mongodb-mcp-api-client-secret", + headerValue: "malicious-secret", + }, + { + configKey: "transport", + headerName: "x-mongodb-mcp-transport", + headerValue: "stdio", + }, + { + configKey: "httpPort", + headerName: "x-mongodb-mcp-http-port", + headerValue: "9999", + }, + { + configKey: "maxBytesPerQuery", + headerName: "x-mongodb-mcp-max-bytes-per-query", + headerValue: "999999", + }, + { + configKey: "maxDocumentsPerQuery", + headerName: "x-mongodb-mcp-max-documents-per-query", + headerValue: "1000", + }, + ])("should reject $configKey override", async ({ configKey, headerName, headerValue }) => { + await startRunner({ + ...defaultTestConfig, + httpPort: 0, + }); + + try { + await connectClient({ + [headerName]: headerValue, + }); + expect.fail("Expected an error to be thrown"); + } catch (error) { + if (!(error instanceof Error)) { + throw new Error("Expected an error to be thrown"); + } + expect(error.message).toContain("Error POSTing to endpoint (HTTP 400)"); + expect(error.message).toContain(`Config key ${configKey} is not allowed to be overridden`); + } + }); + + it("should reject multiple not-allowed fields at once", async () => { + await startRunner({ + ...defaultTestConfig, + httpPort: 0, + }); + + try { + await connectClient({ + "x-mongodb-mcp-api-base-url": "https://malicious.com/", + "x-mongodb-mcp-transport": "stdio", + "x-mongodb-mcp-http-port": "9999", + }); + expect.fail("Expected an error to be thrown"); + } catch (error) { + if (!(error instanceof Error)) { + throw new Error("Expected an error to be thrown"); + } + expect(error.message).toContain("Error POSTing to endpoint (HTTP 400)"); + // Should contain at least one of the not-allowed field errors + const hasNotAllowedError = + error.message.includes("Config key apiBaseUrl is not allowed to be overridden") || + error.message.includes("Config key transport is not allowed to be overridden") || + error.message.includes("Config key httpPort is not allowed to be overridden"); + expect(hasNotAllowedError).toBe(true); + } + }); + }); + + describe("query parameter overrides", () => { + it("should apply overrides from query parameters", async () => { + await startRunner({ + ...defaultTestConfig, + httpPort: 0, + readOnly: false, + }); + + // Note: SDK doesn't support query params directly, so this test verifies the mechanism exists + // In real usage, query params would be in the URL or request + await connectClient({ + ["x-mongodb-mcp-read-only"]: "true", + }); + + const response = await client.listTools(); + + expect(response).toBeDefined(); + const writeTools = response.tools.filter((tool) => tool.name === "insert-many"); + expect(writeTools.length).toBe(0); + }); + }); + + describe("integration with createSessionConfig", () => { + it("should allow createSessionConfig to override header values", async () => { + const userConfig = { + ...defaultTestConfig, + httpPort: 0, + readOnly: false, + }; + + // createSessionConfig receives the config after header overrides are applied + // It can further modify it, but headers have already been applied + const createSessionConfig = async ({ + userConfig: config, + request, + }: { + userConfig: typeof userConfig; + request?: any; + }) => { + expect(request).toBeDefined(); + expect(request.headers).toBeDefined(); + config.readOnly = request.headers["x-mongodb-mcp-read-only"] === "true"; + config.disabledTools = ["count"]; + return config; + }; + + await startRunner(userConfig, createSessionConfig); + + await connectClient({ + ["x-mongodb-mcp-read-only"]: "true", + }); + + const response = await client.listTools(); + + expect(response).toBeDefined(); + + // Verify read-only mode was applied, as specified in request and + const writeTools = response.tools.filter((tool) => tool.name === "insert-many"); + expect(writeTools.length).toBe(0); + + // Verify create session config overrides were applied + const countTool = response.tools.find((tool) => tool.name === "count"); + expect(countTool).toBeUndefined(); + + expect(response.tools).is.not.empty; + }); + + it("should pass request context to createSessionConfig", async () => { + const userConfig = { + ...defaultTestConfig, + httpPort: 0, + }; + + let capturedRequest: any; + const createSessionConfig = async ({ request }: { userConfig: typeof userConfig; request?: any }) => { + capturedRequest = request; + return userConfig; + }; + + await startRunner(userConfig, createSessionConfig); + + await connectClient({ + "x-custom-header": "test-value", + }); + + // Verify that request context was passed + expect(capturedRequest).toBeDefined(); + expect(capturedRequest.headers["x-custom-header"]).toBe("test-value"); + }); + }); + + describe("conditional overrides", () => { + it("should allow readOnly from false to true", async () => { + await startRunner({ + ...defaultTestConfig, + httpPort: 0, + readOnly: false, + }); + + await connectClient({ + ["x-mongodb-mcp-read-only"]: "true", + }); + + const response = await client.listTools(); + + expect(response).toBeDefined(); + expect(response.tools).toBeDefined(); + // Check readonly mode + const writeTools = response.tools.filter((tool) => tool.name === "insert-many"); + expect(writeTools.length).toBe(0); + + // Check read tools are available + const readTools = response.tools.filter((tool) => tool.name === "find"); + expect(readTools.length).toBe(1); + }); + + it("should NOT allow readOnly from true to false", async () => { + await startRunner({ + ...defaultTestConfig, + httpPort: 0, + readOnly: true, + }); + + try { + await connectClient({ + ["x-mongodb-mcp-read-only"]: "false", + }); + expect.fail("Expected an error to be thrown"); + } catch (error) { + if (!(error instanceof Error)) { + throw new Error("Expected an error to be thrown"); + } + expect(error.message).toContain("Error POSTing to endpoint (HTTP 400)"); + expect(error.message).toContain(`Config override validation failed for readOnly`); + } + }); + }); + + describe("multiple overrides", () => { + it("should handle multiple header overrides", async () => { + await startRunner({ + ...defaultTestConfig, + httpPort: 0, + readOnly: false, + indexCheck: false, + idleTimeoutMs: 600_000, + disabledTools: ["tool1"], + }); + + await connectClient({ + ["x-mongodb-mcp-read-only"]: "true", + ["x-mongodb-mcp-index-check"]: "true", + ["x-mongodb-mcp-idle-timeout-ms"]: "300000", + ["x-mongodb-mcp-disabled-tools"]: "count", + }); + + const response = await client.listTools(); + + expect(response).toBeDefined(); + + // Verify read-only mode + const writeTools = response.tools.filter((tool) => tool.name === "insert-many"); + expect(writeTools.length).toBe(0); + + // Verify disabled tools + const countTool = response.tools.find((tool) => tool.name === "count"); + expect(countTool).toBeUndefined(); + + const findTool = response.tools.find((tool) => tool.name === "find"); + expect(findTool).toBeDefined(); + }); + }); +}); diff --git a/tests/integration/transports/createSessionConfig.test.ts b/tests/integration/transports/createSessionConfig.test.ts index a0b72dcd2..02484e9e0 100644 --- a/tests/integration/transports/createSessionConfig.test.ts +++ b/tests/integration/transports/createSessionConfig.test.ts @@ -1,151 +1,143 @@ import { StreamableHttpRunner } from "../../../src/transports/streamableHttp.js"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; -import { describe, expect, it } from "vitest"; +import { afterEach, describe, expect, it } from "vitest"; import type { TransportRunnerConfig } from "../../../src/lib.js"; import { defaultTestConfig } from "../helpers.js"; describe("createSessionConfig", () => { const userConfig = defaultTestConfig; let runner: StreamableHttpRunner; + let client: Client | undefined; + let transport: StreamableHTTPClientTransport | undefined; + + // Helper to start runner with config + const startRunner = async ( + config: { + userConfig?: typeof userConfig; + createSessionConfig?: TransportRunnerConfig["createSessionConfig"]; + } = {} + ) => { + runner = new StreamableHttpRunner({ + userConfig: { ...userConfig, httpPort: 0, ...config.userConfig }, + createSessionConfig: config.createSessionConfig, + }); + await runner.start(); + return runner; + }; + + // Helper to setup server and get user config + const getServerConfig = async () => { + const server = await runner["setupServer"](); + return server.userConfig; + }; + + // Helper to create and connect client + const createConnectedClient = async () => { + client = new Client({ name: "test-client", version: "1.0.0" }); + transport = new StreamableHTTPClientTransport(new URL(`${runner.serverAddress}/mcp`)); + await client.connect(transport); + return { client, transport }; + }; + + afterEach(async () => { + if (client) { + await client.close(); + client = undefined; + } + if (transport) { + await transport.close(); + transport = undefined; + } + if (runner) { + await runner.close(); + } + }); describe("basic functionality", () => { it("should use the modified config from createSessionConfig", async () => { - const createSessionConfig: TransportRunnerConfig["createSessionConfig"] = async (userConfig) => { - return Promise.resolve({ - ...userConfig, - apiBaseUrl: "https://test-api.mongodb.com/", - }); - }; - userConfig.httpPort = 0; // Use a random port - runner = new StreamableHttpRunner({ - userConfig, - createSessionConfig, + await startRunner({ + createSessionConfig: async ({ userConfig }) => + Promise.resolve({ + ...userConfig, + apiBaseUrl: "https://test-api.mongodb.com/", + }), }); - await runner.start(); - - const server = await runner["setupServer"](); - expect(server.userConfig.apiBaseUrl).toBe("https://test-api.mongodb.com/"); - await runner.close(); + const config = await getServerConfig(); + expect(config.apiBaseUrl).toBe("https://test-api.mongodb.com/"); }); it("should work without a createSessionConfig", async () => { - userConfig.httpPort = 0; // Use a random port - runner = new StreamableHttpRunner({ - userConfig, - }); - await runner.start(); - - const server = await runner["setupServer"](); - expect(server.userConfig.apiBaseUrl).toBe(userConfig.apiBaseUrl); + await startRunner(); - await runner.close(); + const config = await getServerConfig(); + expect(config.apiBaseUrl).toBe(userConfig.apiBaseUrl); }); }); describe("connection string modification", () => { it("should allow modifying connection string via createSessionConfig", async () => { - const createSessionConfig: TransportRunnerConfig["createSessionConfig"] = async (userConfig) => { - // Simulate fetching connection string from environment or secrets - await new Promise((resolve) => setTimeout(resolve, 10)); - - return { - ...userConfig, - connectionString: "mongodb://test-server:27017/test-db", - }; - }; - - userConfig.httpPort = 0; // Use a random port - runner = new StreamableHttpRunner({ + await startRunner({ userConfig: { ...userConfig, connectionString: undefined }, - createSessionConfig, + createSessionConfig: async ({ userConfig }) => { + // Simulate fetching connection string from environment or secrets + await new Promise((resolve) => setTimeout(resolve, 10)); + return { + ...userConfig, + connectionString: "mongodb://test-server:27017/test-db", + }; + }, }); - await runner.start(); - const server = await runner["setupServer"](); - expect(server.userConfig.connectionString).toBe("mongodb://test-server:27017/test-db"); - - await runner.close(); + const config = await getServerConfig(); + expect(config.connectionString).toBe("mongodb://test-server:27017/test-db"); }); }); describe("server integration", () => { - let client: Client; - let transport: StreamableHTTPClientTransport; - it("should successfully initialize server with createSessionConfig and serve requests", async () => { - const createSessionConfig: TransportRunnerConfig["createSessionConfig"] = async (userConfig) => { - // Simulate async config modification - await new Promise((resolve) => setTimeout(resolve, 10)); - return { - ...userConfig, - readOnly: true, // Enable read-only mode - }; - }; - - userConfig.httpPort = 0; // Use a random port - runner = new StreamableHttpRunner({ - userConfig, - createSessionConfig, + await startRunner({ + createSessionConfig: async ({ userConfig }) => { + // Simulate async config modification + await new Promise((resolve) => setTimeout(resolve, 10)); + return { + ...userConfig, + readOnly: true, // Enable read-only mode + }; + }, }); - await runner.start(); - client = new Client({ - name: "test-client", - version: "1.0.0", - }); - transport = new StreamableHTTPClientTransport(new URL(`${runner.serverAddress}/mcp`)); - - await client.connect(transport); - const response = await client.listTools(); + await createConnectedClient(); + const response = await client!.listTools(); expect(response).toBeDefined(); expect(response.tools).toBeDefined(); expect(response.tools.length).toBeGreaterThan(0); - // Verify read-only mode is applied - insert-one should not be available - const writeTools = response.tools.filter((tool) => tool.name === "insert-one"); + // Verify read-only mode is applied - insert-many should not be available + const writeTools = response.tools.filter((tool) => tool.name === "insert-many"); expect(writeTools.length).toBe(0); // Verify read tools are available const readTools = response.tools.filter((tool) => tool.name === "find"); expect(readTools.length).toBe(1); - - await client.close(); - await transport.close(); - await runner.close(); }); }); describe("error handling", () => { it("should propagate errors from configProvider on client connection", async () => { - const createSessionConfig: TransportRunnerConfig["createSessionConfig"] = async () => { - return Promise.reject(new Error("Failed to fetch config")); - }; - - userConfig.httpPort = 0; // Use a random port - runner = new StreamableHttpRunner({ - userConfig, - createSessionConfig, + await startRunner({ + createSessionConfig: async () => { + return Promise.reject(new Error("Failed to fetch config")); + }, }); - // Start succeeds because setupServer is only called when a client connects - await runner.start(); - // Error should occur when a client tries to connect - const testClient = new Client({ - name: "test-client", - version: "1.0.0", - }); - const testTransport = new StreamableHTTPClientTransport(new URL(`${runner.serverAddress}/mcp`)); - - await expect(testClient.connect(testTransport)).rejects.toThrow(); - - await testClient.close(); - await testTransport.close(); + client = new Client({ name: "test-client", version: "1.0.0" }); + transport = new StreamableHTTPClientTransport(new URL(`${runner.serverAddress}/mcp`)); - await runner.close(); + await expect(client.connect(transport)).rejects.toThrow(); }); }); }); diff --git a/tests/unit/common/config/configOverrides.test.ts b/tests/unit/common/config/configOverrides.test.ts new file mode 100644 index 000000000..cc10dfa1d --- /dev/null +++ b/tests/unit/common/config/configOverrides.test.ts @@ -0,0 +1,329 @@ +import { describe, it, expect } from "vitest"; +import { applyConfigOverrides, getConfigMeta, nameToConfigKey } from "../../../../src/common/config/configOverrides.js"; +import { UserConfigSchema, type UserConfig } from "../../../../src/common/config/userConfig.js"; +import type { RequestContext } from "../../../../src/transports/base.js"; + +describe("configOverrides", () => { + const baseConfig: Partial = { + readOnly: false, + indexCheck: false, + idleTimeoutMs: 600_000, + notificationTimeoutMs: 540_000, + disabledTools: ["tool1"], + confirmationRequiredTools: ["drop-database"], + connectionString: "mongodb://localhost:27017", + vectorSearchDimensions: 1024, + vectorSearchSimilarityFunction: "euclidean", + disableEmbeddingsValidation: false, + previewFeatures: [], + loggers: ["disk", "mcp"], + exportTimeoutMs: 300_000, + exportCleanupIntervalMs: 120_000, + atlasTemporaryDatabaseUserLifetimeMs: 14_400_000, + }; + + describe("helper functions", () => { + describe("nameToConfigKey", () => { + it("should convert header name to config key", () => { + expect(nameToConfigKey("header", "x-mongodb-mcp-read-only")).toBe("readOnly"); + expect(nameToConfigKey("header", "x-mongodb-mcp-idle-timeout-ms")).toBe("idleTimeoutMs"); + expect(nameToConfigKey("header", "x-mongodb-mcp-connection-string")).toBe("connectionString"); + }); + + it("should convert query parameter name to config key", () => { + expect(nameToConfigKey("query", "mongodbMcpReadOnly")).toBe("readOnly"); + expect(nameToConfigKey("query", "mongodbMcpIdleTimeoutMs")).toBe("idleTimeoutMs"); + expect(nameToConfigKey("query", "mongodbMcpConnectionString")).toBe("connectionString"); + }); + + it("should not mix up header and query parameter names", () => { + expect(nameToConfigKey("header", "mongodbMcpReadOnly")).toBeUndefined(); + expect(nameToConfigKey("query", "x-mongodb-mcp-read-only")).toBeUndefined(); + }); + + it("should return undefined for non-mcp names", () => { + expect(nameToConfigKey("header", "content-type")).toBeUndefined(); + expect(nameToConfigKey("header", "authorization")).toBeUndefined(); + expect(nameToConfigKey("query", "content")).toBeUndefined(); + }); + }); + + it("should get override behavior for config keys", () => { + expect(getConfigMeta("readOnly")?.overrideBehavior).toEqual(expect.any(Function)); + expect(getConfigMeta("disabledTools")?.overrideBehavior).toBe("merge"); + expect(getConfigMeta("apiBaseUrl")?.overrideBehavior).toBe("not-allowed"); + expect(getConfigMeta("maxBytesPerQuery")?.overrideBehavior).toBe("not-allowed"); + }); + }); + + describe("applyConfigOverrides", () => { + it("should return base config when request is undefined", () => { + const result = applyConfigOverrides({ baseConfig: baseConfig as UserConfig }); + expect(result).toEqual(baseConfig); + }); + + it("should return base config when request has no headers or query", () => { + const result = applyConfigOverrides({ baseConfig: baseConfig as UserConfig, request: {} }); + expect(result).toEqual(baseConfig); + }); + + describe("override behavior", () => { + it("should override boolean values with override behavior", () => { + const request: RequestContext = { + headers: { + "x-mongodb-mcp-read-only": "true", + }, + }; + const result = applyConfigOverrides({ baseConfig: baseConfig as UserConfig, request }); + expect(result.readOnly).toBe(true); + }); + + it("should override numeric values with override behavior", () => { + const request: RequestContext = { + headers: { + "x-mongodb-mcp-idle-timeout-ms": "300000", + "x-mongodb-mcp-export-timeout-ms": "600000", + }, + }; + const result = applyConfigOverrides({ baseConfig: baseConfig as UserConfig, request }); + expect(result.idleTimeoutMs).toBe(300000); + expect(result.exportTimeoutMs).toBe(600000); + }); + + it("should override string values with override behavior", () => { + const request: RequestContext = { + headers: { + "x-mongodb-mcp-connection-string": "mongodb://newhost:27017", + "x-mongodb-mcp-vector-search-similarity-function": "cosine", + }, + }; + const result = applyConfigOverrides({ baseConfig: baseConfig as UserConfig, request }); + expect(result.connectionString).toBe("mongodb://newhost:27017"); + expect(result.vectorSearchSimilarityFunction).toBe("cosine"); + }); + }); + + describe("merge behavior", () => { + it("should merge array values", () => { + const request: RequestContext = { + headers: { + "x-mongodb-mcp-disabled-tools": "tool2,tool3", + }, + }; + const result = applyConfigOverrides({ baseConfig: baseConfig as UserConfig, request }); + expect(result.disabledTools).toEqual(["tool1", "tool2", "tool3"]); + }); + + it("should merge multiple array fields", () => { + const request: RequestContext = { + headers: { + "x-mongodb-mcp-disabled-tools": "tool2", + "x-mongodb-mcp-confirmation-required-tools": "drop-collection", + "x-mongodb-mcp-preview-features": "feature1", + }, + }; + const result = applyConfigOverrides({ baseConfig: baseConfig as UserConfig, request }); + expect(result.disabledTools).toEqual(["tool1", "tool2"]); + expect(result.confirmationRequiredTools).toEqual(["drop-database", "drop-collection"]); + // previewFeatures has enum validation - "feature1" isn't a valid value, so it gets rejected + expect(result.previewFeatures).toEqual([]); + }); + + it("should merge loggers", () => { + const request: RequestContext = { + headers: { + "x-mongodb-mcp-loggers": "stderr", + }, + }; + const result = applyConfigOverrides({ baseConfig: baseConfig as UserConfig, request }); + expect(result.loggers).toEqual(["disk", "mcp", "stderr"]); + }); + }); + + describe("not-allowed behavior", () => { + it("should throw an error for not-allowed fields", () => { + const request: RequestContext = { + headers: { + "x-mongodb-mcp-api-base-url": "https://malicious.com/", + "x-mongodb-mcp-max-bytes-per-query": "999999", + "x-mongodb-mcp-max-documents-per-query": "1000", + "x-mongodb-mcp-transport": "stdio", + "x-mongodb-mcp-http-port": "9999", + }, + }; + expect(() => applyConfigOverrides({ baseConfig: baseConfig as UserConfig, request })).toThrow( + "Config key apiBaseUrl is not allowed to be overridden" + ); + }); + }); + + describe("conditional overrides", () => { + it("should have certain config keys to be conditionally overridden", () => { + expect( + Object.keys(UserConfigSchema.shape) + .map((key) => [key, getConfigMeta(key as any)?.overrideBehavior]) + .filter(([_, behavior]) => typeof behavior === "function") + .map(([key]) => key) + ).toEqual(["readOnly", "indexCheck", "disableEmbeddingsValidation"]); + }); + + it("should allow readOnly override from false to true", () => { + const request: RequestContext = { headers: { "x-mongodb-mcp-read-only": "true" } }; + const result = applyConfigOverrides({ + baseConfig: { ...baseConfig, readOnly: false } as UserConfig, + request, + }); + expect(result.readOnly).toBe(true); + }); + + it("should throw when trying to override readOnly from true to false", () => { + const request: RequestContext = { headers: { "x-mongodb-mcp-read-only": "false" } }; + expect(() => + applyConfigOverrides({ baseConfig: { ...baseConfig, readOnly: true } as UserConfig, request }) + ).toThrow("Config override validation failed for readOnly"); + }); + + it("should allow indexCheck override from false to true", () => { + const request: RequestContext = { headers: { "x-mongodb-mcp-index-check": "true" } }; + const result = applyConfigOverrides({ + baseConfig: { ...baseConfig, indexCheck: false } as UserConfig, + request, + }); + expect(result.indexCheck).toBe(true); + }); + + it("should throw when trying to override indexCheck from true to false", () => { + const request: RequestContext = { headers: { "x-mongodb-mcp-index-check": "false" } }; + expect(() => + applyConfigOverrides({ baseConfig: { ...baseConfig, indexCheck: true } as UserConfig, request }) + ).toThrow("Config override validation failed for indexCheck"); + }); + + it("should allow disableEmbeddingsValidation override from true to false", () => { + const request: RequestContext = { headers: { "x-mongodb-mcp-disable-embeddings-validation": "false" } }; + const result = applyConfigOverrides({ + baseConfig: { ...baseConfig, disableEmbeddingsValidation: true } as UserConfig, + request, + }); + expect(result.disableEmbeddingsValidation).toBe(false); + }); + + it("should throw when trying to override disableEmbeddingsValidation from false to true", () => { + const request: RequestContext = { headers: { "x-mongodb-mcp-disable-embeddings-validation": "true" } }; + expect(() => + applyConfigOverrides({ + baseConfig: { ...baseConfig, disableEmbeddingsValidation: false } as UserConfig, + request, + }) + ).toThrow("Config override validation failed for disableEmbeddingsValidation"); + }); + }); + + describe("query parameter overrides", () => { + it("should apply overrides from query parameters", () => { + const request: RequestContext = { + query: { + mongodbMcpReadOnly: "true", + mongodbMcpIdleTimeoutMs: "400000", + }, + }; + const result = applyConfigOverrides({ baseConfig: baseConfig as UserConfig, request }); + expect(result.readOnly).toBe(true); + expect(result.idleTimeoutMs).toBe(400000); + }); + + it("should merge arrays from query parameters", () => { + const request: RequestContext = { + query: { + mongodbMcpDisabledTools: "tool2,tool3", + }, + }; + const result = applyConfigOverrides({ baseConfig: baseConfig as UserConfig, request }); + expect(result.disabledTools).toEqual(["tool1", "tool2", "tool3"]); + }); + }); + + describe("precedence", () => { + it("should give query parameters precedence over headers", () => { + const request: RequestContext = { + headers: { + "x-mongodb-mcp-idle-timeout-ms": "300000", + }, + query: { + mongodbMcpIdleTimeoutMs: "500000", + }, + }; + const result = applyConfigOverrides({ baseConfig: baseConfig as UserConfig, request }); + expect(result.idleTimeoutMs).toBe(500000); + }); + + it("should merge arrays from both headers and query", () => { + const request: RequestContext = { + headers: { + "x-mongodb-mcp-disabled-tools": "tool2", + }, + query: { + mongodbMcpDisabledTools: "tool3", + }, + }; + const result = applyConfigOverrides({ baseConfig: baseConfig as UserConfig, request }); + // Query takes precedence over headers, but base + query result + expect(result.disabledTools).toEqual(["tool1", "tool3"]); + }); + }); + + describe("edge cases", () => { + it("should handle invalid numeric values gracefully", () => { + const request: RequestContext = { + headers: { + "x-mongodb-mcp-idle-timeout-ms": "not-a-number", + }, + }; + const result = applyConfigOverrides({ baseConfig: baseConfig as UserConfig, request }); + expect(result.idleTimeoutMs).toBe(baseConfig.idleTimeoutMs); + }); + + it("should handle empty string values for arrays", () => { + const request: RequestContext = { + headers: { + "x-mongodb-mcp-disabled-tools": "", + }, + }; + const result = applyConfigOverrides({ baseConfig: baseConfig as UserConfig, request }); + // Empty string gets filtered out by commaSeparatedToArray, resulting in [] + // Merging [] with ["tool1"] gives ["tool1"] + expect(result.disabledTools).toEqual(["tool1"]); + }); + + it("should trim whitespace in array values", () => { + const request: RequestContext = { + headers: { + "x-mongodb-mcp-disabled-tools": " tool2 , tool3 ", + }, + }; + const result = applyConfigOverrides({ baseConfig: baseConfig as UserConfig, request }); + expect(result.disabledTools).toEqual(["tool1", "tool2", "tool3"]); + }); + + it("should handle case-insensitive header names", () => { + const request: RequestContext = { + headers: { + "X-MongoDB-MCP-Read-Only": "true", + }, + }; + const result = applyConfigOverrides({ baseConfig: baseConfig as UserConfig, request }); + expect(result.readOnly).toBe(true); + }); + + it("should handle array values sent as multiple headers", () => { + const request: RequestContext = { + headers: { + "x-mongodb-mcp-disabled-tools": ["tool2", "tool3"], + }, + }; + const result = applyConfigOverrides({ baseConfig: baseConfig as UserConfig, request }); + expect(result.disabledTools).toEqual(["tool1", "tool2", "tool3"]); + }); + }); + }); +}); From 132876ebe31df96c3cc7b30a04a4104cdd1af600 Mon Sep 17 00:00:00 2001 From: gagik Date: Mon, 24 Nov 2025 14:59:43 +0100 Subject: [PATCH 02/21] chore: use custom overrides --- src/common/config/configOverrides.ts | 10 +++--- src/common/config/configUtils.ts | 5 +-- src/common/config/userConfig.ts | 16 +++++----- .../transports/configOverrides.test.ts | 25 ++------------- .../common/config/configOverrides.test.ts | 32 ++++++++++++++++--- 5 files changed, 48 insertions(+), 40 deletions(-) diff --git a/src/common/config/configOverrides.ts b/src/common/config/configOverrides.ts index fd3c61299..357db6d84 100644 --- a/src/common/config/configOverrides.ts +++ b/src/common/config/configOverrides.ts @@ -132,13 +132,15 @@ function applyOverride( behavior: OverrideBehavior ): unknown { if (typeof behavior === "function") { - const shouldApply = behavior(baseValue, overrideValue); - if (!shouldApply) { + // Custom logic function returns the value to use (potentially transformed) + // or throws an error if the override cannot be applied + try { + return behavior(baseValue, overrideValue); + } catch (error) { throw new Error( - `Config override validation failed for ${key}: cannot override from ${JSON.stringify(baseValue)} to ${JSON.stringify(overrideValue)}` + `Cannot apply override for ${key} from ${JSON.stringify(baseValue)} to ${JSON.stringify(overrideValue)}: ${error instanceof Error ? error.message : String(error)}` ); } - return overrideValue; } switch (behavior) { case "override": diff --git a/src/common/config/configUtils.ts b/src/common/config/configUtils.ts index 66cc7688a..ca90fc7ed 100644 --- a/src/common/config/configUtils.ts +++ b/src/common/config/configUtils.ts @@ -5,8 +5,9 @@ import * as levenshteinModule from "ts-levenshtein"; const levenshtein = levenshteinModule.default; /// Custom logic function to apply the override value. -/// Returns true if the override should be applied, false otherwise. -export type CustomOverrideLogic = (oldValue: unknown, newValue: unknown) => boolean; +/// Returns the value to use (which may be transformed from newValue). +/// Should throw an error if the override cannot be applied. +export type CustomOverrideLogic = (oldValue: unknown, newValue: unknown) => unknown; /** * Defines how a config field can be overridden via HTTP headers or query parameters. diff --git a/src/common/config/userConfig.ts b/src/common/config/userConfig.ts index afc29701f..22a0beb1d 100644 --- a/src/common/config/userConfig.ts +++ b/src/common/config/userConfig.ts @@ -89,9 +89,9 @@ export const UserConfigSchema = z4.object({ overrideBehavior: (oldValue, newValue) => { // Only allow override if setting to true from false if (oldValue === false && newValue === true) { - return true; + return newValue; } - return false; + throw new Error("Cannot disable readOnly mode"); }, }), indexCheck: z4 @@ -103,10 +103,10 @@ export const UserConfigSchema = z4.object({ .register(configRegistry, { overrideBehavior: (oldValue, newValue) => { // Only allow override if setting to true from false - if (oldValue === false && newValue === true) { - return true; + if (newValue === true) { + return newValue; } - return false; + throw new Error("Cannot disable indexCheck mode"); }, }), telemetry: z4 @@ -200,10 +200,10 @@ export const UserConfigSchema = z4.object({ .register(configRegistry, { overrideBehavior: (oldValue, newValue) => { // Only allow override if setting to false from true (making more restrictive) - if (oldValue === true && newValue === false) { - return true; + if (newValue === false) { + return newValue; } - return false; + throw new Error("Cannot disable disableEmbeddingsValidation"); }, }), vectorSearchDimensions: z4.coerce diff --git a/tests/integration/transports/configOverrides.test.ts b/tests/integration/transports/configOverrides.test.ts index 69cfcd58d..501cf1d04 100644 --- a/tests/integration/transports/configOverrides.test.ts +++ b/tests/integration/transports/configOverrides.test.ts @@ -68,27 +68,6 @@ describe("Config Overrides via HTTP", () => { expect(readTools.length).toBe(1); }); - it("should NOT override readOnly from true to false", async () => { - await startRunner({ - ...defaultTestConfig, - httpPort: 0, - readOnly: true, - }); - - try { - await connectClient({ - ["x-mongodb-mcp-read-only"]: "false", - }); - expect.fail("Expected an error to be thrown"); - } catch (error) { - if (!(error instanceof Error)) { - throw new Error("Expected an error to be thrown"); - } - expect(error.message).toContain("Error POSTing to endpoint (HTTP 400)"); - expect(error.message).toContain(`Config override validation failed for readOnly`); - } - }); - it("should override connectionString via header", async () => { await startRunner({ ...defaultTestConfig, @@ -350,7 +329,9 @@ describe("Config Overrides via HTTP", () => { throw new Error("Expected an error to be thrown"); } expect(error.message).toContain("Error POSTing to endpoint (HTTP 400)"); - expect(error.message).toContain(`Config override validation failed for readOnly`); + expect(error.message).toContain( + `Cannot apply override for readOnly from true to false: Cannot disable readOnly mode` + ); } }); }); diff --git a/tests/unit/common/config/configOverrides.test.ts b/tests/unit/common/config/configOverrides.test.ts index cc10dfa1d..06a72adf8 100644 --- a/tests/unit/common/config/configOverrides.test.ts +++ b/tests/unit/common/config/configOverrides.test.ts @@ -141,6 +141,28 @@ describe("configOverrides", () => { }); describe("not-allowed behavior", () => { + it("shoud have some not-allowed fields", () => { + expect( + Object.keys(UserConfigSchema.shape).filter( + (key) => getConfigMeta(key as any)?.overrideBehavior === "not-allowed" + ) + ).toEqual([ + "apiBaseUrl", + "apiClientId", + "apiClientSecret", + "logPath", + "telemetry", + "transport", + "httpPort", + "httpHost", + "httpHeaders", + "maxBytesPerQuery", + "maxDocumentsPerQuery", + "exportsPath", + "voyageApiKey", + ]); + }); + it("should throw an error for not-allowed fields", () => { const request: RequestContext = { headers: { @@ -157,7 +179,7 @@ describe("configOverrides", () => { }); }); - describe("conditional overrides", () => { + describe("custom overrides", () => { it("should have certain config keys to be conditionally overridden", () => { expect( Object.keys(UserConfigSchema.shape) @@ -180,7 +202,7 @@ describe("configOverrides", () => { const request: RequestContext = { headers: { "x-mongodb-mcp-read-only": "false" } }; expect(() => applyConfigOverrides({ baseConfig: { ...baseConfig, readOnly: true } as UserConfig, request }) - ).toThrow("Config override validation failed for readOnly"); + ).toThrow("Cannot apply override for readOnly from true to false: Cannot disable readOnly mode"); }); it("should allow indexCheck override from false to true", () => { @@ -196,7 +218,7 @@ describe("configOverrides", () => { const request: RequestContext = { headers: { "x-mongodb-mcp-index-check": "false" } }; expect(() => applyConfigOverrides({ baseConfig: { ...baseConfig, indexCheck: true } as UserConfig, request }) - ).toThrow("Config override validation failed for indexCheck"); + ).toThrow("Cannot apply override for indexCheck from true to false: Cannot disable indexCheck mode"); }); it("should allow disableEmbeddingsValidation override from true to false", () => { @@ -215,7 +237,9 @@ describe("configOverrides", () => { baseConfig: { ...baseConfig, disableEmbeddingsValidation: false } as UserConfig, request, }) - ).toThrow("Config override validation failed for disableEmbeddingsValidation"); + ).toThrow( + "Cannot apply override for disableEmbeddingsValidation from false to true: Cannot disable disableEmbeddingsValidation" + ); }); }); From f600d2a8d4e7727e6f6efde87137a6e254528622 Mon Sep 17 00:00:00 2001 From: gagik Date: Mon, 24 Nov 2025 15:12:07 +0100 Subject: [PATCH 03/21] add a common one way override function --- src/common/config/configUtils.ts | 11 ++++++++ src/common/config/userConfig.ts | 25 +++---------------- .../transports/configOverrides.test.ts | 2 +- .../common/config/configOverrides.test.ts | 6 ++--- 4 files changed, 19 insertions(+), 25 deletions(-) diff --git a/src/common/config/configUtils.ts b/src/common/config/configUtils.ts index ca90fc7ed..d633aa621 100644 --- a/src/common/config/configUtils.ts +++ b/src/common/config/configUtils.ts @@ -125,3 +125,14 @@ export function parseBoolean(val: unknown): unknown { } return false; } + +/** Allow overriding only to the allowed value */ +export function oneWayOverride(allowedValue: T): CustomOverrideLogic { + return (oldValue, newValue) => { + // Only allow override if setting to true from false + if (newValue === allowedValue) { + return newValue; + } + throw new Error(`Can only set to ${allowedValue ? "true" : "false"}`); + }; +} diff --git a/src/common/config/userConfig.ts b/src/common/config/userConfig.ts index 22a0beb1d..123948f95 100644 --- a/src/common/config/userConfig.ts +++ b/src/common/config/userConfig.ts @@ -5,6 +5,7 @@ import { commaSeparatedToArray, getExportsPath, getLogPath, + oneWayOverride, parseBoolean, } from "./configUtils.js"; import { previewFeatureValues, similarityValues } from "../schemas.js"; @@ -86,13 +87,7 @@ export const UserConfigSchema = z4.object({ "When set to true, only allows read, connect, and metadata operation types, disabling create/update/delete operations." ) .register(configRegistry, { - overrideBehavior: (oldValue, newValue) => { - // Only allow override if setting to true from false - if (oldValue === false && newValue === true) { - return newValue; - } - throw new Error("Cannot disable readOnly mode"); - }, + overrideBehavior: oneWayOverride(true), }), indexCheck: z4 .preprocess(parseBoolean, z4.boolean()) @@ -101,13 +96,7 @@ export const UserConfigSchema = z4.object({ "When set to true, enforces that query operations must use an index, rejecting queries that perform a collection scan." ) .register(configRegistry, { - overrideBehavior: (oldValue, newValue) => { - // Only allow override if setting to true from false - if (newValue === true) { - return newValue; - } - throw new Error("Cannot disable indexCheck mode"); - }, + overrideBehavior: oneWayOverride(true), }), telemetry: z4 .enum(["enabled", "disabled"]) @@ -198,13 +187,7 @@ export const UserConfigSchema = z4.object({ .default(false) .describe("When set to true, disables validation of embeddings dimensions.") .register(configRegistry, { - overrideBehavior: (oldValue, newValue) => { - // Only allow override if setting to false from true (making more restrictive) - if (newValue === false) { - return newValue; - } - throw new Error("Cannot disable disableEmbeddingsValidation"); - }, + overrideBehavior: oneWayOverride(false), }), vectorSearchDimensions: z4.coerce .number() diff --git a/tests/integration/transports/configOverrides.test.ts b/tests/integration/transports/configOverrides.test.ts index 501cf1d04..1a5a39e6c 100644 --- a/tests/integration/transports/configOverrides.test.ts +++ b/tests/integration/transports/configOverrides.test.ts @@ -330,7 +330,7 @@ describe("Config Overrides via HTTP", () => { } expect(error.message).toContain("Error POSTing to endpoint (HTTP 400)"); expect(error.message).toContain( - `Cannot apply override for readOnly from true to false: Cannot disable readOnly mode` + `Cannot apply override for readOnly from true to false: Can only set to true` ); } }); diff --git a/tests/unit/common/config/configOverrides.test.ts b/tests/unit/common/config/configOverrides.test.ts index 06a72adf8..755e8e67e 100644 --- a/tests/unit/common/config/configOverrides.test.ts +++ b/tests/unit/common/config/configOverrides.test.ts @@ -202,7 +202,7 @@ describe("configOverrides", () => { const request: RequestContext = { headers: { "x-mongodb-mcp-read-only": "false" } }; expect(() => applyConfigOverrides({ baseConfig: { ...baseConfig, readOnly: true } as UserConfig, request }) - ).toThrow("Cannot apply override for readOnly from true to false: Cannot disable readOnly mode"); + ).toThrow("Cannot apply override for readOnly from true to false: Can only set to true"); }); it("should allow indexCheck override from false to true", () => { @@ -218,7 +218,7 @@ describe("configOverrides", () => { const request: RequestContext = { headers: { "x-mongodb-mcp-index-check": "false" } }; expect(() => applyConfigOverrides({ baseConfig: { ...baseConfig, indexCheck: true } as UserConfig, request }) - ).toThrow("Cannot apply override for indexCheck from true to false: Cannot disable indexCheck mode"); + ).toThrow("Cannot apply override for indexCheck from true to false: Can only set to true"); }); it("should allow disableEmbeddingsValidation override from true to false", () => { @@ -238,7 +238,7 @@ describe("configOverrides", () => { request, }) ).toThrow( - "Cannot apply override for disableEmbeddingsValidation from false to true: Cannot disable disableEmbeddingsValidation" + "Cannot apply override for disableEmbeddingsValidation from false to true: Can only set to false" ); }); }); From 1a3aab29167733f57aa21ab42dfa2209bc2cbc18 Mon Sep 17 00:00:00 2001 From: Gagik Amaryan Date: Mon, 24 Nov 2025 15:22:23 +0100 Subject: [PATCH 04/21] fix typo Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tests/unit/common/config/configOverrides.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/common/config/configOverrides.test.ts b/tests/unit/common/config/configOverrides.test.ts index 755e8e67e..7119f3210 100644 --- a/tests/unit/common/config/configOverrides.test.ts +++ b/tests/unit/common/config/configOverrides.test.ts @@ -141,7 +141,7 @@ describe("configOverrides", () => { }); describe("not-allowed behavior", () => { - it("shoud have some not-allowed fields", () => { + it("should have some not-allowed fields", () => { expect( Object.keys(UserConfigSchema.shape).filter( (key) => getConfigMeta(key as any)?.overrideBehavior === "not-allowed" From 3bce26e23a9bcbba5ad5c9202ddc4792387f9d02 Mon Sep 17 00:00:00 2001 From: gagik Date: Mon, 24 Nov 2025 16:40:02 +0100 Subject: [PATCH 05/21] chore: fix lint --- scripts/generateArguments.ts | 7 +--- src/common/config/configOverrides.ts | 12 +++--- .../transports/configOverrides.test.ts | 41 +++++++++++++------ .../transports/createSessionConfig.test.ts | 14 +++---- .../common/config/configOverrides.test.ts | 11 +++-- 5 files changed, 50 insertions(+), 35 deletions(-) diff --git a/scripts/generateArguments.ts b/scripts/generateArguments.ts index 04ff001ef..d77258012 100644 --- a/scripts/generateArguments.ts +++ b/scripts/generateArguments.ts @@ -12,7 +12,6 @@ import { readFileSync, writeFileSync } from "fs"; import { join, dirname } from "path"; import { fileURLToPath } from "url"; import { UserConfigSchema, configRegistry } from "../src/common/config/userConfig.js"; -import assert from "assert"; import { execSync } from "child_process"; import { OPTIONS } from "../src/common/config/argsParserOptions.js"; @@ -69,12 +68,8 @@ function extractZodDescriptions(): Record { let description = schema.description || `Configuration option: ${key}`; if ("innerType" in schema.def) { - // "pipe" is used for our comma-separated arrays + // "pipe" & innerType is assumed to be 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'` - ); description = description.replace("An array of", "Comma separated values of"); } } diff --git a/src/common/config/configOverrides.ts b/src/common/config/configOverrides.ts index 357db6d84..d33265794 100644 --- a/src/common/config/configOverrides.ts +++ b/src/common/config/configOverrides.ts @@ -1,7 +1,7 @@ import type { UserConfig } from "./userConfig.js"; import { UserConfigSchema, configRegistry } from "./userConfig.js"; import type { RequestContext } from "../../transports/base.js"; -import type { OverrideBehavior } from "./configUtils.js"; +import type { ConfigFieldMeta, OverrideBehavior } from "./configUtils.js"; export const CONFIG_HEADER_PREFIX = "x-mongodb-mcp-"; export const CONFIG_QUERY_PREFIX = "mongodbMcp"; @@ -38,7 +38,7 @@ export function applyConfigOverrides({ const behavior = getConfigMeta(key)?.overrideBehavior || "not-allowed"; const baseValue = baseConfig[key as keyof UserConfig]; const newValue = applyOverride(key, baseValue, overrideValue, behavior); - (result as any)[key] = newValue; + (result as Record)[key] = newValue; } return result; @@ -87,7 +87,7 @@ function assertValidConfigKey(key: string): asserts key is keyof typeof UserConf /** * Gets the schema metadata for a config key. */ -export function getConfigMeta(key: keyof typeof UserConfigSchema.shape) { +export function getConfigMeta(key: keyof typeof UserConfigSchema.shape): ConfigFieldMeta | undefined { return configRegistry.get(UserConfigSchema.shape[key]); } @@ -95,7 +95,7 @@ export function getConfigMeta(key: keyof typeof UserConfigSchema.shape) { * Parses a string value to the appropriate type using the Zod schema. */ function parseConfigValue(key: keyof typeof UserConfigSchema.shape, value: unknown): unknown { - const fieldSchema = UserConfigSchema.shape[key as keyof typeof UserConfigSchema.shape]; + const fieldSchema = UserConfigSchema.shape[key]; if (!fieldSchema) { throw new Error(`Invalid config key: ${key}`); } @@ -114,7 +114,7 @@ export function nameToConfigKey(mode: "header" | "query", name: string): string if (mode === "header" && lowerCaseName.startsWith(CONFIG_HEADER_PREFIX)) { const normalized = lowerCaseName.substring(CONFIG_HEADER_PREFIX.length); // Convert kebab-case to camelCase - return normalized.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()); + return normalized.replace(/-([a-z])/g, (_, letter: string) => letter.toUpperCase()); } if (mode === "query" && name.startsWith(CONFIG_QUERY_PREFIX)) { const withoutPrefix = name.substring(CONFIG_QUERY_PREFIX.length); @@ -148,7 +148,7 @@ function applyOverride( case "merge": if (Array.isArray(baseValue) && Array.isArray(overrideValue)) { - return [...baseValue, ...overrideValue]; + return [...(baseValue as unknown[]), ...(overrideValue as unknown[])]; } throw new Error("Cannot merge non-array values, did you mean to use the 'override' behavior?"); diff --git a/tests/integration/transports/configOverrides.test.ts b/tests/integration/transports/configOverrides.test.ts index 1a5a39e6c..a06147ebd 100644 --- a/tests/integration/transports/configOverrides.test.ts +++ b/tests/integration/transports/configOverrides.test.ts @@ -2,7 +2,9 @@ import { StreamableHttpRunner } from "../../../src/transports/streamableHttp.js" import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; import { describe, expect, it, afterEach, beforeEach } from "vitest"; -import { defaultTestConfig } from "../helpers.js"; +import { defaultTestConfig, expectDefined } from "../helpers.js"; +import type { TransportRunnerConfig, UserConfig } from "../../../src/lib.js"; +import type { RequestContext } from "../../../src/transports/base.js"; describe("Config Overrides via HTTP", () => { let runner: StreamableHttpRunner; @@ -10,13 +12,16 @@ describe("Config Overrides via HTTP", () => { let transport: StreamableHTTPClientTransport; // Helper function to setup and start runner with config - async function startRunner(config: any, createSessionConfig?: any) { + async function startRunner( + config: UserConfig, + createSessionConfig?: TransportRunnerConfig["createSessionConfig"] + ): Promise { runner = new StreamableHttpRunner({ userConfig: config, createSessionConfig }); await runner.start(); } // Helper function to connect client with headers - async function connectClient(headers: Record = {}) { + async function connectClient(headers: Record = {}): Promise { transport = new StreamableHTTPClientTransport(new URL(`${runner.serverAddress}/mcp`), { requestInit: { headers }, }); @@ -106,7 +111,8 @@ describe("Config Overrides via HTTP", () => { const insertTool = response.tools.find( (tool) => tool.name === "insert-many" || tool.name === "find" || tool.name === "aggregate" ); - expect(response.tools).is.not.empty; + + expect(response.tools).not.toHaveLength(0); expect(insertTool).toBeUndefined(); }); }); @@ -228,14 +234,15 @@ describe("Config Overrides via HTTP", () => { // createSessionConfig receives the config after header overrides are applied // It can further modify it, but headers have already been applied - const createSessionConfig = async ({ + const createSessionConfig: TransportRunnerConfig["createSessionConfig"] = ({ userConfig: config, request, }: { userConfig: typeof userConfig; - request?: any; - }) => { - expect(request).toBeDefined(); + request?: RequestContext; + }): typeof userConfig => { + expectDefined(request); + expectDefined(request.headers); expect(request.headers).toBeDefined(); config.readOnly = request.headers["x-mongodb-mcp-read-only"] === "true"; config.disabledTools = ["count"]; @@ -260,7 +267,7 @@ describe("Config Overrides via HTTP", () => { const countTool = response.tools.find((tool) => tool.name === "count"); expect(countTool).toBeUndefined(); - expect(response.tools).is.not.empty; + expect(response.tools).not.toHaveLength(0); }); it("should pass request context to createSessionConfig", async () => { @@ -269,10 +276,17 @@ describe("Config Overrides via HTTP", () => { httpPort: 0, }; - let capturedRequest: any; - const createSessionConfig = async ({ request }: { userConfig: typeof userConfig; request?: any }) => { + let capturedRequest: RequestContext | undefined; + const createSessionConfig: TransportRunnerConfig["createSessionConfig"] = ({ + request, + }: { + userConfig: typeof userConfig; + request?: RequestContext; + }): Promise => { + expectDefined(request); + expectDefined(request.headers); capturedRequest = request; - return userConfig; + return Promise.resolve(userConfig); }; await startRunner(userConfig, createSessionConfig); @@ -282,7 +296,8 @@ describe("Config Overrides via HTTP", () => { }); // Verify that request context was passed - expect(capturedRequest).toBeDefined(); + expectDefined(capturedRequest); + expectDefined(capturedRequest.headers); expect(capturedRequest.headers["x-custom-header"]).toBe("test-value"); }); }); diff --git a/tests/integration/transports/createSessionConfig.test.ts b/tests/integration/transports/createSessionConfig.test.ts index 02484e9e0..d61e3dae4 100644 --- a/tests/integration/transports/createSessionConfig.test.ts +++ b/tests/integration/transports/createSessionConfig.test.ts @@ -2,8 +2,8 @@ import { StreamableHttpRunner } from "../../../src/transports/streamableHttp.js" import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; import { afterEach, describe, expect, it } from "vitest"; -import type { TransportRunnerConfig } from "../../../src/lib.js"; -import { defaultTestConfig } from "../helpers.js"; +import type { TransportRunnerConfig, UserConfig } from "../../../src/lib.js"; +import { defaultTestConfig, expectDefined } from "../helpers.js"; describe("createSessionConfig", () => { const userConfig = defaultTestConfig; @@ -17,7 +17,7 @@ describe("createSessionConfig", () => { userConfig?: typeof userConfig; createSessionConfig?: TransportRunnerConfig["createSessionConfig"]; } = {} - ) => { + ): Promise => { runner = new StreamableHttpRunner({ userConfig: { ...userConfig, httpPort: 0, ...config.userConfig }, createSessionConfig: config.createSessionConfig, @@ -27,13 +27,13 @@ describe("createSessionConfig", () => { }; // Helper to setup server and get user config - const getServerConfig = async () => { + const getServerConfig = async (): Promise => { const server = await runner["setupServer"](); return server.userConfig; }; // Helper to create and connect client - const createConnectedClient = async () => { + const createConnectedClient = async (): Promise<{ client: Client; transport: StreamableHTTPClientTransport }> => { client = new Client({ name: "test-client", version: "1.0.0" }); transport = new StreamableHTTPClientTransport(new URL(`${runner.serverAddress}/mcp`)); await client.connect(transport); @@ -109,9 +109,9 @@ describe("createSessionConfig", () => { }); await createConnectedClient(); - const response = await client!.listTools(); + const response = await client?.listTools(); + expectDefined(response); - expect(response).toBeDefined(); expect(response.tools).toBeDefined(); expect(response.tools.length).toBeGreaterThan(0); diff --git a/tests/unit/common/config/configOverrides.test.ts b/tests/unit/common/config/configOverrides.test.ts index 7119f3210..3c31b45f2 100644 --- a/tests/unit/common/config/configOverrides.test.ts +++ b/tests/unit/common/config/configOverrides.test.ts @@ -144,7 +144,9 @@ describe("configOverrides", () => { it("should have some not-allowed fields", () => { expect( Object.keys(UserConfigSchema.shape).filter( - (key) => getConfigMeta(key as any)?.overrideBehavior === "not-allowed" + (key) => + getConfigMeta(key as keyof typeof UserConfigSchema.shape)?.overrideBehavior === + "not-allowed" ) ).toEqual([ "apiBaseUrl", @@ -183,8 +185,11 @@ describe("configOverrides", () => { it("should have certain config keys to be conditionally overridden", () => { expect( Object.keys(UserConfigSchema.shape) - .map((key) => [key, getConfigMeta(key as any)?.overrideBehavior]) - .filter(([_, behavior]) => typeof behavior === "function") + .map((key) => [ + key, + getConfigMeta(key as keyof typeof UserConfigSchema.shape)?.overrideBehavior, + ]) + .filter(([, behavior]) => typeof behavior === "function") .map(([key]) => key) ).toEqual(["readOnly", "indexCheck", "disableEmbeddingsValidation"]); }); From c57dea4c6763bc27c4e6b007683656fca4370a8d Mon Sep 17 00:00:00 2001 From: gagik Date: Mon, 24 Nov 2025 16:57:42 +0100 Subject: [PATCH 06/21] chore: cleanup helper --- src/common/config/configUtils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/common/config/configUtils.ts b/src/common/config/configUtils.ts index d633aa621..96456c82f 100644 --- a/src/common/config/configUtils.ts +++ b/src/common/config/configUtils.ts @@ -129,10 +129,10 @@ export function parseBoolean(val: unknown): unknown { /** Allow overriding only to the allowed value */ export function oneWayOverride(allowedValue: T): CustomOverrideLogic { return (oldValue, newValue) => { - // Only allow override if setting to true from false + // Only allow override if setting to allowed value if (newValue === allowedValue) { return newValue; } - throw new Error(`Can only set to ${allowedValue ? "true" : "false"}`); + throw new Error(`Can only set to ${String(allowedValue)}`); }; } From c21a707a224124d5f58d43b705250adc270ab643 Mon Sep 17 00:00:00 2001 From: gagik Date: Mon, 24 Nov 2025 17:00:56 +0100 Subject: [PATCH 07/21] chore: auto-replace --- scripts/generateArguments.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/generateArguments.ts b/scripts/generateArguments.ts index d77258012..f3b470a98 100644 --- a/scripts/generateArguments.ts +++ b/scripts/generateArguments.ts @@ -68,7 +68,7 @@ function extractZodDescriptions(): Record { let description = schema.description || `Configuration option: ${key}`; if ("innerType" in schema.def) { - // "pipe" & innerType is assumed to be for our comma-separated arrays + // "pipe" is also used for our comma-separated arrays if (schema.def.innerType.def.type === "pipe") { description = description.replace("An array of", "Comma separated values of"); } From 82f38d0f0f0410e0a57b42f48e0ddca77d99a922 Mon Sep 17 00:00:00 2001 From: gagik Date: Mon, 24 Nov 2025 17:06:39 +0100 Subject: [PATCH 08/21] chore: add allowRequestOverrides --- src/common/config/configOverrides.ts | 5 ++ src/common/config/userConfig.ts | 7 +++ .../transports/configOverrides.test.ts | 33 +++++++++++++ tests/tsconfig.json | 0 .../common/config/configOverrides.test.ts | 47 +++++++++++++++++++ 5 files changed, 92 insertions(+) create mode 100644 tests/tsconfig.json diff --git a/src/common/config/configOverrides.ts b/src/common/config/configOverrides.ts index d33265794..894322c5b 100644 --- a/src/common/config/configOverrides.ts +++ b/src/common/config/configOverrides.ts @@ -25,6 +25,11 @@ export function applyConfigOverrides({ return baseConfig; } + // Only apply overrides if allowRequestOverrides is enabled + if (!baseConfig.allowRequestOverrides) { + return baseConfig; + } + const result: UserConfig = { ...baseConfig }; const overridesFromHeaders = extractConfigOverrides("header", request.headers); const overridesFromQuery = extractConfigOverrides("query", request.query); diff --git a/src/common/config/userConfig.ts b/src/common/config/userConfig.ts index 123948f95..f452f9ae9 100644 --- a/src/common/config/userConfig.ts +++ b/src/common/config/userConfig.ts @@ -207,4 +207,11 @@ export const UserConfigSchema = z4.object({ .default([]) .describe("An array of preview features that are enabled.") .register(configRegistry, { overrideBehavior: "merge" }), + allowRequestOverrides: z4 + .preprocess(parseBoolean, z4.boolean()) + .default(false) + .describe( + "When set to true, allows configuration values to be overridden via request headers and query parameters." + ) + .register(configRegistry, { overrideBehavior: "not-allowed" }), }); diff --git a/tests/integration/transports/configOverrides.test.ts b/tests/integration/transports/configOverrides.test.ts index a06147ebd..becbcc6de 100644 --- a/tests/integration/transports/configOverrides.test.ts +++ b/tests/integration/transports/configOverrides.test.ts @@ -48,11 +48,34 @@ describe("Config Overrides via HTTP", () => { }); describe("override behavior", () => { + it("should not apply overrides when allowRequestOverrides is false", async () => { + await startRunner({ + ...defaultTestConfig, + httpPort: 0, + readOnly: false, + allowRequestOverrides: false, + }); + + await connectClient({ + ["x-mongodb-mcp-read-only"]: "true", + }); + + const response = await client.listTools(); + + expect(response).toBeDefined(); + expect(response.tools).toBeDefined(); + + // Verify read-only mode is NOT applied - insert-many should still be available + const writeTools = response.tools.filter((tool) => tool.name === "insert-many"); + expect(writeTools.length).toBe(1); + }); + it("should override readOnly config via header (false to true)", async () => { await startRunner({ ...defaultTestConfig, httpPort: 0, readOnly: false, + allowRequestOverrides: true, }); await connectClient({ @@ -78,6 +101,7 @@ describe("Config Overrides via HTTP", () => { ...defaultTestConfig, httpPort: 0, connectionString: undefined, + allowRequestOverrides: true, }); await connectClient({ @@ -96,6 +120,7 @@ describe("Config Overrides via HTTP", () => { ...defaultTestConfig, httpPort: 0, disabledTools: ["insert-many"], + allowRequestOverrides: true, }); await connectClient({ @@ -158,6 +183,7 @@ describe("Config Overrides via HTTP", () => { await startRunner({ ...defaultTestConfig, httpPort: 0, + allowRequestOverrides: true, }); try { @@ -178,6 +204,7 @@ describe("Config Overrides via HTTP", () => { await startRunner({ ...defaultTestConfig, httpPort: 0, + allowRequestOverrides: true, }); try { @@ -208,6 +235,7 @@ describe("Config Overrides via HTTP", () => { ...defaultTestConfig, httpPort: 0, readOnly: false, + allowRequestOverrides: true, }); // Note: SDK doesn't support query params directly, so this test verifies the mechanism exists @@ -230,6 +258,7 @@ describe("Config Overrides via HTTP", () => { ...defaultTestConfig, httpPort: 0, readOnly: false, + allowRequestOverrides: true, }; // createSessionConfig receives the config after header overrides are applied @@ -274,6 +303,7 @@ describe("Config Overrides via HTTP", () => { const userConfig = { ...defaultTestConfig, httpPort: 0, + allowRequestOverrides: true, }; let capturedRequest: RequestContext | undefined; @@ -308,6 +338,7 @@ describe("Config Overrides via HTTP", () => { ...defaultTestConfig, httpPort: 0, readOnly: false, + allowRequestOverrides: true, }); await connectClient({ @@ -332,6 +363,7 @@ describe("Config Overrides via HTTP", () => { ...defaultTestConfig, httpPort: 0, readOnly: true, + allowRequestOverrides: true, }); try { @@ -360,6 +392,7 @@ describe("Config Overrides via HTTP", () => { indexCheck: false, idleTimeoutMs: 600_000, disabledTools: ["tool1"], + allowRequestOverrides: true, }); await connectClient({ diff --git a/tests/tsconfig.json b/tests/tsconfig.json new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit/common/config/configOverrides.test.ts b/tests/unit/common/config/configOverrides.test.ts index 3c31b45f2..3ce9a2a5e 100644 --- a/tests/unit/common/config/configOverrides.test.ts +++ b/tests/unit/common/config/configOverrides.test.ts @@ -20,6 +20,7 @@ describe("configOverrides", () => { exportTimeoutMs: 300_000, exportCleanupIntervalMs: 120_000, atlasTemporaryDatabaseUserLifetimeMs: 14_400_000, + allowRequestOverrides: true, }; describe("helper functions", () => { @@ -67,6 +68,51 @@ describe("configOverrides", () => { expect(result).toEqual(baseConfig); }); + describe("allowRequestOverrides", () => { + it("should not apply overrides when allowRequestOverrides is false", () => { + const request: RequestContext = { + headers: { + "x-mongodb-mcp-read-only": "true", + "x-mongodb-mcp-idle-timeout-ms": "300000", + }, + }; + const configWithOverridesDisabled = { + ...baseConfig, + allowRequestOverrides: false, + } as UserConfig; + const result = applyConfigOverrides({ baseConfig: configWithOverridesDisabled, request }); + // Config should remain unchanged + expect(result.readOnly).toBe(false); + expect(result.idleTimeoutMs).toBe(600_000); + }); + + it("should apply overrides when allowRequestOverrides is true", () => { + const request: RequestContext = { + headers: { + "x-mongodb-mcp-read-only": "true", + "x-mongodb-mcp-idle-timeout-ms": "300000", + }, + }; + const result = applyConfigOverrides({ baseConfig: baseConfig as UserConfig, request }); + // Config should be overridden + expect(result.readOnly).toBe(true); + expect(result.idleTimeoutMs).toBe(300000); + }); + + it("should not apply overrides by default when allowRequestOverrides is not set", () => { + const request: RequestContext = { + headers: { + "x-mongodb-mcp-read-only": "true", + }, + }; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { allowRequestOverrides, ...configWithoutOverridesFlag } = baseConfig; + const result = applyConfigOverrides({ baseConfig: configWithoutOverridesFlag as UserConfig, request }); + // Should not apply overrides since the default is false + expect(result.readOnly).toBe(false); + }); + }); + describe("override behavior", () => { it("should override boolean values with override behavior", () => { const request: RequestContext = { @@ -162,6 +208,7 @@ describe("configOverrides", () => { "maxDocumentsPerQuery", "exportsPath", "voyageApiKey", + "allowRequestOverrides", ]); }); From 94b7863f76677c13789005aade4212d70550d273 Mon Sep 17 00:00:00 2001 From: gagik Date: Mon, 24 Nov 2025 17:32:24 +0100 Subject: [PATCH 09/21] chore: fix defaults --- tests/unit/common/config.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/unit/common/config.test.ts b/tests/unit/common/config.test.ts index 372445c79..8dfe565f5 100644 --- a/tests/unit/common/config.test.ts +++ b/tests/unit/common/config.test.ts @@ -61,6 +61,7 @@ describe("config", () => { vectorSearchSimilarityFunction: "euclidean", disableEmbeddingsValidation: false, previewFeatures: [], + allowRequestOverrides: false, }; expect(UserConfigSchema.parse({})).toStrictEqual(expectedDefaults); }); @@ -99,6 +100,7 @@ describe("config", () => { vectorSearchSimilarityFunction: "euclidean", disableEmbeddingsValidation: false, previewFeatures: [], + allowRequestOverrides: false, }; expect(createUserConfig()).toStrictEqual(expectedDefaults); }); From 1b284e9c815afae0733441a725a8ff92dcd4f256 Mon Sep 17 00:00:00 2001 From: gagik Date: Mon, 24 Nov 2025 17:50:56 +0100 Subject: [PATCH 10/21] chore: add boolean parsing edge cases --- src/common/config/argsParserOptions.ts | 1 + src/common/config/configUtils.ts | 2 +- tests/unit/common/config.test.ts | 24 ++++++++++++++++++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/common/config/argsParserOptions.ts b/src/common/config/argsParserOptions.ts index 8decc318a..7ef9e7f44 100644 --- a/src/common/config/argsParserOptions.ts +++ b/src/common/config/argsParserOptions.ts @@ -18,6 +18,7 @@ export const OPTIONS = { "connectionString", "httpHost", "httpPort", + "allowRequestOverrides", "idleTimeoutMs", "logPath", "notificationTimeoutMs", diff --git a/src/common/config/configUtils.ts b/src/common/config/configUtils.ts index 96456c82f..48461ec58 100644 --- a/src/common/config/configUtils.ts +++ b/src/common/config/configUtils.ts @@ -123,7 +123,7 @@ export function parseBoolean(val: unknown): unknown { if (typeof val === "number") { return val !== 0; } - return false; + return !!val; } /** Allow overriding only to the allowed value */ diff --git a/tests/unit/common/config.test.ts b/tests/unit/common/config.test.ts index 8dfe565f5..3c5667978 100644 --- a/tests/unit/common/config.test.ts +++ b/tests/unit/common/config.test.ts @@ -452,6 +452,30 @@ describe("config", () => { cli: ["--version"], expected: { version: true }, }, + { + cli: ["--allowRequestOverrides", "false"], + expected: { allowRequestOverrides: false }, + }, + { + cli: ["--allowRequestOverrides", "0"], + expected: { allowRequestOverrides: false }, + }, + { + cli: ["--allowRequestOverrides", "1"], + expected: { allowRequestOverrides: true }, + }, + { + cli: ["--allowRequestOverrides", "true"], + expected: { allowRequestOverrides: true }, + }, + { + cli: ["--allowRequestOverrides", "yes"], + expected: { allowRequestOverrides: true }, + }, + { + cli: ["--allowRequestOverrides", ""], + expected: { allowRequestOverrides: false }, + }, ] as { cli: string[]; expected: Partial }[]; for (const { cli, expected } of testCases) { From b094b8352d8ec8f1caf8aea5d0ee3b6add335a5c Mon Sep 17 00:00:00 2001 From: gagik Date: Mon, 24 Nov 2025 17:55:01 +0100 Subject: [PATCH 11/21] chore: run generate --- README.md | 1 + server.json | 26 +++++++++ src/common/atlas/openapi.d.ts | 105 ++++++++++++++++++++++++++++++++-- 3 files changed, 126 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index d16928200..5595390e3 100644 --- a/README.md +++ b/README.md @@ -346,6 +346,7 @@ The MongoDB MCP Server can be configured using multiple methods, with the follow | CLI Option | Environment Variable | Default | Description | | -------------------------------------- | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `allowRequestOverrides` | `MDB_MCP_ALLOW_REQUEST_OVERRIDES` | `false` | When set to true, allows configuration values to be overridden via request headers and query parameters. | | `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. | diff --git a/server.json b/server.json index bc0c7ada2..67db9074f 100644 --- a/server.json +++ b/server.json @@ -16,6 +16,13 @@ "type": "stdio" }, "environmentVariables": [ + { + "name": "MDB_MCP_ALLOW_REQUEST_OVERRIDES", + "description": "When set to true, allows configuration values to be overridden via request headers and query parameters.", + "isRequired": false, + "format": "string", + "isSecret": false + }, { "name": "MDB_MCP_API_CLIENT_ID", "description": "Atlas API client ID for authentication. Required for running Atlas tools.", @@ -186,6 +193,12 @@ } ], "packageArguments": [ + { + "type": "named", + "name": "--allowRequestOverrides", + "description": "When set to true, allows configuration values to be overridden via request headers and query parameters.", + "isRequired": false + }, { "type": "named", "name": "--apiClientId", @@ -344,6 +357,13 @@ "type": "stdio" }, "environmentVariables": [ + { + "name": "MDB_MCP_ALLOW_REQUEST_OVERRIDES", + "description": "When set to true, allows configuration values to be overridden via request headers and query parameters.", + "isRequired": false, + "format": "string", + "isSecret": false + }, { "name": "MDB_MCP_API_CLIENT_ID", "description": "Atlas API client ID for authentication. Required for running Atlas tools.", @@ -514,6 +534,12 @@ } ], "packageArguments": [ + { + "type": "named", + "name": "--allowRequestOverrides", + "description": "When set to true, allows configuration values to be overridden via request headers and query parameters.", + "isRequired": false + }, { "type": "named", "name": "--apiClientId", diff --git a/src/common/atlas/openapi.d.ts b/src/common/atlas/openapi.d.ts index 33cbc04ae..a8afa4842 100644 --- a/src/common/atlas/openapi.d.ts +++ b/src/common/atlas/openapi.d.ts @@ -735,11 +735,13 @@ export interface components { ApiAtlasClusterAdvancedConfigurationView: { /** @description The custom OpenSSL cipher suite list for TLS 1.2. This field is only valid when `tlsCipherConfigMode` is set to `CUSTOM`. */ customOpensslCipherConfigTls12?: ("TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384" | "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256")[]; + /** @description The custom OpenSSL cipher suite list for TLS 1.3. This field is only valid when `tlsCipherConfigMode` is set to `CUSTOM`. */ + customOpensslCipherConfigTls13?: ("TLS_AES_256_GCM_SHA384" | "TLS_CHACHA20_POLY1305_SHA256" | "TLS_AES_128_GCM_SHA256" | "TLS_AES_128_CCM_SHA256")[]; /** * @description Minimum Transport Layer Security (TLS) version that the cluster accepts for incoming connections. Clusters using TLS 1.0 or 1.1 should consider setting TLS 1.2 as the minimum TLS protocol version. * @enum {string} */ - minimumEnabledTlsProtocol?: "TLS1_0" | "TLS1_1" | "TLS1_2"; + minimumEnabledTlsProtocol?: "TLS1_0" | "TLS1_1" | "TLS1_2" | "TLS1_3"; /** * @description The TLS cipher suite configuration mode. The default mode uses the default cipher suites. The custom mode allows you to specify custom cipher suites for both TLS 1.2 and TLS 1.3. * @enum {string} @@ -945,7 +947,7 @@ export interface components { * @description Azure region to which MongoDB Cloud deployed this network peering container. * @enum {string} */ - region: "US_CENTRAL" | "US_EAST" | "US_EAST_2" | "US_EAST_2_EUAP" | "US_NORTH_CENTRAL" | "US_WEST" | "US_SOUTH_CENTRAL" | "EUROPE_NORTH" | "EUROPE_WEST" | "US_WEST_CENTRAL" | "US_WEST_2" | "US_WEST_3" | "CANADA_EAST" | "CANADA_CENTRAL" | "BRAZIL_SOUTH" | "BRAZIL_SOUTHEAST" | "AUSTRALIA_EAST" | "AUSTRALIA_SOUTH_EAST" | "AUSTRALIA_CENTRAL" | "AUSTRALIA_CENTRAL_2" | "UAE_NORTH" | "GERMANY_WEST_CENTRAL" | "GERMANY_NORTH" | "SWITZERLAND_NORTH" | "SWITZERLAND_WEST" | "SWEDEN_CENTRAL" | "SWEDEN_SOUTH" | "UK_SOUTH" | "UK_WEST" | "INDIA_CENTRAL" | "INDIA_WEST" | "INDIA_SOUTH" | "CHINA_EAST" | "CHINA_NORTH" | "ASIA_EAST" | "JAPAN_EAST" | "JAPAN_WEST" | "ASIA_SOUTH_EAST" | "KOREA_CENTRAL" | "KOREA_SOUTH" | "FRANCE_CENTRAL" | "FRANCE_SOUTH" | "SOUTH_AFRICA_NORTH" | "SOUTH_AFRICA_WEST" | "NORWAY_EAST" | "NORWAY_WEST" | "UAE_CENTRAL" | "QATAR_CENTRAL" | "POLAND_CENTRAL" | "ISRAEL_CENTRAL" | "ITALY_NORTH" | "SPAIN_CENTRAL" | "MEXICO_CENTRAL" | "NEW_ZEALAND_NORTH"; + region: "US_CENTRAL" | "US_EAST" | "US_EAST_2" | "US_EAST_2_EUAP" | "US_NORTH_CENTRAL" | "US_WEST" | "US_SOUTH_CENTRAL" | "EUROPE_NORTH" | "EUROPE_WEST" | "US_WEST_CENTRAL" | "US_WEST_2" | "US_WEST_3" | "CANADA_EAST" | "CANADA_CENTRAL" | "BRAZIL_SOUTH" | "BRAZIL_SOUTHEAST" | "AUSTRALIA_EAST" | "AUSTRALIA_SOUTH_EAST" | "AUSTRALIA_CENTRAL" | "AUSTRALIA_CENTRAL_2" | "UAE_NORTH" | "GERMANY_WEST_CENTRAL" | "GERMANY_NORTH" | "SWITZERLAND_NORTH" | "SWITZERLAND_WEST" | "SWEDEN_CENTRAL" | "SWEDEN_SOUTH" | "UK_SOUTH" | "UK_WEST" | "INDIA_CENTRAL" | "INDIA_WEST" | "INDIA_SOUTH" | "CHINA_EAST" | "CHINA_NORTH" | "ASIA_EAST" | "JAPAN_EAST" | "JAPAN_WEST" | "ASIA_SOUTH_EAST" | "KOREA_CENTRAL" | "KOREA_SOUTH" | "FRANCE_CENTRAL" | "FRANCE_SOUTH" | "SOUTH_AFRICA_NORTH" | "SOUTH_AFRICA_WEST" | "NORWAY_EAST" | "NORWAY_WEST" | "UAE_CENTRAL" | "QATAR_CENTRAL" | "POLAND_CENTRAL" | "ISRAEL_CENTRAL" | "ITALY_NORTH" | "SPAIN_CENTRAL" | "MEXICO_CENTRAL" | "NEW_ZEALAND_NORTH" | "INDONESIA_CENTRAL" | "MALAYSIA_WEST" | "CHILE_CENTRAL"; /** @description Unique string that identifies the Azure VNet in which MongoDB Cloud clusters in this network peering container exist. The response returns **null** if no clusters exist in this network peering container. */ readonly vnetName?: string; } & { @@ -2369,6 +2371,70 @@ export interface components { /** @description Value set to the Key applied to tag and categorize this component. */ value?: string; }; + /** @description SASL_INHERIT authentication configuration from Kafka for Confluent Schema Registry. Username and password are inherited from the Kafka connection. */ + ConfluentSaslInheritAuthentication: Omit, "type"> & { + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + type: "SASL_INHERIT"; + } & { + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + type: "SASL_INHERIT"; + }; + /** @description Authentication configuration for Confluent Schema Registry. */ + ConfluentSchemaRegistryAuthentication: { + /** + * @description Authentication type discriminator. Specifies the authentication mechanism for Confluent Schema Registry. + * @enum {string} + */ + type: "USER_INFO" | "SASL_INHERIT"; + } & (components["schemas"]["ConfluentUserInfoAuthentication"] | components["schemas"]["ConfluentSaslInheritAuthentication"]); + /** @description The configuration for Confluent Schema Registry connections. */ + ConfluentSchemaRegistryConnectionPrivatePreview: Omit & { + authentication?: components["schemas"]["ConfluentSchemaRegistryAuthentication"]; + /** + * @description The Schema Registry provider. + * @enum {string} + */ + provider: "CONFLUENT"; + /** @description List of Schema Registry URLs. */ + schemaRegistryUrls?: string[]; + } & Omit & { + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + provider: "CONFLUENT"; + } & { + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + provider: "CONFLUENT"; + }; + /** @description Direct authentication with username and password for Confluent Schema Registry. */ + ConfluentUserInfoAuthentication: Omit, "type"> & { + /** @description Password for USER_INFO authentication. Required. */ + password: string; + /** @description Username for USER_INFO authentication. Required. */ + username: string; + } & { + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + type: "USER_INFO"; + } & { + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + type: "USER_INFO"; + }; /** * AWS * @description Group of Private Endpoint settings. @@ -3009,7 +3075,7 @@ export interface components { */ readonly created: string; /** @description Incident that triggered this alert. */ - readonly eventTypeName: ("CREDIT_CARD_ABOUT_TO_EXPIRE" | "PENDING_INVOICE_OVER_THRESHOLD" | "DAILY_BILL_OVER_THRESHOLD") | ("CPS_SNAPSHOT_STARTED" | "CPS_SNAPSHOT_SUCCESSFUL" | "CPS_SNAPSHOT_FAILED" | "CPS_CONCURRENT_SNAPSHOT_FAILED_WILL_RETRY" | "CPS_SNAPSHOT_BEHIND" | "CPS_COPY_SNAPSHOT_STARTED" | "CPS_COPY_SNAPSHOT_FAILED" | "CPS_COPY_SNAPSHOT_FAILED_WILL_RETRY" | "CPS_COPY_SNAPSHOT_SUCCESSFUL" | "CPS_PREV_SNAPSHOT_OLD" | "CPS_SNAPSHOT_FALLBACK_SUCCESSFUL" | "CPS_SNAPSHOT_FALLBACK_FAILED" | "CPS_RESTORE_SUCCESSFUL" | "CPS_EXPORT_SUCCESSFUL" | "CPS_RESTORE_FAILED" | "CPS_EXPORT_FAILED" | "CPS_COLLECTION_RESTORE_SUCCESSFUL" | "CPS_COLLECTION_RESTORE_FAILED" | "CPS_COLLECTION_RESTORE_PARTIAL_SUCCESS" | "CPS_COLLECTION_RESTORE_CANCELED" | "CPS_AUTO_EXPORT_FAILED" | "CPS_SNAPSHOT_DOWNLOAD_REQUEST_FAILED" | "CPS_OPLOG_BEHIND" | "CPS_OPLOG_CAUGHT_UP") | ("AWS_ENCRYPTION_KEY_NEEDS_ROTATION" | "AZURE_ENCRYPTION_KEY_NEEDS_ROTATION" | "GCP_ENCRYPTION_KEY_NEEDS_ROTATION" | "AWS_ENCRYPTION_KEY_INVALID" | "AZURE_ENCRYPTION_KEY_INVALID" | "GCP_ENCRYPTION_KEY_INVALID") | ("FTS_INDEX_DELETION_FAILED" | "FTS_INDEX_BUILD_COMPLETE" | "FTS_INDEX_BUILD_FAILED" | "FTS_INDEX_STALE" | "FTS_INDEXES_RESTORE_FAILED" | "FTS_INDEXES_SYNONYM_MAPPING_INVALID") | ("USERS_WITHOUT_MULTI_FACTOR_AUTH" | "ENCRYPTION_AT_REST_KMS_NETWORK_ACCESS_DENIED" | "ENCRYPTION_AT_REST_CONFIG_NO_LONGER_VALID" | "GROUP_SERVICE_ACCOUNT_SECRETS_EXPIRING" | "GROUP_SERVICE_ACCOUNT_SECRETS_EXPIRED") | ("CLUSTER_INSTANCE_STOP_START" | "CLUSTER_INSTANCE_RESYNC_REQUESTED" | "CLUSTER_INSTANCE_UPDATE_REQUESTED" | "SAMPLE_DATASET_LOAD_REQUESTED" | "TENANT_UPGRADE_TO_SERVERLESS_SUCCESSFUL" | "TENANT_UPGRADE_TO_SERVERLESS_FAILED" | "NETWORK_PERMISSION_ENTRY_ADDED" | "NETWORK_PERMISSION_ENTRY_REMOVED" | "NETWORK_PERMISSION_ENTRY_UPDATED" | "CLUSTER_BLOCK_WRITE" | "CLUSTER_UNBLOCK_WRITE") | ("MAINTENANCE_IN_ADVANCED" | "MAINTENANCE_AUTO_DEFERRED" | "MAINTENANCE_STARTED" | "MAINTENANCE_NO_LONGER_NEEDED") | ("NDS_X509_USER_AUTHENTICATION_CUSTOMER_CA_EXPIRATION_CHECK" | "NDS_X509_USER_AUTHENTICATION_CUSTOMER_CRL_EXPIRATION_CHECK" | "NDS_X509_USER_AUTHENTICATION_MANAGED_USER_CERTS_EXPIRATION_CHECK") | ("ONLINE_ARCHIVE_INSUFFICIENT_INDEXES_CHECK" | "ONLINE_ARCHIVE_MAX_CONSECUTIVE_OFFLOAD_WINDOWS_CHECK") | "OUTSIDE_SERVERLESS_METRIC_THRESHOLD" | "OUTSIDE_FLEX_METRIC_THRESHOLD" | ("JOINED_GROUP" | "REMOVED_FROM_GROUP" | "USER_ROLES_CHANGED_AUDIT") | ("TAGS_MODIFIED" | "CLUSTER_TAGS_MODIFIED" | "GROUP_TAGS_MODIFIED") | ("STREAM_PROCESSOR_STATE_IS_FAILED" | "OUTSIDE_STREAM_PROCESSOR_METRIC_THRESHOLD") | ("COMPUTE_AUTO_SCALE_INITIATED_BASE" | "COMPUTE_AUTO_SCALE_INITIATED_ANALYTICS" | "COMPUTE_AUTO_SCALE_SCALE_DOWN_FAIL_BASE" | "COMPUTE_AUTO_SCALE_SCALE_DOWN_FAIL_ANALYTICS" | "COMPUTE_AUTO_SCALE_MAX_INSTANCE_SIZE_FAIL_BASE" | "COMPUTE_AUTO_SCALE_MAX_INSTANCE_SIZE_FAIL_ANALYTICS" | "COMPUTE_AUTO_SCALE_OPLOG_FAIL_BASE" | "COMPUTE_AUTO_SCALE_OPLOG_FAIL_ANALYTICS" | "DISK_AUTO_SCALE_INITIATED" | "DISK_AUTO_SCALE_MAX_DISK_SIZE_FAIL" | "DISK_AUTO_SCALE_OPLOG_FAIL" | "PREDICTIVE_COMPUTE_AUTO_SCALE_INITIATED_BASE" | "PREDICTIVE_COMPUTE_AUTO_SCALE_MAX_INSTANCE_SIZE_FAIL_BASE" | "PREDICTIVE_COMPUTE_AUTO_SCALE_OPLOG_FAIL_BASE" | "CLUSTER_AUTO_SHARDING_INITIATED") | ("CPS_DATA_PROTECTION_ENABLE_REQUESTED" | "CPS_DATA_PROTECTION_ENABLED" | "CPS_DATA_PROTECTION_UPDATE_REQUESTED" | "CPS_DATA_PROTECTION_UPDATED" | "CPS_DATA_PROTECTION_DISABLE_REQUESTED" | "CPS_DATA_PROTECTION_DISABLED" | "CPS_DATA_PROTECTION_APPROVED_FOR_DISABLEMENT") | "RESOURCE_POLICY_VIOLATED"; + readonly eventTypeName: ("CREDIT_CARD_ABOUT_TO_EXPIRE" | "PENDING_INVOICE_OVER_THRESHOLD" | "DAILY_BILL_OVER_THRESHOLD") | ("CPS_SNAPSHOT_STARTED" | "CPS_SNAPSHOT_SUCCESSFUL" | "CPS_SNAPSHOT_FAILED" | "CPS_CONCURRENT_SNAPSHOT_FAILED_WILL_RETRY" | "CPS_SNAPSHOT_BEHIND" | "CPS_COPY_SNAPSHOT_STARTED" | "CPS_COPY_SNAPSHOT_FAILED" | "CPS_COPY_SNAPSHOT_FAILED_WILL_RETRY" | "CPS_COPY_SNAPSHOT_SUCCESSFUL" | "CPS_PREV_SNAPSHOT_OLD" | "CPS_SNAPSHOT_FALLBACK_SUCCESSFUL" | "CPS_SNAPSHOT_FALLBACK_FAILED" | "CPS_RESTORE_SUCCESSFUL" | "CPS_EXPORT_SUCCESSFUL" | "CPS_RESTORE_FAILED" | "CPS_EXPORT_FAILED" | "CPS_COLLECTION_RESTORE_SUCCESSFUL" | "CPS_COLLECTION_RESTORE_FAILED" | "CPS_COLLECTION_RESTORE_PARTIAL_SUCCESS" | "CPS_COLLECTION_RESTORE_CANCELED" | "CPS_AUTO_EXPORT_FAILED" | "CPS_SNAPSHOT_DOWNLOAD_REQUEST_FAILED" | "CPS_OPLOG_BEHIND" | "CPS_OPLOG_CAUGHT_UP") | ("AWS_ENCRYPTION_KEY_NEEDS_ROTATION" | "AZURE_ENCRYPTION_KEY_NEEDS_ROTATION" | "GCP_ENCRYPTION_KEY_NEEDS_ROTATION" | "AWS_ENCRYPTION_KEY_INVALID" | "AZURE_ENCRYPTION_KEY_INVALID" | "GCP_ENCRYPTION_KEY_INVALID") | ("FTS_INDEX_DELETION_FAILED" | "FTS_INDEX_BUILD_COMPLETE" | "FTS_INDEX_BUILD_FAILED" | "FTS_INDEX_STALE" | "FTS_INDEXES_RESTORE_FAILED" | "FTS_INDEXES_SYNONYM_MAPPING_INVALID") | ("USERS_WITHOUT_MULTI_FACTOR_AUTH" | "ENCRYPTION_AT_REST_KMS_NETWORK_ACCESS_DENIED" | "ENCRYPTION_AT_REST_CONFIG_NO_LONGER_VALID" | "GROUP_SERVICE_ACCOUNT_SECRETS_EXPIRING" | "GROUP_SERVICE_ACCOUNT_SECRETS_EXPIRED") | ("CLUSTER_INSTANCE_STOP_START" | "CLUSTER_INSTANCE_RESYNC_REQUESTED" | "CLUSTER_INSTANCE_UPDATE_REQUESTED" | "SAMPLE_DATASET_LOAD_REQUESTED" | "TENANT_UPGRADE_TO_SERVERLESS_SUCCESSFUL" | "TENANT_UPGRADE_TO_SERVERLESS_FAILED" | "NETWORK_PERMISSION_ENTRY_ADDED" | "NETWORK_PERMISSION_ENTRY_REMOVED" | "NETWORK_PERMISSION_ENTRY_UPDATED" | "CLUSTER_BLOCK_WRITE" | "CLUSTER_UNBLOCK_WRITE") | ("MAINTENANCE_IN_ADVANCED" | "MAINTENANCE_AUTO_DEFERRED" | "MAINTENANCE_STARTED" | "MAINTENANCE_COMPLETED" | "MAINTENANCE_NO_LONGER_NEEDED") | ("NDS_X509_USER_AUTHENTICATION_CUSTOMER_CA_EXPIRATION_CHECK" | "NDS_X509_USER_AUTHENTICATION_CUSTOMER_CRL_EXPIRATION_CHECK" | "NDS_X509_USER_AUTHENTICATION_MANAGED_USER_CERTS_EXPIRATION_CHECK") | ("ONLINE_ARCHIVE_INSUFFICIENT_INDEXES_CHECK" | "ONLINE_ARCHIVE_MAX_CONSECUTIVE_OFFLOAD_WINDOWS_CHECK") | "OUTSIDE_SERVERLESS_METRIC_THRESHOLD" | "OUTSIDE_FLEX_METRIC_THRESHOLD" | ("JOINED_GROUP" | "REMOVED_FROM_GROUP" | "USER_ROLES_CHANGED_AUDIT") | ("TAGS_MODIFIED" | "CLUSTER_TAGS_MODIFIED" | "GROUP_TAGS_MODIFIED") | ("STREAM_PROCESSOR_STATE_IS_FAILED" | "OUTSIDE_STREAM_PROCESSOR_METRIC_THRESHOLD") | ("COMPUTE_AUTO_SCALE_INITIATED_BASE" | "COMPUTE_AUTO_SCALE_INITIATED_ANALYTICS" | "COMPUTE_AUTO_SCALE_SCALE_DOWN_FAIL_BASE" | "COMPUTE_AUTO_SCALE_SCALE_DOWN_FAIL_ANALYTICS" | "COMPUTE_AUTO_SCALE_MAX_INSTANCE_SIZE_FAIL_BASE" | "COMPUTE_AUTO_SCALE_MAX_INSTANCE_SIZE_FAIL_ANALYTICS" | "COMPUTE_AUTO_SCALE_OPLOG_FAIL_BASE" | "COMPUTE_AUTO_SCALE_OPLOG_FAIL_ANALYTICS" | "DISK_AUTO_SCALE_INITIATED" | "DISK_AUTO_SCALE_MAX_DISK_SIZE_FAIL" | "DISK_AUTO_SCALE_OPLOG_FAIL" | "PREDICTIVE_COMPUTE_AUTO_SCALE_INITIATED_BASE" | "PREDICTIVE_COMPUTE_AUTO_SCALE_MAX_INSTANCE_SIZE_FAIL_BASE" | "PREDICTIVE_COMPUTE_AUTO_SCALE_OPLOG_FAIL_BASE" | "CLUSTER_AUTO_SHARDING_INITIATED") | ("CPS_DATA_PROTECTION_ENABLE_REQUESTED" | "CPS_DATA_PROTECTION_ENABLED" | "CPS_DATA_PROTECTION_UPDATE_REQUESTED" | "CPS_DATA_PROTECTION_UPDATED" | "CPS_DATA_PROTECTION_DISABLE_REQUESTED" | "CPS_DATA_PROTECTION_DISABLED" | "CPS_DATA_PROTECTION_APPROVED_FOR_DISABLEMENT") | "RESOURCE_POLICY_VIOLATED"; /** * @description Unique 24-hexadecimal digit string that identifies the project that owns this alert. * @example 32b6e34b3d91647abb20e7b8 @@ -3884,7 +3950,7 @@ export interface components { * @example HOST_DOWN * @enum {string} */ - HostEventTypeViewForNdsGroupAlertable: "HOST_DOWN" | "HOST_HAS_INDEX_SUGGESTIONS" | "HOST_MONGOT_CRASHING_OOM" | "HOST_MONGOT_STOP_REPLICATION" | "HOST_MONGOT_APPROACHING_STOP_REPLICATION" | "HOST_SEARCH_NODE_INDEX_FAILED" | "HOST_NOT_ENOUGH_DISK_SPACE" | "SSH_KEY_NDS_HOST_ACCESS_REQUESTED" | "SSH_KEY_NDS_HOST_ACCESS_REFRESHED" | "PUSH_BASED_LOG_EXPORT_STOPPED" | "PUSH_BASED_LOG_EXPORT_DROPPED_LOG" | "HOST_VERSION_BEHIND" | "VERSION_BEHIND" | "HOST_EXPOSED" | "HOST_SSL_CERTIFICATE_STALE" | "HOST_SECURITY_CHECKUP_NOT_MET"; + HostEventTypeViewForNdsGroupAlertable: "HOST_DOWN" | "HOST_HAS_INDEX_SUGGESTIONS" | "HOST_MONGOT_CRASHING_OOM" | "HOST_MONGOT_STOP_REPLICATION" | "HOST_MONGOT_APPROACHING_STOP_REPLICATION" | "HOST_MONGOT_PAUSE_INITIAL_SYNC" | "HOST_SEARCH_NODE_INDEX_FAILED" | "HOST_EXTERNAL_LOG_SINK_EXPORT_DOWN" | "HOST_NOT_ENOUGH_DISK_SPACE" | "SSH_KEY_NDS_HOST_ACCESS_REQUESTED" | "SSH_KEY_NDS_HOST_ACCESS_REFRESHED" | "PUSH_BASED_LOG_EXPORT_STOPPED" | "PUSH_BASED_LOG_EXPORT_DROPPED_LOG" | "HOST_VERSION_BEHIND" | "VERSION_BEHIND" | "HOST_EXPOSED" | "HOST_SSL_CERTIFICATE_STALE" | "HOST_SECURITY_CHECKUP_NOT_MET"; /** * Host Metric Alerts * @description Host Metric Alert notifies about changes of measurements or metrics for mongod host. @@ -4002,7 +4068,7 @@ export interface components { * @description Element used to express the quantity in **currentValue.number**. This can be an element of time, storage capacity, and the like. This metric triggered the alert. * @enum {string} */ - readonly units?: "bits" | "Kbits" | "Mbits" | "Gbits" | "bytes" | "KB" | "MB" | "GB" | "TB" | "PB" | "nsec" | "msec" | "sec" | "min" | "hours" | "million minutes" | "days" | "requests" | "1000 requests" | "tokens" | "pixels" | "GB seconds" | "GB hours" | "GB days" | "RPU" | "thousand RPU" | "million RPU" | "WPU" | "thousand WPU" | "million WPU" | "count" | "thousand" | "million" | "billion"; + readonly units?: "bits" | "Kbits" | "Mbits" | "Gbits" | "bytes" | "KB" | "MB" | "GB" | "TB" | "PB" | "nsec" | "msec" | "sec" | "min" | "hours" | "million minutes" | "days" | "requests" | "1000 requests" | "tokens" | "million tokens" | "pixels" | "billion pixels" | "GB seconds" | "GB hours" | "GB days" | "RPU" | "thousand RPU" | "million RPU" | "WPU" | "thousand WPU" | "million WPU" | "count" | "thousand" | "million" | "billion"; }; /** * Ingestion Destination @@ -4071,7 +4137,7 @@ export interface components { * @description Human-readable description of the service that this line item provided. This Stock Keeping Unit (SKU) could be the instance type, a support charge, advanced security, or another service. * @enum {string} */ - readonly sku?: "CLASSIC_BACKUP_OPLOG" | "CLASSIC_BACKUP_STORAGE" | "CLASSIC_BACKUP_SNAPSHOT_CREATE" | "CLASSIC_BACKUP_DAILY_MINIMUM" | "CLASSIC_BACKUP_FREE_TIER" | "CLASSIC_COUPON" | "BACKUP_STORAGE_FREE_TIER" | "BACKUP_STORAGE" | "FLEX_CONSULTING" | "CLOUD_MANAGER_CLASSIC" | "CLOUD_MANAGER_BASIC_FREE_TIER" | "CLOUD_MANAGER_BASIC" | "CLOUD_MANAGER_PREMIUM" | "CLOUD_MANAGER_FREE_TIER" | "CLOUD_MANAGER_STANDARD_FREE_TIER" | "CLOUD_MANAGER_STANDARD_ANNUAL" | "CLOUD_MANAGER_STANDARD" | "CLOUD_MANAGER_FREE_TRIAL" | "ATLAS_INSTANCE_M0" | "ATLAS_INSTANCE_M2" | "ATLAS_INSTANCE_M5" | "ATLAS_AWS_INSTANCE_M10" | "ATLAS_AWS_INSTANCE_M20" | "ATLAS_AWS_INSTANCE_M30" | "ATLAS_AWS_INSTANCE_M40" | "ATLAS_AWS_INSTANCE_M50" | "ATLAS_AWS_INSTANCE_M60" | "ATLAS_AWS_INSTANCE_M80" | "ATLAS_AWS_INSTANCE_M100" | "ATLAS_AWS_INSTANCE_M140" | "ATLAS_AWS_INSTANCE_M200" | "ATLAS_AWS_INSTANCE_M300" | "ATLAS_AWS_INSTANCE_M40_LOW_CPU" | "ATLAS_AWS_INSTANCE_M50_LOW_CPU" | "ATLAS_AWS_INSTANCE_M60_LOW_CPU" | "ATLAS_AWS_INSTANCE_M80_LOW_CPU" | "ATLAS_AWS_INSTANCE_M200_LOW_CPU" | "ATLAS_AWS_INSTANCE_M300_LOW_CPU" | "ATLAS_AWS_INSTANCE_M400_LOW_CPU" | "ATLAS_AWS_INSTANCE_M700_LOW_CPU" | "ATLAS_AWS_INSTANCE_M40_NVME" | "ATLAS_AWS_INSTANCE_M50_NVME" | "ATLAS_AWS_INSTANCE_M60_NVME" | "ATLAS_AWS_INSTANCE_M80_NVME" | "ATLAS_AWS_INSTANCE_M200_NVME" | "ATLAS_AWS_INSTANCE_M400_NVME" | "ATLAS_AWS_INSTANCE_M10_PAUSED" | "ATLAS_AWS_INSTANCE_M20_PAUSED" | "ATLAS_AWS_INSTANCE_M30_PAUSED" | "ATLAS_AWS_INSTANCE_M40_PAUSED" | "ATLAS_AWS_INSTANCE_M50_PAUSED" | "ATLAS_AWS_INSTANCE_M60_PAUSED" | "ATLAS_AWS_INSTANCE_M80_PAUSED" | "ATLAS_AWS_INSTANCE_M100_PAUSED" | "ATLAS_AWS_INSTANCE_M140_PAUSED" | "ATLAS_AWS_INSTANCE_M200_PAUSED" | "ATLAS_AWS_INSTANCE_M300_PAUSED" | "ATLAS_AWS_INSTANCE_M40_LOW_CPU_PAUSED" | "ATLAS_AWS_INSTANCE_M50_LOW_CPU_PAUSED" | "ATLAS_AWS_INSTANCE_M60_LOW_CPU_PAUSED" | "ATLAS_AWS_INSTANCE_M80_LOW_CPU_PAUSED" | "ATLAS_AWS_INSTANCE_M200_LOW_CPU_PAUSED" | "ATLAS_AWS_INSTANCE_M300_LOW_CPU_PAUSED" | "ATLAS_AWS_INSTANCE_M400_LOW_CPU_PAUSED" | "ATLAS_AWS_INSTANCE_M700_LOW_CPU_PAUSED" | "ATLAS_AWS_SEARCH_INSTANCE_S20_COMPUTE_NVME" | "ATLAS_AWS_SEARCH_INSTANCE_S30_COMPUTE_NVME" | "ATLAS_AWS_SEARCH_INSTANCE_S40_COMPUTE_NVME" | "ATLAS_AWS_SEARCH_INSTANCE_S50_COMPUTE_NVME" | "ATLAS_AWS_SEARCH_INSTANCE_S60_COMPUTE_NVME" | "ATLAS_AWS_SEARCH_INSTANCE_S70_COMPUTE_NVME" | "ATLAS_AWS_SEARCH_INSTANCE_S80_COMPUTE_NVME" | "ATLAS_AWS_SEARCH_INSTANCE_S30_MEMORY_NVME" | "ATLAS_AWS_SEARCH_INSTANCE_S40_MEMORY_NVME" | "ATLAS_AWS_SEARCH_INSTANCE_S50_MEMORY_NVME" | "ATLAS_AWS_SEARCH_INSTANCE_S60_MEMORY_NVME" | "ATLAS_AWS_SEARCH_INSTANCE_S80_MEMORY_NVME" | "ATLAS_AWS_SEARCH_INSTANCE_S90_MEMORY_NVME" | "ATLAS_AWS_SEARCH_INSTANCE_S100_MEMORY_NVME" | "ATLAS_AWS_SEARCH_INSTANCE_S110_MEMORY_NVME" | "ATLAS_AWS_SEARCH_INSTANCE_S40_STORAGE_NVME" | "ATLAS_AWS_SEARCH_INSTANCE_S50_STORAGE_NVME" | "ATLAS_AWS_SEARCH_INSTANCE_S60_STORAGE_NVME" | "ATLAS_AWS_SEARCH_INSTANCE_S80_STORAGE_NVME" | "ATLAS_AWS_SEARCH_INSTANCE_S90_STORAGE_NVME" | "ATLAS_AWS_STORAGE_PROVISIONED" | "ATLAS_AWS_STORAGE_STANDARD" | "ATLAS_AWS_STORAGE_STANDARD_GP3" | "ATLAS_AWS_STORAGE_IOPS" | "ATLAS_AWS_DATA_TRANSFER_SAME_REGION" | "ATLAS_AWS_DATA_TRANSFER_DIFFERENT_REGION" | "ATLAS_AWS_DATA_TRANSFER_INTERNET" | "ATLAS_AWS_TIME_BASED_SNAPSHOT_COPY_LEVEL_1" | "ATLAS_AWS_TIME_BASED_SNAPSHOT_COPY_LEVEL_2" | "ATLAS_AWS_TIME_BASED_SNAPSHOT_COPY_LEVEL_3" | "ATLAS_AWS_TIME_BASED_SNAPSHOT_COPY_LEVEL_4" | "ATLAS_AWS_TIME_BASED_SNAPSHOT_COPY_LEVEL_5" | "ATLAS_AWS_TIME_BASED_SNAPSHOT_COPY_LEVEL_6" | "ATLAS_AWS_TIME_BASED_SNAPSHOT_COPY_LEVEL_7" | "ATLAS_AWS_BACKUP_SNAPSHOT_STORAGE" | "ATLAS_AWS_BACKUP_DOWNLOAD_VM" | "ATLAS_AWS_BACKUP_DOWNLOAD_VM_STORAGE" | "ATLAS_AWS_BACKUP_DOWNLOAD_VM_STORAGE_IOPS" | "ATLAS_AWS_PRIVATE_ENDPOINT" | "ATLAS_AWS_PRIVATE_ENDPOINT_CAPACITY_UNITS" | "ATLAS_GCP_SEARCH_INSTANCE_S20_COMPUTE_LOCALSSD" | "ATLAS_GCP_SEARCH_INSTANCE_S30_COMPUTE_LOCALSSD" | "ATLAS_GCP_SEARCH_INSTANCE_S40_COMPUTE_LOCALSSD" | "ATLAS_GCP_SEARCH_INSTANCE_S50_COMPUTE_LOCALSSD" | "ATLAS_GCP_SEARCH_INSTANCE_S60_COMPUTE_LOCALSSD" | "ATLAS_GCP_SEARCH_INSTANCE_S70_COMPUTE_LOCALSSD" | "ATLAS_GCP_SEARCH_INSTANCE_S80_COMPUTE_LOCALSSD" | "ATLAS_GCP_SEARCH_INSTANCE_S30_MEMORY_LOCALSSD" | "ATLAS_GCP_SEARCH_INSTANCE_S40_MEMORY_LOCALSSD" | "ATLAS_GCP_SEARCH_INSTANCE_S50_MEMORY_LOCALSSD" | "ATLAS_GCP_SEARCH_INSTANCE_S60_MEMORY_LOCALSSD" | "ATLAS_GCP_SEARCH_INSTANCE_S70_MEMORY_LOCALSSD" | "ATLAS_GCP_SEARCH_INSTANCE_S80_MEMORY_LOCALSSD" | "ATLAS_GCP_SEARCH_INSTANCE_S90_MEMORY_LOCALSSD" | "ATLAS_GCP_SEARCH_INSTANCE_S100_MEMORY_LOCALSSD" | "ATLAS_GCP_SEARCH_INSTANCE_S110_MEMORY_LOCALSSD" | "ATLAS_GCP_SEARCH_INSTANCE_S120_MEMORY_LOCALSSD" | "ATLAS_GCP_SEARCH_INSTANCE_S130_MEMORY_LOCALSSD" | "ATLAS_GCP_SEARCH_INSTANCE_S140_MEMORY_LOCALSSD" | "ATLAS_GCP_INSTANCE_M10" | "ATLAS_GCP_INSTANCE_M20" | "ATLAS_GCP_INSTANCE_M30" | "ATLAS_GCP_INSTANCE_M40" | "ATLAS_GCP_INSTANCE_M50" | "ATLAS_GCP_INSTANCE_M60" | "ATLAS_GCP_INSTANCE_M80" | "ATLAS_GCP_INSTANCE_M140" | "ATLAS_GCP_INSTANCE_M200" | "ATLAS_GCP_INSTANCE_M250" | "ATLAS_GCP_INSTANCE_M300" | "ATLAS_GCP_INSTANCE_M400" | "ATLAS_GCP_INSTANCE_M40_LOW_CPU" | "ATLAS_GCP_INSTANCE_M50_LOW_CPU" | "ATLAS_GCP_INSTANCE_M60_LOW_CPU" | "ATLAS_GCP_INSTANCE_M80_LOW_CPU" | "ATLAS_GCP_INSTANCE_M200_LOW_CPU" | "ATLAS_GCP_INSTANCE_M300_LOW_CPU" | "ATLAS_GCP_INSTANCE_M400_LOW_CPU" | "ATLAS_GCP_INSTANCE_M600_LOW_CPU" | "ATLAS_GCP_INSTANCE_M10_PAUSED" | "ATLAS_GCP_INSTANCE_M20_PAUSED" | "ATLAS_GCP_INSTANCE_M30_PAUSED" | "ATLAS_GCP_INSTANCE_M40_PAUSED" | "ATLAS_GCP_INSTANCE_M50_PAUSED" | "ATLAS_GCP_INSTANCE_M60_PAUSED" | "ATLAS_GCP_INSTANCE_M80_PAUSED" | "ATLAS_GCP_INSTANCE_M140_PAUSED" | "ATLAS_GCP_INSTANCE_M200_PAUSED" | "ATLAS_GCP_INSTANCE_M250_PAUSED" | "ATLAS_GCP_INSTANCE_M300_PAUSED" | "ATLAS_GCP_INSTANCE_M400_PAUSED" | "ATLAS_GCP_INSTANCE_M40_LOW_CPU_PAUSED" | "ATLAS_GCP_INSTANCE_M50_LOW_CPU_PAUSED" | "ATLAS_GCP_INSTANCE_M60_LOW_CPU_PAUSED" | "ATLAS_GCP_INSTANCE_M80_LOW_CPU_PAUSED" | "ATLAS_GCP_INSTANCE_M200_LOW_CPU_PAUSED" | "ATLAS_GCP_INSTANCE_M300_LOW_CPU_PAUSED" | "ATLAS_GCP_INSTANCE_M400_LOW_CPU_PAUSED" | "ATLAS_GCP_INSTANCE_M600_LOW_CPU_PAUSED" | "ATLAS_GCP_DATA_TRANSFER_INTERNET" | "ATLAS_GCP_STORAGE_SSD" | "ATLAS_GCP_DATA_TRANSFER_INTER_CONNECT" | "ATLAS_GCP_DATA_TRANSFER_INTER_ZONE" | "ATLAS_GCP_DATA_TRANSFER_INTER_REGION" | "ATLAS_GCP_DATA_TRANSFER_GOOGLE" | "ATLAS_GCP_BACKUP_SNAPSHOT_STORAGE" | "ATLAS_GCP_BACKUP_DOWNLOAD_VM" | "ATLAS_GCP_BACKUP_DOWNLOAD_VM_STORAGE" | "ATLAS_GCP_PRIVATE_ENDPOINT" | "ATLAS_GCP_PRIVATE_ENDPOINT_CAPACITY_UNITS" | "ATLAS_GCP_SNAPSHOT_COPY_DATA_TRANSFER" | "ATLAS_AZURE_INSTANCE_M10" | "ATLAS_AZURE_INSTANCE_M20" | "ATLAS_AZURE_INSTANCE_M30" | "ATLAS_AZURE_INSTANCE_M40" | "ATLAS_AZURE_INSTANCE_M50" | "ATLAS_AZURE_INSTANCE_M60" | "ATLAS_AZURE_INSTANCE_M80" | "ATLAS_AZURE_INSTANCE_M90" | "ATLAS_AZURE_INSTANCE_M200" | "ATLAS_AZURE_INSTANCE_R40" | "ATLAS_AZURE_INSTANCE_R50" | "ATLAS_AZURE_INSTANCE_R60" | "ATLAS_AZURE_INSTANCE_R80" | "ATLAS_AZURE_INSTANCE_R200" | "ATLAS_AZURE_INSTANCE_R300" | "ATLAS_AZURE_INSTANCE_R400" | "ATLAS_AZURE_INSTANCE_M60_NVME" | "ATLAS_AZURE_INSTANCE_M80_NVME" | "ATLAS_AZURE_INSTANCE_M200_NVME" | "ATLAS_AZURE_INSTANCE_M300_NVME" | "ATLAS_AZURE_INSTANCE_M400_NVME" | "ATLAS_AZURE_INSTANCE_M600_NVME" | "ATLAS_AZURE_INSTANCE_M10_PAUSED" | "ATLAS_AZURE_INSTANCE_M20_PAUSED" | "ATLAS_AZURE_INSTANCE_M30_PAUSED" | "ATLAS_AZURE_INSTANCE_M40_PAUSED" | "ATLAS_AZURE_INSTANCE_M50_PAUSED" | "ATLAS_AZURE_INSTANCE_M60_PAUSED" | "ATLAS_AZURE_INSTANCE_M80_PAUSED" | "ATLAS_AZURE_INSTANCE_M90_PAUSED" | "ATLAS_AZURE_INSTANCE_M200_PAUSED" | "ATLAS_AZURE_INSTANCE_R40_PAUSED" | "ATLAS_AZURE_INSTANCE_R50_PAUSED" | "ATLAS_AZURE_INSTANCE_R60_PAUSED" | "ATLAS_AZURE_INSTANCE_R80_PAUSED" | "ATLAS_AZURE_INSTANCE_R200_PAUSED" | "ATLAS_AZURE_INSTANCE_R300_PAUSED" | "ATLAS_AZURE_INSTANCE_R400_PAUSED" | "ATLAS_AZURE_SEARCH_INSTANCE_S20_COMPUTE_LOCALSSD" | "ATLAS_AZURE_SEARCH_INSTANCE_S30_COMPUTE_LOCALSSD" | "ATLAS_AZURE_SEARCH_INSTANCE_S40_COMPUTE_LOCALSSD" | "ATLAS_AZURE_SEARCH_INSTANCE_S50_COMPUTE_LOCALSSD" | "ATLAS_AZURE_SEARCH_INSTANCE_S60_COMPUTE_LOCALSSD" | "ATLAS_AZURE_SEARCH_INSTANCE_S70_COMPUTE_LOCALSSD" | "ATLAS_AZURE_SEARCH_INSTANCE_S80_COMPUTE_LOCALSSD" | "ATLAS_AZURE_SEARCH_INSTANCE_S40_MEMORY_LOCALSSD" | "ATLAS_AZURE_SEARCH_INSTANCE_S50_MEMORY_LOCALSSD" | "ATLAS_AZURE_SEARCH_INSTANCE_S60_MEMORY_LOCALSSD" | "ATLAS_AZURE_SEARCH_INSTANCE_S80_MEMORY_LOCALSSD" | "ATLAS_AZURE_SEARCH_INSTANCE_S90_MEMORY_LOCALSSD" | "ATLAS_AZURE_SEARCH_INSTANCE_S100_MEMORY_LOCALSSD" | "ATLAS_AZURE_SEARCH_INSTANCE_S110_MEMORY_LOCALSSD" | "ATLAS_AZURE_SEARCH_INSTANCE_S130_MEMORY_LOCALSSD" | "ATLAS_AZURE_SEARCH_INSTANCE_S135_MEMORY_LOCALSSD" | "ATLAS_AZURE_STORAGE_P2" | "ATLAS_AZURE_STORAGE_P3" | "ATLAS_AZURE_STORAGE_P4" | "ATLAS_AZURE_STORAGE_P6" | "ATLAS_AZURE_STORAGE_P10" | "ATLAS_AZURE_STORAGE_P15" | "ATLAS_AZURE_STORAGE_P20" | "ATLAS_AZURE_STORAGE_P30" | "ATLAS_AZURE_STORAGE_P40" | "ATLAS_AZURE_STORAGE_P50" | "ATLAS_AZURE_DATA_TRANSFER" | "ATLAS_AZURE_DATA_TRANSFER_REGIONAL_VNET_IN" | "ATLAS_AZURE_DATA_TRANSFER_REGIONAL_VNET_OUT" | "ATLAS_AZURE_DATA_TRANSFER_GLOBAL_VNET_IN" | "ATLAS_AZURE_DATA_TRANSFER_GLOBAL_VNET_OUT" | "ATLAS_AZURE_DATA_TRANSFER_AVAILABILITY_ZONE_IN" | "ATLAS_AZURE_DATA_TRANSFER_AVAILABILITY_ZONE_OUT" | "ATLAS_AZURE_DATA_TRANSFER_INTER_REGION_INTRA_CONTINENT" | "ATLAS_AZURE_DATA_TRANSFER_INTER_REGION_INTER_CONTINENT" | "ATLAS_AZURE_BACKUP_SNAPSHOT_STORAGE" | "ATLAS_AZURE_BACKUP_DOWNLOAD_VM" | "ATLAS_AZURE_BACKUP_DOWNLOAD_VM_STORAGE_P2" | "ATLAS_AZURE_BACKUP_DOWNLOAD_VM_STORAGE_P3" | "ATLAS_AZURE_BACKUP_DOWNLOAD_VM_STORAGE_P4" | "ATLAS_AZURE_BACKUP_DOWNLOAD_VM_STORAGE_P6" | "ATLAS_AZURE_BACKUP_DOWNLOAD_VM_STORAGE_P10" | "ATLAS_AZURE_BACKUP_DOWNLOAD_VM_STORAGE_P15" | "ATLAS_AZURE_BACKUP_DOWNLOAD_VM_STORAGE_P20" | "ATLAS_AZURE_BACKUP_DOWNLOAD_VM_STORAGE_P30" | "ATLAS_AZURE_BACKUP_DOWNLOAD_VM_STORAGE_P40" | "ATLAS_AZURE_BACKUP_DOWNLOAD_VM_STORAGE_P50" | "ATLAS_AZURE_STANDARD_STORAGE" | "ATLAS_AZURE_EXTENDED_STANDARD_IOPS" | "ATLAS_AZURE_BACKUP_DOWNLOAD_VM_STORAGE" | "ATLAS_AZURE_BACKUP_DOWNLOAD_VM_STORAGE_EXTENDED_IOPS" | "ATLAS_AZURE_SNAPSHOT_EXPORT_VM_STORAGE" | "ATLAS_AZURE_SNAPSHOT_EXPORT_VM_STORAGE_EXTENDED_IOPS" | "ATLAS_BI_CONNECTOR" | "ATLAS_ADVANCED_SECURITY" | "ATLAS_ENTERPRISE_AUDITING" | "ATLAS_FREE_SUPPORT" | "ATLAS_SUPPORT" | "ATLAS_NDS_BACKFILL_SUPPORT" | "STITCH_DATA_DOWNLOADED_FREE_TIER" | "STITCH_DATA_DOWNLOADED" | "STITCH_COMPUTE_FREE_TIER" | "STITCH_COMPUTE" | "CREDIT" | "MINIMUM_CHARGE" | "CHARTS_DATA_DOWNLOADED_FREE_TIER" | "CHARTS_DATA_DOWNLOADED" | "ATLAS_DATA_LAKE_AWS_DATA_RETURNED_SAME_REGION" | "ATLAS_DATA_LAKE_AWS_DATA_RETURNED_DIFFERENT_REGION" | "ATLAS_DATA_LAKE_AWS_DATA_RETURNED_INTERNET" | "ATLAS_DATA_LAKE_AWS_DATA_SCANNED" | "ATLAS_DATA_LAKE_AWS_DATA_TRANSFERRED_FROM_DIFFERENT_REGION" | "ATLAS_NDS_AWS_DATA_LAKE_STORAGE_ACCESS" | "ATLAS_NDS_AWS_DATA_LAKE_STORAGE" | "ATLAS_DATA_FEDERATION_AZURE_DATA_RETURNED_SAME_REGION" | "ATLAS_DATA_FEDERATION_AZURE_DATA_RETURNED_SAME_CONTINENT" | "ATLAS_DATA_FEDERATION_AZURE_DATA_RETURNED_DIFFERENT_CONTINENT" | "ATLAS_DATA_FEDERATION_AZURE_DATA_RETURNED_INTERNET" | "ATLAS_DATA_FEDERATION_GCP_DATA_RETURNED_SAME_REGION" | "ATLAS_DATA_FEDERATION_GCP_DATA_RETURNED_DIFFERENT_REGION" | "ATLAS_DATA_FEDERATION_GCP_DATA_RETURNED_INTERNET" | "ATLAS_DATA_FEDERATION_AZURE_DATA_SCANNED" | "ATLAS_NDS_AZURE_DATA_LAKE_STORAGE_ACCESS" | "ATLAS_NDS_AZURE_DATA_LAKE_STORAGE" | "ATLAS_DATA_FEDERATION_GCP_DATA_SCANNED" | "ATLAS_NDS_GCP_DATA_LAKE_STORAGE_ACCESS" | "ATLAS_NDS_GCP_DATA_LAKE_STORAGE" | "ATLAS_NDS_AWS_OBJECT_STORAGE_ACCESS" | "ATLAS_NDS_AWS_COMPRESSED_OBJECT_STORAGE" | "ATLAS_NDS_AZURE_OBJECT_STORAGE_ACCESS" | "ATLAS_NDS_AZURE_OBJECT_STORAGE" | "ATLAS_NDS_AZURE_COMPRESSED_OBJECT_STORAGE" | "ATLAS_NDS_GCP_OBJECT_STORAGE_ACCESS" | "ATLAS_NDS_GCP_OBJECT_STORAGE" | "ATLAS_NDS_GCP_COMPRESSED_OBJECT_STORAGE" | "ATLAS_ARCHIVE_ACCESS_PARTITION_LOCATE" | "ATLAS_NDS_AWS_PIT_RESTORE_STORAGE_FREE_TIER" | "ATLAS_NDS_AWS_PIT_RESTORE_STORAGE" | "ATLAS_NDS_GCP_PIT_RESTORE_STORAGE_FREE_TIER" | "ATLAS_NDS_GCP_PIT_RESTORE_STORAGE" | "ATLAS_NDS_AZURE_PIT_RESTORE_STORAGE_FREE_TIER" | "ATLAS_NDS_AZURE_PIT_RESTORE_STORAGE" | "ATLAS_NDS_AZURE_PRIVATE_ENDPOINT_CAPACITY_UNITS" | "ATLAS_NDS_AZURE_CMK_PRIVATE_NETWORKING" | "ATLAS_NDS_AWS_CMK_PRIVATE_NETWORKING" | "ATLAS_NDS_AWS_OBJECT_STORAGE" | "ATLAS_NDS_AWS_SNAPSHOT_EXPORT_UPLOAD" | "ATLAS_NDS_AZURE_SNAPSHOT_EXPORT_UPLOAD" | "ATLAS_NDS_AZURE_SNAPSHOT_EXPORT_VM" | "ATLAS_NDS_AZURE_SNAPSHOT_EXPORT_VM_M40" | "ATLAS_NDS_AZURE_SNAPSHOT_EXPORT_VM_M50" | "ATLAS_NDS_AZURE_SNAPSHOT_EXPORT_VM_M60" | "ATLAS_NDS_AZURE_SNAPSHOT_EXPORT_VM_STORAGE_P2" | "ATLAS_NDS_AZURE_SNAPSHOT_EXPORT_VM_STORAGE_P3" | "ATLAS_NDS_AZURE_SNAPSHOT_EXPORT_VM_STORAGE_P4" | "ATLAS_NDS_AZURE_SNAPSHOT_EXPORT_VM_STORAGE_P6" | "ATLAS_NDS_AZURE_SNAPSHOT_EXPORT_VM_STORAGE_P10" | "ATLAS_NDS_AZURE_SNAPSHOT_EXPORT_VM_STORAGE_P15" | "ATLAS_NDS_AZURE_SNAPSHOT_EXPORT_VM_STORAGE_P20" | "ATLAS_NDS_AZURE_SNAPSHOT_EXPORT_VM_STORAGE_P30" | "ATLAS_NDS_AZURE_SNAPSHOT_EXPORT_VM_STORAGE_P40" | "ATLAS_NDS_AZURE_SNAPSHOT_EXPORT_VM_STORAGE_P50" | "ATLAS_NDS_AWS_SNAPSHOT_EXPORT_VM" | "ATLAS_NDS_AWS_SNAPSHOT_EXPORT_VM_M40" | "ATLAS_NDS_AWS_SNAPSHOT_EXPORT_VM_M50" | "ATLAS_NDS_AWS_SNAPSHOT_EXPORT_VM_M60" | "ATLAS_NDS_AWS_SNAPSHOT_EXPORT_VM_STORAGE" | "ATLAS_NDS_AWS_SNAPSHOT_EXPORT_VM_STORAGE_IOPS" | "ATLAS_NDS_GCP_SNAPSHOT_EXPORT_UPLOAD" | "ATLAS_NDS_GCP_SNAPSHOT_EXPORT_VM" | "ATLAS_NDS_GCP_SNAPSHOT_EXPORT_VM_M40" | "ATLAS_NDS_GCP_SNAPSHOT_EXPORT_VM_M50" | "ATLAS_NDS_GCP_SNAPSHOT_EXPORT_VM_M60" | "ATLAS_NDS_GCP_SNAPSHOT_EXPORT_VM_STORAGE" | "ATLAS_NDS_AWS_SERVERLESS_RPU" | "ATLAS_NDS_AWS_SERVERLESS_WPU" | "ATLAS_NDS_AWS_SERVERLESS_STORAGE" | "ATLAS_NDS_AWS_SERVERLESS_CONTINUOUS_BACKUP" | "ATLAS_NDS_AWS_SERVERLESS_BACKUP_RESTORE_VM" | "ATLAS_NDS_AWS_SERVERLESS_DATA_TRANSFER_PREVIEW" | "ATLAS_NDS_AWS_SERVERLESS_DATA_TRANSFER" | "ATLAS_NDS_AWS_SERVERLESS_DATA_TRANSFER_REGIONAL" | "ATLAS_NDS_AWS_SERVERLESS_DATA_TRANSFER_CROSS_REGION" | "ATLAS_NDS_AWS_SERVERLESS_DATA_TRANSFER_INTERNET" | "ATLAS_NDS_GCP_SERVERLESS_RPU" | "ATLAS_NDS_GCP_SERVERLESS_WPU" | "ATLAS_NDS_GCP_SERVERLESS_STORAGE" | "ATLAS_NDS_GCP_SERVERLESS_CONTINUOUS_BACKUP" | "ATLAS_NDS_GCP_SERVERLESS_BACKUP_RESTORE_VM" | "ATLAS_NDS_GCP_SERVERLESS_DATA_TRANSFER_PREVIEW" | "ATLAS_NDS_GCP_SERVERLESS_DATA_TRANSFER" | "ATLAS_NDS_GCP_SERVERLESS_DATA_TRANSFER_REGIONAL" | "ATLAS_NDS_GCP_SERVERLESS_DATA_TRANSFER_CROSS_REGION" | "ATLAS_NDS_GCP_SERVERLESS_DATA_TRANSFER_INTERNET" | "ATLAS_NDS_AZURE_SERVERLESS_RPU" | "ATLAS_NDS_AZURE_SERVERLESS_WPU" | "ATLAS_NDS_AZURE_SERVERLESS_STORAGE" | "ATLAS_NDS_AZURE_SERVERLESS_CONTINUOUS_BACKUP" | "ATLAS_NDS_AZURE_SERVERLESS_BACKUP_RESTORE_VM" | "ATLAS_NDS_AZURE_SERVERLESS_DATA_TRANSFER_PREVIEW" | "ATLAS_NDS_AZURE_SERVERLESS_DATA_TRANSFER" | "ATLAS_NDS_AZURE_SERVERLESS_DATA_TRANSFER_REGIONAL" | "ATLAS_NDS_AZURE_SERVERLESS_DATA_TRANSFER_CROSS_REGION" | "ATLAS_NDS_AZURE_SERVERLESS_DATA_TRANSFER_INTERNET" | "REALM_APP_REQUESTS_FREE_TIER" | "REALM_APP_REQUESTS" | "REALM_APP_COMPUTE_FREE_TIER" | "REALM_APP_COMPUTE" | "REALM_APP_SYNC_FREE_TIER" | "REALM_APP_SYNC" | "REALM_APP_DATA_TRANSFER_FREE_TIER" | "REALM_APP_DATA_TRANSFER" | "GCP_SNAPSHOT_COPY_DISK" | "ATLAS_AWS_STREAM_PROCESSING_INSTANCE_SP2" | "ATLAS_AWS_STREAM_PROCESSING_INSTANCE_SP5" | "ATLAS_AWS_STREAM_PROCESSING_INSTANCE_SP10" | "ATLAS_AWS_STREAM_PROCESSING_INSTANCE_SP30" | "ATLAS_AWS_STREAM_PROCESSING_INSTANCE_SP50" | "ATLAS_AZURE_STREAM_PROCESSING_INSTANCE_SP2" | "ATLAS_AZURE_STREAM_PROCESSING_INSTANCE_SP5" | "ATLAS_AZURE_STREAM_PROCESSING_INSTANCE_SP10" | "ATLAS_AZURE_STREAM_PROCESSING_INSTANCE_SP30" | "ATLAS_AZURE_STREAM_PROCESSING_INSTANCE_SP50" | "ATLAS_AWS_STREAM_PROCESSING_DATA_TRANSFER" | "ATLAS_AWS_STREAM_PROCESSING_DATA_TRANSFER_SAME_REGION" | "ATLAS_AWS_STREAM_PROCESSING_DATA_TRANSFER_DIFFERENT_REGION" | "ATLAS_AWS_STREAM_PROCESSING_DATA_TRANSFER_INTERNET" | "ATLAS_AZURE_STREAM_PROCESSING_DATA_TRANSFER" | "ATLAS_AZURE_STREAM_PROCESSING_DATA_TRANSFER_INTERNET" | "ATLAS_AZURE_STREAM_PROCESSING_DATA_TRANSFER_SAME_CONTINENT" | "ATLAS_AZURE_STREAM_PROCESSING_DATA_TRANSFER_DIFFERENT_CONTINENT" | "ATLAS_AWS_STREAM_PROCESSING_VPC_PEERING" | "ATLAS_AZURE_STREAM_PROCESSING_PRIVATELINK" | "ATLAS_AWS_STREAM_PROCESSING_PRIVATELINK" | "ATLAS_FLEX_AWS_100_USAGE_HOURS" | "ATLAS_FLEX_AWS_200_USAGE_HOURS" | "ATLAS_FLEX_AWS_300_USAGE_HOURS" | "ATLAS_FLEX_AWS_400_USAGE_HOURS" | "ATLAS_FLEX_AWS_500_USAGE_HOURS" | "ATLAS_FLEX_AZURE_100_USAGE_HOURS" | "ATLAS_FLEX_AZURE_200_USAGE_HOURS" | "ATLAS_FLEX_AZURE_300_USAGE_HOURS" | "ATLAS_FLEX_AZURE_400_USAGE_HOURS" | "ATLAS_FLEX_AZURE_500_USAGE_HOURS" | "ATLAS_FLEX_GCP_100_USAGE_HOURS" | "ATLAS_FLEX_GCP_200_USAGE_HOURS" | "ATLAS_FLEX_GCP_300_USAGE_HOURS" | "ATLAS_FLEX_GCP_400_USAGE_HOURS" | "ATLAS_FLEX_GCP_500_USAGE_HOURS" | "ATLAS_FLEX_AWS_LEGACY_100_USAGE_HOURS" | "ATLAS_FLEX_AWS_LEGACY_200_USAGE_HOURS" | "ATLAS_FLEX_AWS_LEGACY_300_USAGE_HOURS" | "ATLAS_FLEX_AWS_LEGACY_400_USAGE_HOURS" | "ATLAS_FLEX_AWS_LEGACY_500_USAGE_HOURS" | "ATLAS_FLEX_AZURE_LEGACY_100_USAGE_HOURS" | "ATLAS_FLEX_AZURE_LEGACY_200_USAGE_HOURS" | "ATLAS_FLEX_AZURE_LEGACY_300_USAGE_HOURS" | "ATLAS_FLEX_AZURE_LEGACY_400_USAGE_HOURS" | "ATLAS_FLEX_AZURE_LEGACY_500_USAGE_HOURS" | "ATLAS_FLEX_GCP_LEGACY_100_USAGE_HOURS" | "ATLAS_FLEX_GCP_LEGACY_200_USAGE_HOURS" | "ATLAS_FLEX_GCP_LEGACY_300_USAGE_HOURS" | "ATLAS_FLEX_GCP_LEGACY_400_USAGE_HOURS" | "ATLAS_FLEX_GCP_LEGACY_500_USAGE_HOURS" | "ATLAS_GCP_STREAM_PROCESSING_INSTANCE_SP2" | "ATLAS_GCP_STREAM_PROCESSING_INSTANCE_SP5" | "ATLAS_GCP_STREAM_PROCESSING_INSTANCE_SP10" | "ATLAS_GCP_STREAM_PROCESSING_INSTANCE_SP30" | "ATLAS_GCP_STREAM_PROCESSING_INSTANCE_SP50" | "ATLAS_GCP_STREAM_PROCESSING_DATA_TRANSFER" | "ATLAS_GCP_STREAM_PROCESSING_DATA_TRANSFER_SAME_REGION" | "ATLAS_GCP_STREAM_PROCESSING_DATA_TRANSFER_DIFFERENT_REGION" | "ATLAS_GCP_STREAM_PROCESSING_DATA_TRANSFER_INTERNET" | "ATLAS_GCP_STREAM_PROCESSING_PRIVATELINK"; + readonly sku?: "CLASSIC_BACKUP_OPLOG" | "CLASSIC_BACKUP_STORAGE" | "CLASSIC_BACKUP_SNAPSHOT_CREATE" | "CLASSIC_BACKUP_DAILY_MINIMUM" | "CLASSIC_BACKUP_FREE_TIER" | "CLASSIC_COUPON" | "BACKUP_STORAGE_FREE_TIER" | "BACKUP_STORAGE" | "FLEX_CONSULTING" | "CLOUD_MANAGER_CLASSIC" | "CLOUD_MANAGER_BASIC_FREE_TIER" | "CLOUD_MANAGER_BASIC" | "CLOUD_MANAGER_PREMIUM" | "CLOUD_MANAGER_FREE_TIER" | "CLOUD_MANAGER_STANDARD_FREE_TIER" | "CLOUD_MANAGER_STANDARD_ANNUAL" | "CLOUD_MANAGER_STANDARD" | "CLOUD_MANAGER_FREE_TRIAL" | "ATLAS_INSTANCE_M0" | "ATLAS_INSTANCE_M2" | "ATLAS_INSTANCE_M5" | "ATLAS_AWS_INSTANCE_M10" | "ATLAS_AWS_INSTANCE_M20" | "ATLAS_AWS_INSTANCE_M30" | "ATLAS_AWS_INSTANCE_M40" | "ATLAS_AWS_INSTANCE_M50" | "ATLAS_AWS_INSTANCE_M60" | "ATLAS_AWS_INSTANCE_M80" | "ATLAS_AWS_INSTANCE_M100" | "ATLAS_AWS_INSTANCE_M140" | "ATLAS_AWS_INSTANCE_M200" | "ATLAS_AWS_INSTANCE_M300" | "ATLAS_AWS_INSTANCE_M40_LOW_CPU" | "ATLAS_AWS_INSTANCE_M50_LOW_CPU" | "ATLAS_AWS_INSTANCE_M60_LOW_CPU" | "ATLAS_AWS_INSTANCE_M80_LOW_CPU" | "ATLAS_AWS_INSTANCE_M200_LOW_CPU" | "ATLAS_AWS_INSTANCE_M300_LOW_CPU" | "ATLAS_AWS_INSTANCE_M400_LOW_CPU" | "ATLAS_AWS_INSTANCE_M700_LOW_CPU" | "ATLAS_AWS_INSTANCE_M40_NVME" | "ATLAS_AWS_INSTANCE_M50_NVME" | "ATLAS_AWS_INSTANCE_M60_NVME" | "ATLAS_AWS_INSTANCE_M80_NVME" | "ATLAS_AWS_INSTANCE_M200_NVME" | "ATLAS_AWS_INSTANCE_M400_NVME" | "ATLAS_AWS_INSTANCE_M10_PAUSED" | "ATLAS_AWS_INSTANCE_M20_PAUSED" | "ATLAS_AWS_INSTANCE_M30_PAUSED" | "ATLAS_AWS_INSTANCE_M40_PAUSED" | "ATLAS_AWS_INSTANCE_M50_PAUSED" | "ATLAS_AWS_INSTANCE_M60_PAUSED" | "ATLAS_AWS_INSTANCE_M80_PAUSED" | "ATLAS_AWS_INSTANCE_M100_PAUSED" | "ATLAS_AWS_INSTANCE_M140_PAUSED" | "ATLAS_AWS_INSTANCE_M200_PAUSED" | "ATLAS_AWS_INSTANCE_M300_PAUSED" | "ATLAS_AWS_INSTANCE_M40_LOW_CPU_PAUSED" | "ATLAS_AWS_INSTANCE_M50_LOW_CPU_PAUSED" | "ATLAS_AWS_INSTANCE_M60_LOW_CPU_PAUSED" | "ATLAS_AWS_INSTANCE_M80_LOW_CPU_PAUSED" | "ATLAS_AWS_INSTANCE_M200_LOW_CPU_PAUSED" | "ATLAS_AWS_INSTANCE_M300_LOW_CPU_PAUSED" | "ATLAS_AWS_INSTANCE_M400_LOW_CPU_PAUSED" | "ATLAS_AWS_INSTANCE_M700_LOW_CPU_PAUSED" | "ATLAS_AWS_SEARCH_INSTANCE_S20_COMPUTE_NVME" | "ATLAS_AWS_SEARCH_INSTANCE_S30_COMPUTE_NVME" | "ATLAS_AWS_SEARCH_INSTANCE_S40_COMPUTE_NVME" | "ATLAS_AWS_SEARCH_INSTANCE_S50_COMPUTE_NVME" | "ATLAS_AWS_SEARCH_INSTANCE_S60_COMPUTE_NVME" | "ATLAS_AWS_SEARCH_INSTANCE_S70_COMPUTE_NVME" | "ATLAS_AWS_SEARCH_INSTANCE_S80_COMPUTE_NVME" | "ATLAS_AWS_SEARCH_INSTANCE_S30_MEMORY_NVME" | "ATLAS_AWS_SEARCH_INSTANCE_S40_MEMORY_NVME" | "ATLAS_AWS_SEARCH_INSTANCE_S50_MEMORY_NVME" | "ATLAS_AWS_SEARCH_INSTANCE_S60_MEMORY_NVME" | "ATLAS_AWS_SEARCH_INSTANCE_S80_MEMORY_NVME" | "ATLAS_AWS_SEARCH_INSTANCE_S90_MEMORY_NVME" | "ATLAS_AWS_SEARCH_INSTANCE_S100_MEMORY_NVME" | "ATLAS_AWS_SEARCH_INSTANCE_S110_MEMORY_NVME" | "ATLAS_AWS_SEARCH_INSTANCE_S40_STORAGE_NVME" | "ATLAS_AWS_SEARCH_INSTANCE_S50_STORAGE_NVME" | "ATLAS_AWS_SEARCH_INSTANCE_S60_STORAGE_NVME" | "ATLAS_AWS_SEARCH_INSTANCE_S80_STORAGE_NVME" | "ATLAS_AWS_SEARCH_INSTANCE_S90_STORAGE_NVME" | "ATLAS_AWS_STORAGE_PROVISIONED" | "ATLAS_AWS_STORAGE_STANDARD" | "ATLAS_AWS_STORAGE_STANDARD_GP3" | "ATLAS_AWS_STORAGE_IOPS" | "ATLAS_AWS_DATA_TRANSFER_SAME_REGION" | "ATLAS_AWS_DATA_TRANSFER_DIFFERENT_REGION" | "ATLAS_AWS_DATA_TRANSFER_INTERNET" | "ATLAS_AWS_TIME_BASED_SNAPSHOT_COPY_LEVEL_1" | "ATLAS_AWS_TIME_BASED_SNAPSHOT_COPY_LEVEL_2" | "ATLAS_AWS_TIME_BASED_SNAPSHOT_COPY_LEVEL_3" | "ATLAS_AWS_TIME_BASED_SNAPSHOT_COPY_LEVEL_4" | "ATLAS_AWS_TIME_BASED_SNAPSHOT_COPY_LEVEL_5" | "ATLAS_AWS_TIME_BASED_SNAPSHOT_COPY_LEVEL_6" | "ATLAS_AWS_TIME_BASED_SNAPSHOT_COPY_LEVEL_7" | "ATLAS_AWS_BACKUP_SNAPSHOT_STORAGE" | "ATLAS_AWS_BACKUP_DOWNLOAD_VM" | "ATLAS_AWS_BACKUP_DOWNLOAD_VM_STORAGE" | "ATLAS_AWS_BACKUP_DOWNLOAD_VM_STORAGE_IOPS" | "ATLAS_AWS_PRIVATE_ENDPOINT" | "ATLAS_AWS_PRIVATE_ENDPOINT_CAPACITY_UNITS" | "ATLAS_GCP_SEARCH_INSTANCE_S20_COMPUTE_LOCALSSD" | "ATLAS_GCP_SEARCH_INSTANCE_S30_COMPUTE_LOCALSSD" | "ATLAS_GCP_SEARCH_INSTANCE_S40_COMPUTE_LOCALSSD" | "ATLAS_GCP_SEARCH_INSTANCE_S50_COMPUTE_LOCALSSD" | "ATLAS_GCP_SEARCH_INSTANCE_S60_COMPUTE_LOCALSSD" | "ATLAS_GCP_SEARCH_INSTANCE_S70_COMPUTE_LOCALSSD" | "ATLAS_GCP_SEARCH_INSTANCE_S80_COMPUTE_LOCALSSD" | "ATLAS_GCP_SEARCH_INSTANCE_S30_MEMORY_LOCALSSD" | "ATLAS_GCP_SEARCH_INSTANCE_S40_MEMORY_LOCALSSD" | "ATLAS_GCP_SEARCH_INSTANCE_S50_MEMORY_LOCALSSD" | "ATLAS_GCP_SEARCH_INSTANCE_S60_MEMORY_LOCALSSD" | "ATLAS_GCP_SEARCH_INSTANCE_S70_MEMORY_LOCALSSD" | "ATLAS_GCP_SEARCH_INSTANCE_S80_MEMORY_LOCALSSD" | "ATLAS_GCP_SEARCH_INSTANCE_S90_MEMORY_LOCALSSD" | "ATLAS_GCP_SEARCH_INSTANCE_S100_MEMORY_LOCALSSD" | "ATLAS_GCP_SEARCH_INSTANCE_S110_MEMORY_LOCALSSD" | "ATLAS_GCP_SEARCH_INSTANCE_S120_MEMORY_LOCALSSD" | "ATLAS_GCP_SEARCH_INSTANCE_S130_MEMORY_LOCALSSD" | "ATLAS_GCP_SEARCH_INSTANCE_S140_MEMORY_LOCALSSD" | "ATLAS_GCP_INSTANCE_M10" | "ATLAS_GCP_INSTANCE_M20" | "ATLAS_GCP_INSTANCE_M30" | "ATLAS_GCP_INSTANCE_M40" | "ATLAS_GCP_INSTANCE_M50" | "ATLAS_GCP_INSTANCE_M60" | "ATLAS_GCP_INSTANCE_M80" | "ATLAS_GCP_INSTANCE_M140" | "ATLAS_GCP_INSTANCE_M200" | "ATLAS_GCP_INSTANCE_M250" | "ATLAS_GCP_INSTANCE_M300" | "ATLAS_GCP_INSTANCE_M400" | "ATLAS_GCP_INSTANCE_M40_LOW_CPU" | "ATLAS_GCP_INSTANCE_M50_LOW_CPU" | "ATLAS_GCP_INSTANCE_M60_LOW_CPU" | "ATLAS_GCP_INSTANCE_M80_LOW_CPU" | "ATLAS_GCP_INSTANCE_M200_LOW_CPU" | "ATLAS_GCP_INSTANCE_M300_LOW_CPU" | "ATLAS_GCP_INSTANCE_M400_LOW_CPU" | "ATLAS_GCP_INSTANCE_M600_LOW_CPU" | "ATLAS_GCP_INSTANCE_M10_PAUSED" | "ATLAS_GCP_INSTANCE_M20_PAUSED" | "ATLAS_GCP_INSTANCE_M30_PAUSED" | "ATLAS_GCP_INSTANCE_M40_PAUSED" | "ATLAS_GCP_INSTANCE_M50_PAUSED" | "ATLAS_GCP_INSTANCE_M60_PAUSED" | "ATLAS_GCP_INSTANCE_M80_PAUSED" | "ATLAS_GCP_INSTANCE_M140_PAUSED" | "ATLAS_GCP_INSTANCE_M200_PAUSED" | "ATLAS_GCP_INSTANCE_M250_PAUSED" | "ATLAS_GCP_INSTANCE_M300_PAUSED" | "ATLAS_GCP_INSTANCE_M400_PAUSED" | "ATLAS_GCP_INSTANCE_M40_LOW_CPU_PAUSED" | "ATLAS_GCP_INSTANCE_M50_LOW_CPU_PAUSED" | "ATLAS_GCP_INSTANCE_M60_LOW_CPU_PAUSED" | "ATLAS_GCP_INSTANCE_M80_LOW_CPU_PAUSED" | "ATLAS_GCP_INSTANCE_M200_LOW_CPU_PAUSED" | "ATLAS_GCP_INSTANCE_M300_LOW_CPU_PAUSED" | "ATLAS_GCP_INSTANCE_M400_LOW_CPU_PAUSED" | "ATLAS_GCP_INSTANCE_M600_LOW_CPU_PAUSED" | "ATLAS_GCP_DATA_TRANSFER_INTERNET" | "ATLAS_GCP_STORAGE_SSD" | "ATLAS_GCP_DATA_TRANSFER_INTER_CONNECT" | "ATLAS_GCP_DATA_TRANSFER_INTER_ZONE" | "ATLAS_GCP_DATA_TRANSFER_INTER_REGION" | "ATLAS_GCP_DATA_TRANSFER_GOOGLE" | "ATLAS_GCP_BACKUP_SNAPSHOT_STORAGE" | "ATLAS_GCP_BACKUP_DOWNLOAD_VM" | "ATLAS_GCP_BACKUP_DOWNLOAD_VM_STORAGE" | "ATLAS_GCP_PRIVATE_ENDPOINT" | "ATLAS_GCP_PRIVATE_ENDPOINT_CAPACITY_UNITS" | "ATLAS_GCP_SNAPSHOT_COPY_DATA_TRANSFER" | "ATLAS_AZURE_INSTANCE_M10" | "ATLAS_AZURE_INSTANCE_M20" | "ATLAS_AZURE_INSTANCE_M30" | "ATLAS_AZURE_INSTANCE_M40" | "ATLAS_AZURE_INSTANCE_M50" | "ATLAS_AZURE_INSTANCE_M60" | "ATLAS_AZURE_INSTANCE_M80" | "ATLAS_AZURE_INSTANCE_M90" | "ATLAS_AZURE_INSTANCE_M200" | "ATLAS_AZURE_INSTANCE_R40" | "ATLAS_AZURE_INSTANCE_R50" | "ATLAS_AZURE_INSTANCE_R60" | "ATLAS_AZURE_INSTANCE_R80" | "ATLAS_AZURE_INSTANCE_R200" | "ATLAS_AZURE_INSTANCE_R300" | "ATLAS_AZURE_INSTANCE_R400" | "ATLAS_AZURE_INSTANCE_M60_NVME" | "ATLAS_AZURE_INSTANCE_M80_NVME" | "ATLAS_AZURE_INSTANCE_M200_NVME" | "ATLAS_AZURE_INSTANCE_M300_NVME" | "ATLAS_AZURE_INSTANCE_M400_NVME" | "ATLAS_AZURE_INSTANCE_M600_NVME" | "ATLAS_AZURE_INSTANCE_M10_PAUSED" | "ATLAS_AZURE_INSTANCE_M20_PAUSED" | "ATLAS_AZURE_INSTANCE_M30_PAUSED" | "ATLAS_AZURE_INSTANCE_M40_PAUSED" | "ATLAS_AZURE_INSTANCE_M50_PAUSED" | "ATLAS_AZURE_INSTANCE_M60_PAUSED" | "ATLAS_AZURE_INSTANCE_M80_PAUSED" | "ATLAS_AZURE_INSTANCE_M90_PAUSED" | "ATLAS_AZURE_INSTANCE_M200_PAUSED" | "ATLAS_AZURE_INSTANCE_R40_PAUSED" | "ATLAS_AZURE_INSTANCE_R50_PAUSED" | "ATLAS_AZURE_INSTANCE_R60_PAUSED" | "ATLAS_AZURE_INSTANCE_R80_PAUSED" | "ATLAS_AZURE_INSTANCE_R200_PAUSED" | "ATLAS_AZURE_INSTANCE_R300_PAUSED" | "ATLAS_AZURE_INSTANCE_R400_PAUSED" | "ATLAS_AZURE_SEARCH_INSTANCE_S20_COMPUTE_LOCALSSD" | "ATLAS_AZURE_SEARCH_INSTANCE_S30_COMPUTE_LOCALSSD" | "ATLAS_AZURE_SEARCH_INSTANCE_S40_COMPUTE_LOCALSSD" | "ATLAS_AZURE_SEARCH_INSTANCE_S50_COMPUTE_LOCALSSD" | "ATLAS_AZURE_SEARCH_INSTANCE_S60_COMPUTE_LOCALSSD" | "ATLAS_AZURE_SEARCH_INSTANCE_S70_COMPUTE_LOCALSSD" | "ATLAS_AZURE_SEARCH_INSTANCE_S80_COMPUTE_LOCALSSD" | "ATLAS_AZURE_SEARCH_INSTANCE_S40_MEMORY_LOCALSSD" | "ATLAS_AZURE_SEARCH_INSTANCE_S50_MEMORY_LOCALSSD" | "ATLAS_AZURE_SEARCH_INSTANCE_S60_MEMORY_LOCALSSD" | "ATLAS_AZURE_SEARCH_INSTANCE_S80_MEMORY_LOCALSSD" | "ATLAS_AZURE_SEARCH_INSTANCE_S90_MEMORY_LOCALSSD" | "ATLAS_AZURE_SEARCH_INSTANCE_S100_MEMORY_LOCALSSD" | "ATLAS_AZURE_SEARCH_INSTANCE_S110_MEMORY_LOCALSSD" | "ATLAS_AZURE_SEARCH_INSTANCE_S130_MEMORY_LOCALSSD" | "ATLAS_AZURE_SEARCH_INSTANCE_S135_MEMORY_LOCALSSD" | "ATLAS_AZURE_STORAGE_P2" | "ATLAS_AZURE_STORAGE_P3" | "ATLAS_AZURE_STORAGE_P4" | "ATLAS_AZURE_STORAGE_P6" | "ATLAS_AZURE_STORAGE_P10" | "ATLAS_AZURE_STORAGE_P15" | "ATLAS_AZURE_STORAGE_P20" | "ATLAS_AZURE_STORAGE_P30" | "ATLAS_AZURE_STORAGE_P40" | "ATLAS_AZURE_STORAGE_P50" | "ATLAS_AZURE_DATA_TRANSFER" | "ATLAS_AZURE_DATA_TRANSFER_REGIONAL_VNET_IN" | "ATLAS_AZURE_DATA_TRANSFER_REGIONAL_VNET_OUT" | "ATLAS_AZURE_DATA_TRANSFER_GLOBAL_VNET_IN" | "ATLAS_AZURE_DATA_TRANSFER_GLOBAL_VNET_OUT" | "ATLAS_AZURE_DATA_TRANSFER_AVAILABILITY_ZONE_IN" | "ATLAS_AZURE_DATA_TRANSFER_AVAILABILITY_ZONE_OUT" | "ATLAS_AZURE_DATA_TRANSFER_INTER_REGION_INTRA_CONTINENT" | "ATLAS_AZURE_DATA_TRANSFER_INTER_REGION_INTER_CONTINENT" | "ATLAS_AZURE_BACKUP_SNAPSHOT_STORAGE" | "ATLAS_AZURE_BACKUP_DOWNLOAD_VM" | "ATLAS_AZURE_BACKUP_DOWNLOAD_VM_STORAGE_P2" | "ATLAS_AZURE_BACKUP_DOWNLOAD_VM_STORAGE_P3" | "ATLAS_AZURE_BACKUP_DOWNLOAD_VM_STORAGE_P4" | "ATLAS_AZURE_BACKUP_DOWNLOAD_VM_STORAGE_P6" | "ATLAS_AZURE_BACKUP_DOWNLOAD_VM_STORAGE_P10" | "ATLAS_AZURE_BACKUP_DOWNLOAD_VM_STORAGE_P15" | "ATLAS_AZURE_BACKUP_DOWNLOAD_VM_STORAGE_P20" | "ATLAS_AZURE_BACKUP_DOWNLOAD_VM_STORAGE_P30" | "ATLAS_AZURE_BACKUP_DOWNLOAD_VM_STORAGE_P40" | "ATLAS_AZURE_BACKUP_DOWNLOAD_VM_STORAGE_P50" | "ATLAS_AZURE_STANDARD_STORAGE" | "ATLAS_AZURE_EXTENDED_STANDARD_IOPS" | "ATLAS_AZURE_BACKUP_DOWNLOAD_VM_STORAGE" | "ATLAS_AZURE_BACKUP_DOWNLOAD_VM_STORAGE_EXTENDED_IOPS" | "ATLAS_AZURE_SNAPSHOT_EXPORT_VM_STORAGE" | "ATLAS_AZURE_SNAPSHOT_EXPORT_VM_STORAGE_EXTENDED_IOPS" | "ATLAS_BI_CONNECTOR" | "ATLAS_ADVANCED_SECURITY" | "ATLAS_ENTERPRISE_AUDITING" | "ATLAS_FREE_SUPPORT" | "ATLAS_SUPPORT" | "ATLAS_NDS_BACKFILL_SUPPORT" | "STITCH_DATA_DOWNLOADED_FREE_TIER" | "STITCH_DATA_DOWNLOADED" | "STITCH_COMPUTE_FREE_TIER" | "STITCH_COMPUTE" | "CREDIT" | "MINIMUM_CHARGE" | "CHARTS_DATA_DOWNLOADED_FREE_TIER" | "CHARTS_DATA_DOWNLOADED" | "ATLAS_DATA_LAKE_AWS_DATA_RETURNED_SAME_REGION" | "ATLAS_DATA_LAKE_AWS_DATA_RETURNED_DIFFERENT_REGION" | "ATLAS_DATA_LAKE_AWS_DATA_RETURNED_INTERNET" | "ATLAS_DATA_LAKE_AWS_DATA_SCANNED" | "ATLAS_DATA_LAKE_AWS_DATA_TRANSFERRED_FROM_DIFFERENT_REGION" | "ATLAS_NDS_AWS_DATA_LAKE_STORAGE_ACCESS" | "ATLAS_NDS_AWS_DATA_LAKE_STORAGE" | "ATLAS_DATA_FEDERATION_AZURE_DATA_RETURNED_SAME_REGION" | "ATLAS_DATA_FEDERATION_AZURE_DATA_RETURNED_SAME_CONTINENT" | "ATLAS_DATA_FEDERATION_AZURE_DATA_RETURNED_DIFFERENT_CONTINENT" | "ATLAS_DATA_FEDERATION_AZURE_DATA_RETURNED_INTERNET" | "ATLAS_DATA_FEDERATION_GCP_DATA_RETURNED_SAME_REGION" | "ATLAS_DATA_FEDERATION_GCP_DATA_RETURNED_DIFFERENT_REGION" | "ATLAS_DATA_FEDERATION_GCP_DATA_RETURNED_INTERNET" | "ATLAS_DATA_FEDERATION_AZURE_DATA_SCANNED" | "ATLAS_NDS_AZURE_DATA_LAKE_STORAGE_ACCESS" | "ATLAS_NDS_AZURE_DATA_LAKE_STORAGE" | "ATLAS_DATA_FEDERATION_GCP_DATA_SCANNED" | "ATLAS_NDS_GCP_DATA_LAKE_STORAGE_ACCESS" | "ATLAS_NDS_GCP_DATA_LAKE_STORAGE" | "ATLAS_NDS_AWS_OBJECT_STORAGE_ACCESS" | "ATLAS_NDS_AWS_COMPRESSED_OBJECT_STORAGE" | "ATLAS_NDS_AZURE_OBJECT_STORAGE_ACCESS" | "ATLAS_NDS_AZURE_OBJECT_STORAGE" | "ATLAS_NDS_AZURE_COMPRESSED_OBJECT_STORAGE" | "ATLAS_NDS_GCP_OBJECT_STORAGE_ACCESS" | "ATLAS_NDS_GCP_OBJECT_STORAGE" | "ATLAS_NDS_GCP_COMPRESSED_OBJECT_STORAGE" | "ATLAS_ARCHIVE_ACCESS_PARTITION_LOCATE" | "ATLAS_NDS_AWS_PIT_RESTORE_STORAGE_FREE_TIER" | "ATLAS_NDS_AWS_PIT_RESTORE_STORAGE" | "ATLAS_NDS_GCP_PIT_RESTORE_STORAGE_FREE_TIER" | "ATLAS_NDS_GCP_PIT_RESTORE_STORAGE" | "ATLAS_NDS_AZURE_PIT_RESTORE_STORAGE_FREE_TIER" | "ATLAS_NDS_AZURE_PIT_RESTORE_STORAGE" | "ATLAS_NDS_AZURE_PRIVATE_ENDPOINT_CAPACITY_UNITS" | "ATLAS_NDS_AZURE_CMK_PRIVATE_NETWORKING" | "ATLAS_NDS_AWS_CMK_PRIVATE_NETWORKING" | "ATLAS_NDS_AWS_OBJECT_STORAGE" | "ATLAS_NDS_AWS_SNAPSHOT_EXPORT_UPLOAD" | "ATLAS_NDS_AZURE_SNAPSHOT_EXPORT_UPLOAD" | "ATLAS_NDS_AZURE_SNAPSHOT_EXPORT_VM" | "ATLAS_NDS_AZURE_SNAPSHOT_EXPORT_VM_M40" | "ATLAS_NDS_AZURE_SNAPSHOT_EXPORT_VM_M50" | "ATLAS_NDS_AZURE_SNAPSHOT_EXPORT_VM_M60" | "ATLAS_NDS_AZURE_SNAPSHOT_EXPORT_VM_STORAGE_P2" | "ATLAS_NDS_AZURE_SNAPSHOT_EXPORT_VM_STORAGE_P3" | "ATLAS_NDS_AZURE_SNAPSHOT_EXPORT_VM_STORAGE_P4" | "ATLAS_NDS_AZURE_SNAPSHOT_EXPORT_VM_STORAGE_P6" | "ATLAS_NDS_AZURE_SNAPSHOT_EXPORT_VM_STORAGE_P10" | "ATLAS_NDS_AZURE_SNAPSHOT_EXPORT_VM_STORAGE_P15" | "ATLAS_NDS_AZURE_SNAPSHOT_EXPORT_VM_STORAGE_P20" | "ATLAS_NDS_AZURE_SNAPSHOT_EXPORT_VM_STORAGE_P30" | "ATLAS_NDS_AZURE_SNAPSHOT_EXPORT_VM_STORAGE_P40" | "ATLAS_NDS_AZURE_SNAPSHOT_EXPORT_VM_STORAGE_P50" | "ATLAS_NDS_AWS_SNAPSHOT_EXPORT_VM" | "ATLAS_NDS_AWS_SNAPSHOT_EXPORT_VM_M40" | "ATLAS_NDS_AWS_SNAPSHOT_EXPORT_VM_M50" | "ATLAS_NDS_AWS_SNAPSHOT_EXPORT_VM_M60" | "ATLAS_NDS_AWS_SNAPSHOT_EXPORT_VM_STORAGE" | "ATLAS_NDS_AWS_SNAPSHOT_EXPORT_VM_STORAGE_IOPS" | "ATLAS_NDS_GCP_SNAPSHOT_EXPORT_UPLOAD" | "ATLAS_NDS_GCP_SNAPSHOT_EXPORT_VM" | "ATLAS_NDS_GCP_SNAPSHOT_EXPORT_VM_M40" | "ATLAS_NDS_GCP_SNAPSHOT_EXPORT_VM_M50" | "ATLAS_NDS_GCP_SNAPSHOT_EXPORT_VM_M60" | "ATLAS_NDS_GCP_SNAPSHOT_EXPORT_VM_STORAGE" | "ATLAS_NDS_AWS_SERVERLESS_RPU" | "ATLAS_NDS_AWS_SERVERLESS_WPU" | "ATLAS_NDS_AWS_SERVERLESS_STORAGE" | "ATLAS_NDS_AWS_SERVERLESS_CONTINUOUS_BACKUP" | "ATLAS_NDS_AWS_SERVERLESS_BACKUP_RESTORE_VM" | "ATLAS_NDS_AWS_SERVERLESS_DATA_TRANSFER_PREVIEW" | "ATLAS_NDS_AWS_SERVERLESS_DATA_TRANSFER" | "ATLAS_NDS_AWS_SERVERLESS_DATA_TRANSFER_REGIONAL" | "ATLAS_NDS_AWS_SERVERLESS_DATA_TRANSFER_CROSS_REGION" | "ATLAS_NDS_AWS_SERVERLESS_DATA_TRANSFER_INTERNET" | "ATLAS_NDS_GCP_SERVERLESS_RPU" | "ATLAS_NDS_GCP_SERVERLESS_WPU" | "ATLAS_NDS_GCP_SERVERLESS_STORAGE" | "ATLAS_NDS_GCP_SERVERLESS_CONTINUOUS_BACKUP" | "ATLAS_NDS_GCP_SERVERLESS_BACKUP_RESTORE_VM" | "ATLAS_NDS_GCP_SERVERLESS_DATA_TRANSFER_PREVIEW" | "ATLAS_NDS_GCP_SERVERLESS_DATA_TRANSFER" | "ATLAS_NDS_GCP_SERVERLESS_DATA_TRANSFER_REGIONAL" | "ATLAS_NDS_GCP_SERVERLESS_DATA_TRANSFER_CROSS_REGION" | "ATLAS_NDS_GCP_SERVERLESS_DATA_TRANSFER_INTERNET" | "ATLAS_NDS_AZURE_SERVERLESS_RPU" | "ATLAS_NDS_AZURE_SERVERLESS_WPU" | "ATLAS_NDS_AZURE_SERVERLESS_STORAGE" | "ATLAS_NDS_AZURE_SERVERLESS_CONTINUOUS_BACKUP" | "ATLAS_NDS_AZURE_SERVERLESS_BACKUP_RESTORE_VM" | "ATLAS_NDS_AZURE_SERVERLESS_DATA_TRANSFER_PREVIEW" | "ATLAS_NDS_AZURE_SERVERLESS_DATA_TRANSFER" | "ATLAS_NDS_AZURE_SERVERLESS_DATA_TRANSFER_REGIONAL" | "ATLAS_NDS_AZURE_SERVERLESS_DATA_TRANSFER_CROSS_REGION" | "ATLAS_NDS_AZURE_SERVERLESS_DATA_TRANSFER_INTERNET" | "REALM_APP_REQUESTS_FREE_TIER" | "REALM_APP_REQUESTS" | "REALM_APP_COMPUTE_FREE_TIER" | "REALM_APP_COMPUTE" | "REALM_APP_SYNC_FREE_TIER" | "REALM_APP_SYNC" | "REALM_APP_DATA_TRANSFER_FREE_TIER" | "REALM_APP_DATA_TRANSFER" | "GCP_SNAPSHOT_COPY_DISK" | "ATLAS_AWS_STREAM_PROCESSING_INSTANCE_SP2" | "ATLAS_AWS_STREAM_PROCESSING_INSTANCE_SP5" | "ATLAS_AWS_STREAM_PROCESSING_INSTANCE_SP10" | "ATLAS_AWS_STREAM_PROCESSING_INSTANCE_SP30" | "ATLAS_AWS_STREAM_PROCESSING_INSTANCE_SP50" | "ATLAS_AZURE_STREAM_PROCESSING_INSTANCE_SP2" | "ATLAS_AZURE_STREAM_PROCESSING_INSTANCE_SP5" | "ATLAS_AZURE_STREAM_PROCESSING_INSTANCE_SP10" | "ATLAS_AZURE_STREAM_PROCESSING_INSTANCE_SP30" | "ATLAS_AZURE_STREAM_PROCESSING_INSTANCE_SP50" | "ATLAS_AWS_STREAM_PROCESSING_DATA_TRANSFER" | "ATLAS_AWS_STREAM_PROCESSING_DATA_TRANSFER_SAME_REGION" | "ATLAS_AWS_STREAM_PROCESSING_DATA_TRANSFER_DIFFERENT_REGION" | "ATLAS_AWS_STREAM_PROCESSING_DATA_TRANSFER_INTERNET" | "ATLAS_AZURE_STREAM_PROCESSING_DATA_TRANSFER" | "ATLAS_AZURE_STREAM_PROCESSING_DATA_TRANSFER_INTERNET" | "ATLAS_AZURE_STREAM_PROCESSING_DATA_TRANSFER_SAME_CONTINENT" | "ATLAS_AZURE_STREAM_PROCESSING_DATA_TRANSFER_DIFFERENT_CONTINENT" | "ATLAS_AWS_STREAM_PROCESSING_VPC_PEERING" | "ATLAS_AZURE_STREAM_PROCESSING_PRIVATELINK" | "ATLAS_AWS_STREAM_PROCESSING_PRIVATELINK" | "ATLAS_FLEX_AWS_100_USAGE_HOURS" | "ATLAS_FLEX_AWS_200_USAGE_HOURS" | "ATLAS_FLEX_AWS_300_USAGE_HOURS" | "ATLAS_FLEX_AWS_400_USAGE_HOURS" | "ATLAS_FLEX_AWS_500_USAGE_HOURS" | "ATLAS_FLEX_AZURE_100_USAGE_HOURS" | "ATLAS_FLEX_AZURE_200_USAGE_HOURS" | "ATLAS_FLEX_AZURE_300_USAGE_HOURS" | "ATLAS_FLEX_AZURE_400_USAGE_HOURS" | "ATLAS_FLEX_AZURE_500_USAGE_HOURS" | "ATLAS_FLEX_GCP_100_USAGE_HOURS" | "ATLAS_FLEX_GCP_200_USAGE_HOURS" | "ATLAS_FLEX_GCP_300_USAGE_HOURS" | "ATLAS_FLEX_GCP_400_USAGE_HOURS" | "ATLAS_FLEX_GCP_500_USAGE_HOURS" | "ATLAS_FLEX_AWS_LEGACY_100_USAGE_HOURS" | "ATLAS_FLEX_AWS_LEGACY_200_USAGE_HOURS" | "ATLAS_FLEX_AWS_LEGACY_300_USAGE_HOURS" | "ATLAS_FLEX_AWS_LEGACY_400_USAGE_HOURS" | "ATLAS_FLEX_AWS_LEGACY_500_USAGE_HOURS" | "ATLAS_FLEX_AZURE_LEGACY_100_USAGE_HOURS" | "ATLAS_FLEX_AZURE_LEGACY_200_USAGE_HOURS" | "ATLAS_FLEX_AZURE_LEGACY_300_USAGE_HOURS" | "ATLAS_FLEX_AZURE_LEGACY_400_USAGE_HOURS" | "ATLAS_FLEX_AZURE_LEGACY_500_USAGE_HOURS" | "ATLAS_FLEX_GCP_LEGACY_100_USAGE_HOURS" | "ATLAS_FLEX_GCP_LEGACY_200_USAGE_HOURS" | "ATLAS_FLEX_GCP_LEGACY_300_USAGE_HOURS" | "ATLAS_FLEX_GCP_LEGACY_400_USAGE_HOURS" | "ATLAS_FLEX_GCP_LEGACY_500_USAGE_HOURS" | "ATLAS_GCP_STREAM_PROCESSING_INSTANCE_SP2" | "ATLAS_GCP_STREAM_PROCESSING_INSTANCE_SP5" | "ATLAS_GCP_STREAM_PROCESSING_INSTANCE_SP10" | "ATLAS_GCP_STREAM_PROCESSING_INSTANCE_SP30" | "ATLAS_GCP_STREAM_PROCESSING_INSTANCE_SP50" | "ATLAS_GCP_STREAM_PROCESSING_DATA_TRANSFER" | "ATLAS_GCP_STREAM_PROCESSING_DATA_TRANSFER_SAME_REGION" | "ATLAS_GCP_STREAM_PROCESSING_DATA_TRANSFER_DIFFERENT_REGION" | "ATLAS_GCP_STREAM_PROCESSING_DATA_TRANSFER_INTERNET" | "ATLAS_GCP_STREAM_PROCESSING_PRIVATELINK" | "VOYAGE_EMBEDDING_3_TEXT_LARGE_API" | "VOYAGE_EMBEDDING_3_TEXT_API" | "VOYAGE_EMBEDDING_3_TEXT_LITE_API" | "VOYAGE_EMBEDDING_3_5_TEXT_API" | "VOYAGE_EMBEDDING_3_5_TEXT_LITE_API" | "VOYAGE_EMBEDDING_3_TEXT_CONTEXT_API" | "VOYAGE_EMBEDDING_3_CODE_API" | "VOYAGE_EMBEDDING_2_FINANCE_API" | "VOYAGE_EMBEDDING_2_LAW_API" | "VOYAGE_EMBEDDING_2_CODE_API" | "VOYAGE_EMBEDDING_3_5_MULTIMODAL_TOKENS_API" | "VOYAGE_EMBEDDING_3_5_MULTIMODAL_PIXELS_API" | "VOYAGE_EMBEDDING_3_MULTIMODAL_TOKENS_API" | "VOYAGE_EMBEDDING_3_MULTIMODAL_PIXELS_API" | "VOYAGE_RERANK_2_5_LITE_API" | "VOYAGE_RERANK_2_5_API" | "VOYAGE_RERANK_2_LITE_API" | "VOYAGE_RERANK_2_API" | "VOYAGE_EMBEDDING_3_TEXT_LARGE_AUTOEMBED" | "VOYAGE_EMBEDDING_3_TEXT_AUTOEMBED" | "VOYAGE_EMBEDDING_3_TEXT_LITE_AUTOEMBED" | "VOYAGE_EMBEDDING_3_5_TEXT_AUTOEMBED" | "VOYAGE_EMBEDDING_3_5_TEXT_LITE_AUTOEMBED" | "VOYAGE_EMBEDDING_3_TEXT_CONTEXT_AUTOEMBED" | "VOYAGE_EMBEDDING_3_CODE_AUTOEMBED" | "VOYAGE_EMBEDDING_2_FINANCE_AUTOEMBED" | "VOYAGE_EMBEDDING_2_LAW_AUTOEMBED" | "VOYAGE_EMBEDDING_2_CODE_AUTOEMBED" | "VOYAGE_EMBEDDING_3_5_MULTIMODAL_TOKENS_AUTOEMBED" | "VOYAGE_EMBEDDING_3_5_MULTIMODAL_PIXELS_AUTOEMBED" | "VOYAGE_EMBEDDING_3_MULTIMODAL_TOKENS_AUTOEMBED" | "VOYAGE_EMBEDDING_3_MULTIMODAL_PIXELS_AUTOEMBED"; /** * Format: date-time * @description Date and time when MongoDB Cloud began charging for this line item. This parameter expresses its value in the ISO 8601 timestamp format in UTC. @@ -5431,6 +5497,10 @@ export interface components { */ type?: "Kafka" | "Cluster" | "Sample" | "Https" | "AWSLambda" | "AWSKinesisDataStreams"; } & (components["schemas"]["StreamsSampleConnection"] | components["schemas"]["StreamsClusterConnection"] | components["schemas"]["StreamsKafkaConnection"] | components["schemas"]["StreamsHttpsConnection"] | components["schemas"]["StreamsAWSLambdaConnection"] | components["schemas"]["StreamsS3Connection"] | components["schemas"]["StreamsAWSKinesisDataStreamsConnection"]); + /** @description Settings that define a connection to an external data store. */ + StreamsConnectionPrivatePreview: { + type: string; + } & components["schemas"]["StreamsSchemaRegistryConnectionPrivatePreview"]; StreamsHttpsConnection: Omit & { /** @description A map of key-value pairs that will be passed as headers for the request. */ headers?: { @@ -5583,6 +5653,23 @@ export interface components { */ type: "Sample"; }; + /** @description The configuration for Schema Registry connections. */ + StreamsSchemaRegistryConnectionPrivatePreview: { + /** @description List of one or more Uniform Resource Locators (URLs) that point to API sub-resources, related API resources, or both. RFC 5988 outlines these relationships. */ + readonly links?: components["schemas"]["Link"][]; + /** @description Human-readable label that identifies the stream connection. In the case of the Sample type, this is the name of the sample source. */ + name?: string; + /** + * @description The Schema Registry provider. + * @enum {string} + */ + provider: "CONFLUENT"; + /** + * @description Type of the connection. (enum property replaced by openapi-typescript) + * @enum {string} + */ + type: "SchemaRegistry"; + } & components["schemas"]["ConfluentSchemaRegistryConnectionPrivatePreview"]; /** * Synonym Mapping Status Detail * @description Contains the status of the index's synonym mappings on each search host. This field (and its subfields) only appear if the index has synonyms defined. @@ -6858,6 +6945,10 @@ export type ClusterFreeProviderSettings = components['schemas']['ClusterFreeProv export type ClusterProviderSettings = components['schemas']['ClusterProviderSettings']; export type ClusterSearchIndex = components['schemas']['ClusterSearchIndex']; export type ComponentLabel = components['schemas']['ComponentLabel']; +export type ConfluentSaslInheritAuthentication = components['schemas']['ConfluentSaslInheritAuthentication']; +export type ConfluentSchemaRegistryAuthentication = components['schemas']['ConfluentSchemaRegistryAuthentication']; +export type ConfluentSchemaRegistryConnectionPrivatePreview = components['schemas']['ConfluentSchemaRegistryConnectionPrivatePreview']; +export type ConfluentUserInfoAuthentication = components['schemas']['ConfluentUserInfoAuthentication']; export type CreateAwsEndpointRequest = components['schemas']['CreateAWSEndpointRequest']; export type CreateAzureEndpointRequest = components['schemas']['CreateAzureEndpointRequest']; export type CreateDataProcessRegionView = components['schemas']['CreateDataProcessRegionView']; @@ -6997,6 +7088,7 @@ export type StreamsAwsKinesisDataStreamsConnection = components['schemas']['Stre export type StreamsAwsLambdaConnection = components['schemas']['StreamsAWSLambdaConnection']; export type StreamsClusterConnection = components['schemas']['StreamsClusterConnection']; export type StreamsConnection = components['schemas']['StreamsConnection']; +export type StreamsConnectionPrivatePreview = components['schemas']['StreamsConnectionPrivatePreview']; export type StreamsHttpsConnection = components['schemas']['StreamsHttpsConnection']; export type StreamsKafkaAuthentication = components['schemas']['StreamsKafkaAuthentication']; export type StreamsKafkaConnection = components['schemas']['StreamsKafkaConnection']; @@ -7005,6 +7097,7 @@ export type StreamsKafkaNetworkingAccess = components['schemas']['StreamsKafkaNe export type StreamsKafkaSecurity = components['schemas']['StreamsKafkaSecurity']; export type StreamsS3Connection = components['schemas']['StreamsS3Connection']; export type StreamsSampleConnection = components['schemas']['StreamsSampleConnection']; +export type StreamsSchemaRegistryConnectionPrivatePreview = components['schemas']['StreamsSchemaRegistryConnectionPrivatePreview']; export type SynonymMappingStatusDetail = components['schemas']['SynonymMappingStatusDetail']; export type SynonymMappingStatusDetailMap = components['schemas']['SynonymMappingStatusDetailMap']; export type SynonymSource = components['schemas']['SynonymSource']; From acb1eba481890f8cde356116482d25dc886b8351 Mon Sep 17 00:00:00 2001 From: gagik Date: Tue, 25 Nov 2025 11:24:09 +0100 Subject: [PATCH 12/21] chore: remove override value print, add erroring when disabled, cleanup --- src/common/config/configOverrides.ts | 12 +++----- .../transports/configOverrides.test.ts | 29 +++++++++---------- .../common/config/configOverrides.test.ts | 21 ++++++-------- 3 files changed, 26 insertions(+), 36 deletions(-) diff --git a/src/common/config/configOverrides.ts b/src/common/config/configOverrides.ts index 894322c5b..7f821192c 100644 --- a/src/common/config/configOverrides.ts +++ b/src/common/config/configOverrides.ts @@ -27,7 +27,7 @@ export function applyConfigOverrides({ // Only apply overrides if allowRequestOverrides is enabled if (!baseConfig.allowRequestOverrides) { - return baseConfig; + throw new Error("Request overrides are not enabled"); } const result: UserConfig = { ...baseConfig }; @@ -69,11 +69,6 @@ function extractConfigOverrides( } assertValidConfigKey(configKey); - const behavior = getConfigMeta(configKey)?.overrideBehavior || "not-allowed"; - if (behavior === "not-allowed") { - throw new Error(`Config key ${configKey} is not allowed to be overridden`); - } - const parsedValue = parseConfigValue(configKey, value); if (parsedValue !== undefined) { overrides[configKey] = parsedValue; @@ -143,7 +138,7 @@ function applyOverride( return behavior(baseValue, overrideValue); } catch (error) { throw new Error( - `Cannot apply override for ${key} from ${JSON.stringify(baseValue)} to ${JSON.stringify(overrideValue)}: ${error instanceof Error ? error.message : String(error)}` + `Cannot apply override for ${key}: ${error instanceof Error ? error.message : String(error)}` ); } } @@ -155,9 +150,10 @@ function applyOverride( if (Array.isArray(baseValue) && Array.isArray(overrideValue)) { return [...(baseValue as unknown[]), ...(overrideValue as unknown[])]; } - throw new Error("Cannot merge non-array values, did you mean to use the 'override' behavior?"); + throw new Error(`Cannot merge non-array values for ${key}`); case "not-allowed": + throw new Error(`Config key ${key} is not allowed to be overridden`); default: return baseValue; } diff --git a/tests/integration/transports/configOverrides.test.ts b/tests/integration/transports/configOverrides.test.ts index becbcc6de..be43097bb 100644 --- a/tests/integration/transports/configOverrides.test.ts +++ b/tests/integration/transports/configOverrides.test.ts @@ -48,7 +48,7 @@ describe("Config Overrides via HTTP", () => { }); describe("override behavior", () => { - it("should not apply overrides when allowRequestOverrides is false", async () => { + it("should error when allowRequestOverrides is false", async () => { await startRunner({ ...defaultTestConfig, httpPort: 0, @@ -56,18 +56,17 @@ describe("Config Overrides via HTTP", () => { allowRequestOverrides: false, }); - await connectClient({ - ["x-mongodb-mcp-read-only"]: "true", - }); - - const response = await client.listTools(); - - expect(response).toBeDefined(); - expect(response.tools).toBeDefined(); - - // Verify read-only mode is NOT applied - insert-many should still be available - const writeTools = response.tools.filter((tool) => tool.name === "insert-many"); - expect(writeTools.length).toBe(1); + try { + await connectClient({ + ["x-mongodb-mcp-read-only"]: "true", + }); + expect.fail("Expected an error to be thrown"); + } catch (error) { + if (!(error instanceof Error)) { + throw new Error("Expected an error to be thrown"); + } + expect(error.message).toContain("Request overrides are not enabled"); + } }); it("should override readOnly config via header (false to true)", async () => { @@ -376,9 +375,7 @@ describe("Config Overrides via HTTP", () => { throw new Error("Expected an error to be thrown"); } expect(error.message).toContain("Error POSTing to endpoint (HTTP 400)"); - expect(error.message).toContain( - `Cannot apply override for readOnly from true to false: Can only set to true` - ); + expect(error.message).toContain(`Cannot apply override for readOnly: Can only set to true`); } }); }); diff --git a/tests/unit/common/config/configOverrides.test.ts b/tests/unit/common/config/configOverrides.test.ts index 3ce9a2a5e..52169e9f1 100644 --- a/tests/unit/common/config/configOverrides.test.ts +++ b/tests/unit/common/config/configOverrides.test.ts @@ -80,10 +80,9 @@ describe("configOverrides", () => { ...baseConfig, allowRequestOverrides: false, } as UserConfig; - const result = applyConfigOverrides({ baseConfig: configWithOverridesDisabled, request }); - // Config should remain unchanged - expect(result.readOnly).toBe(false); - expect(result.idleTimeoutMs).toBe(600_000); + expect(() => applyConfigOverrides({ baseConfig: configWithOverridesDisabled, request })).to.throw( + "Request overrides are not enabled" + ); }); it("should apply overrides when allowRequestOverrides is true", () => { @@ -107,9 +106,9 @@ describe("configOverrides", () => { }; // eslint-disable-next-line @typescript-eslint/no-unused-vars const { allowRequestOverrides, ...configWithoutOverridesFlag } = baseConfig; - const result = applyConfigOverrides({ baseConfig: configWithoutOverridesFlag as UserConfig, request }); - // Should not apply overrides since the default is false - expect(result.readOnly).toBe(false); + expect(() => + applyConfigOverrides({ baseConfig: configWithoutOverridesFlag as UserConfig, request }) + ).to.throw("Request overrides are not enabled"); }); }); @@ -254,7 +253,7 @@ describe("configOverrides", () => { const request: RequestContext = { headers: { "x-mongodb-mcp-read-only": "false" } }; expect(() => applyConfigOverrides({ baseConfig: { ...baseConfig, readOnly: true } as UserConfig, request }) - ).toThrow("Cannot apply override for readOnly from true to false: Can only set to true"); + ).toThrow("Cannot apply override for readOnly: Can only set to true"); }); it("should allow indexCheck override from false to true", () => { @@ -270,7 +269,7 @@ describe("configOverrides", () => { const request: RequestContext = { headers: { "x-mongodb-mcp-index-check": "false" } }; expect(() => applyConfigOverrides({ baseConfig: { ...baseConfig, indexCheck: true } as UserConfig, request }) - ).toThrow("Cannot apply override for indexCheck from true to false: Can only set to true"); + ).toThrow("Cannot apply override for indexCheck: Can only set to true"); }); it("should allow disableEmbeddingsValidation override from true to false", () => { @@ -289,9 +288,7 @@ describe("configOverrides", () => { baseConfig: { ...baseConfig, disableEmbeddingsValidation: false } as UserConfig, request, }) - ).toThrow( - "Cannot apply override for disableEmbeddingsValidation from false to true: Can only set to false" - ); + ).toThrow("Cannot apply override for disableEmbeddingsValidation: Can only set to false"); }); }); From 83af6bc18377056ed8ba4a91a215daf907e36d3d Mon Sep 17 00:00:00 2001 From: gagik Date: Tue, 25 Nov 2025 11:47:57 +0100 Subject: [PATCH 13/21] cleanup parse boolean --- src/common/config/configUtils.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/common/config/configUtils.ts b/src/common/config/configUtils.ts index 48461ec58..2b9423152 100644 --- a/src/common/config/configUtils.ts +++ b/src/common/config/configUtils.ts @@ -108,14 +108,16 @@ export function commaSeparatedToArray(str: string | string[] } /** - * Preprocessor for boolean values that handles string "true"/"false" correctly. + * Preprocessor for boolean values that handles string "false"/"0" correctly. * Zod's coerce.boolean() treats any non-empty string as true, which is not what we want. */ export function parseBoolean(val: unknown): unknown { if (typeof val === "string") { const lower = val.toLowerCase().trim(); - if (lower === "false" || lower === "0") return false; - if (lower === "true" || lower === "1") return true; + if (lower === "false" || lower === "0" || lower === "") { + return false; + } + return true; } if (typeof val === "boolean") { return val; From 24e8e928b12fe67760a78f777ba4867c2fedf03d Mon Sep 17 00:00:00 2001 From: gagik Date: Tue, 25 Nov 2025 13:48:44 +0100 Subject: [PATCH 14/21] feat: enforce secret overrides only from headers --- src/common/config/configOverrides.ts | 24 +++++++++++++++---- .../transports/configOverrides.test.ts | 8 +++---- .../common/config/configOverrides.test.ts | 23 ++++++++++++++++++ 3 files changed, 46 insertions(+), 9 deletions(-) diff --git a/src/common/config/configOverrides.ts b/src/common/config/configOverrides.ts index 7f821192c..29ef990ec 100644 --- a/src/common/config/configOverrides.ts +++ b/src/common/config/configOverrides.ts @@ -34,13 +34,27 @@ export function applyConfigOverrides({ const overridesFromHeaders = extractConfigOverrides("header", request.headers); const overridesFromQuery = extractConfigOverrides("query", request.query); - // Merge overrides, with query params taking precedence - const allOverrides = { ...overridesFromHeaders, ...overridesFromQuery }; + // Apply header overrides first + for (const [key, overrideValue] of Object.entries(overridesFromHeaders)) { + assertValidConfigKey(key); + const meta = getConfigMeta(key); + const behavior = meta?.overrideBehavior || "not-allowed"; + const baseValue = baseConfig[key as keyof UserConfig]; + const newValue = applyOverride(key, baseValue, overrideValue, behavior); + (result as Record)[key] = newValue; + } - // Apply each override according to its behavior - for (const [key, overrideValue] of Object.entries(allOverrides)) { + // Apply query overrides (with precedence), but block secret fields + for (const [key, overrideValue] of Object.entries(overridesFromQuery)) { assertValidConfigKey(key); - const behavior = getConfigMeta(key)?.overrideBehavior || "not-allowed"; + const meta = getConfigMeta(key); + + // Prevent overriding secret fields via query params + if (meta?.isSecret) { + throw new Error(`Config key ${key} can only be overriden with headers.`); + } + + const behavior = meta?.overrideBehavior || "not-allowed"; const baseValue = baseConfig[key as keyof UserConfig]; const newValue = applyOverride(key, baseValue, overrideValue, behavior); (result as Record)[key] = newValue; diff --git a/tests/integration/transports/configOverrides.test.ts b/tests/integration/transports/configOverrides.test.ts index be43097bb..38002978a 100644 --- a/tests/integration/transports/configOverrides.test.ts +++ b/tests/integration/transports/configOverrides.test.ts @@ -69,7 +69,7 @@ describe("Config Overrides via HTTP", () => { } }); - it("should override readOnly config via header (false to true)", async () => { + it("should override readOnly config with header (false to true)", async () => { await startRunner({ ...defaultTestConfig, httpPort: 0, @@ -95,7 +95,7 @@ describe("Config Overrides via HTTP", () => { expect(readTools.length).toBe(1); }); - it("should override connectionString via header", async () => { + it("should override connectionString with header", async () => { await startRunner({ ...defaultTestConfig, httpPort: 0, @@ -114,7 +114,7 @@ describe("Config Overrides via HTTP", () => { }); describe("merge behavior", () => { - it("should merge disabledTools via header", async () => { + it("should merge disabledTools with header", async () => { await startRunner({ ...defaultTestConfig, httpPort: 0, @@ -178,7 +178,7 @@ describe("Config Overrides via HTTP", () => { headerName: "x-mongodb-mcp-max-documents-per-query", headerValue: "1000", }, - ])("should reject $configKey override", async ({ configKey, headerName, headerValue }) => { + ])("should reject $configKey with header", async ({ configKey, headerName, headerValue }) => { await startRunner({ ...defaultTestConfig, httpPort: 0, diff --git a/tests/unit/common/config/configOverrides.test.ts b/tests/unit/common/config/configOverrides.test.ts index 52169e9f1..6a26c3b1d 100644 --- a/tests/unit/common/config/configOverrides.test.ts +++ b/tests/unit/common/config/configOverrides.test.ts @@ -227,6 +227,29 @@ describe("configOverrides", () => { }); }); + describe("secret fields", () => { + it("should allow overriding secret fields with headers if they have override behavior", () => { + const request: RequestContext = { + headers: { + "x-mongodb-mcp-connection-string": "mongodb://newhost:27017/", + }, + }; + const result = applyConfigOverrides({ baseConfig: baseConfig as UserConfig, request }); + expect(result.connectionString).toBe("mongodb://newhost:27017/"); + }); + + it("should not allow overriding secret fields via query params", () => { + const request: RequestContext = { + query: { + mongodbMcpConnectionString: "mongodb://malicious.com/", + }, + }; + expect(() => applyConfigOverrides({ baseConfig: baseConfig as UserConfig, request })).toThrow( + "Config key connectionString can only be overriden with headers" + ); + }); + }); + describe("custom overrides", () => { it("should have certain config keys to be conditionally overridden", () => { expect( From 386be02a595309dc92754c2d4a41acbf144bb64a Mon Sep 17 00:00:00 2001 From: gagik Date: Tue, 25 Nov 2025 18:05:06 +0100 Subject: [PATCH 15/21] chore: add conditional subset and lower than cases --- src/common/config/configOverrides.ts | 3 +- src/common/config/configUtils.ts | 35 +++++ src/common/config/userConfig.ts | 18 ++- src/lib.ts | 1 + src/transports/base.ts | 7 +- .../transports/configOverrides.test.ts | 148 +++++++++++++++++- tests/unit/common/config.test.ts | 92 ++++++++++- 7 files changed, 282 insertions(+), 22 deletions(-) diff --git a/src/common/config/configOverrides.ts b/src/common/config/configOverrides.ts index 29ef990ec..a0249046a 100644 --- a/src/common/config/configOverrides.ts +++ b/src/common/config/configOverrides.ts @@ -8,7 +8,8 @@ export const CONFIG_QUERY_PREFIX = "mongodbMcp"; /** * Applies config overrides from request context (headers and query parameters). - * Query parameters take precedence over headers. + * Query parameters take precedence over headers. Can be used within the createSessionConfig + * hook to manually apply the overrides. Requires `allowRequestOverrides` to be enabled. * * @param baseConfig - The base user configuration * @param request - The request context containing headers and query parameters diff --git a/src/common/config/configUtils.ts b/src/common/config/configUtils.ts index 2b9423152..8c0aca5dd 100644 --- a/src/common/config/configUtils.ts +++ b/src/common/config/configUtils.ts @@ -138,3 +138,38 @@ export function oneWayOverride(allowedValue: T): CustomOverrideLogic { throw new Error(`Can only set to ${String(allowedValue)}`); }; } + +/** Allow overriding only to a value lower than the specified value */ +export function onlyLowerThanBaseValueOverride(): CustomOverrideLogic { + return (oldValue, newValue) => { + if (typeof oldValue !== "number") { + throw new Error(`Unsupported type for base value for override: ${typeof oldValue}`); + } + if (typeof newValue !== "number") { + throw new Error(`Unsupported type for new value for override: ${typeof newValue}`); + } + if (newValue >= oldValue) { + throw new Error(`Can only set to a value lower than the base value`); + } + return newValue; + }; +} + +/** Allow overriding only to a subset of an array but not a superset */ +export function onlySubsetOfBaseValueOverride(): CustomOverrideLogic { + return (oldValue, newValue) => { + if (!Array.isArray(oldValue)) { + throw new Error(`Unsupported type for base value for override: ${typeof oldValue}`); + } + if (!Array.isArray(newValue)) { + throw new Error(`Unsupported type for new value for override: ${typeof newValue}`); + } + if (newValue.length > oldValue.length) { + throw new Error(`Can only override to a subset of the base value`); + } + if (!newValue.every((value) => oldValue.includes(value))) { + throw new Error(`Can only override to a subset of the base value`); + } + return newValue as unknown; + }; +} diff --git a/src/common/config/userConfig.ts b/src/common/config/userConfig.ts index f452f9ae9..3a52b9318 100644 --- a/src/common/config/userConfig.ts +++ b/src/common/config/userConfig.ts @@ -6,6 +6,8 @@ import { getExportsPath, getLogPath, oneWayOverride, + onlyLowerThanBaseValueOverride, + onlySubsetOfBaseValueOverride, parseBoolean, } from "./configUtils.js"; import { previewFeatureValues, similarityValues } from "../schemas.js"; @@ -38,7 +40,7 @@ export const UserConfigSchema = z4.object({ .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." ) - .register(configRegistry, { isSecret: true, overrideBehavior: "override" }), + .register(configRegistry, { isSecret: true, overrideBehavior: "not-allowed" }), loggers: z4 .preprocess( (val: string | string[] | undefined) => commaSeparatedToArray(val), @@ -54,7 +56,7 @@ export const UserConfigSchema = z4.object({ .describe("An array of logger types.") .register(configRegistry, { defaultValueDescription: '`"disk,mcp"` see below*', - overrideBehavior: "merge", + overrideBehavior: "not-allowed", }), logPath: z4 .string() @@ -133,12 +135,12 @@ export const UserConfigSchema = z4.object({ .number() .default(600_000) .describe("Idle timeout for a client to disconnect (only applies to http transport).") - .register(configRegistry, { overrideBehavior: "override" }), + .register(configRegistry, { overrideBehavior: onlyLowerThanBaseValueOverride() }), notificationTimeoutMs: z4.coerce .number() .default(540_000) .describe("Notification timeout for a client to be aware of disconnect (only applies to http transport).") - .register(configRegistry, { overrideBehavior: "override" }), + .register(configRegistry, { overrideBehavior: onlyLowerThanBaseValueOverride() }), maxBytesPerQuery: z4.coerce .number() .default(16_777_216) @@ -167,21 +169,21 @@ export const UserConfigSchema = z4.object({ .number() .default(120_000) .describe("Time in milliseconds between export cleanup cycles that remove expired export files.") - .register(configRegistry, { overrideBehavior: "override" }), + .register(configRegistry, { overrideBehavior: "not-allowed" }), atlasTemporaryDatabaseUserLifetimeMs: z4.coerce .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." ) - .register(configRegistry, { overrideBehavior: "override" }), + .register(configRegistry, { overrideBehavior: onlyLowerThanBaseValueOverride() }), voyageApiKey: z4 .string() .default("") .describe( "API key for Voyage AI embeddings service (required for vector search operations with text-to-embedding conversion)." ) - .register(configRegistry, { isSecret: true, overrideBehavior: "not-allowed" }), + .register(configRegistry, { isSecret: true, overrideBehavior: "override" }), disableEmbeddingsValidation: z4 .preprocess(parseBoolean, z4.boolean()) .default(false) @@ -206,7 +208,7 @@ export const UserConfigSchema = z4.object({ ) .default([]) .describe("An array of preview features that are enabled.") - .register(configRegistry, { overrideBehavior: "merge" }), + .register(configRegistry, { overrideBehavior: onlySubsetOfBaseValueOverride() }), allowRequestOverrides: z4 .preprocess(parseBoolean, z4.boolean()) .default(false) diff --git a/src/lib.ts b/src/lib.ts index 44ee47c65..72cad9a45 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -24,3 +24,4 @@ export { Telemetry } from "./telemetry/telemetry.js"; export { Keychain, registerGlobalSecretToRedact } from "./common/keychain.js"; export type { Secret } from "./common/keychain.js"; export { Elicitation } from "./elicitation.js"; +export { applyConfigOverrides } from "./common/config/configOverrides.js"; diff --git a/src/transports/base.ts b/src/transports/base.ts index f4d173d5e..9addd550f 100644 --- a/src/transports/base.ts +++ b/src/transports/base.ts @@ -100,13 +100,12 @@ export abstract class TransportRunnerBase { } protected async setupServer(request?: RequestContext): Promise { - // Apply config overrides from request context (headers and query parameters) - let userConfig = applyConfigOverrides({ baseConfig: this.userConfig, request }); + let userConfig: UserConfig = this.userConfig; - // Call the config provider hook if provided, allowing consumers to - // fetch or modify configuration after applying request context overrides if (this.createSessionConfig) { userConfig = await this.createSessionConfig({ userConfig, request }); + } else { + userConfig = applyConfigOverrides({ baseConfig: this.userConfig, request }); } const mcpServer = new McpServer({ diff --git a/tests/integration/transports/configOverrides.test.ts b/tests/integration/transports/configOverrides.test.ts index 38002978a..a60c378ca 100644 --- a/tests/integration/transports/configOverrides.test.ts +++ b/tests/integration/transports/configOverrides.test.ts @@ -95,7 +95,7 @@ describe("Config Overrides via HTTP", () => { expect(readTools.length).toBe(1); }); - it("should override connectionString with header", async () => { + it("should not be able tooverride connectionString with header", async () => { await startRunner({ ...defaultTestConfig, httpPort: 0, @@ -103,13 +103,18 @@ describe("Config Overrides via HTTP", () => { allowRequestOverrides: true, }); - await connectClient({ - ["x-mongodb-mcp-connection-string"]: "mongodb://override:27017", - }); - - const response = await client.listTools(); - - expect(response).toBeDefined(); + try { + await connectClient({ + ["x-mongodb-mcp-connection-string"]: "mongodb://override:27017", + }); + expect.fail("Expected an error to be thrown"); + } catch (error) { + if (!(error instanceof Error)) { + throw new Error("Expected an error to be thrown"); + } + expect(error.message).toContain("Error POSTing to endpoint (HTTP 400)"); + expect(error.message).toContain(`Config key connectionString is not allowed to be overridden`); + } }); }); @@ -415,4 +420,131 @@ describe("Config Overrides via HTTP", () => { expect(findTool).toBeDefined(); }); }); + + describe("onlyLowerThanBaseValueOverride behavior", () => { + it("should allow override to a lower value", async () => { + await startRunner({ + ...defaultTestConfig, + httpPort: 0, + idleTimeoutMs: 600_000, + allowRequestOverrides: true, + }); + + await connectClient({ + ["x-mongodb-mcp-idle-timeout-ms"]: "300000", + }); + + const response = await client.listTools(); + expect(response).toBeDefined(); + }); + + it("should reject override to a higher value", async () => { + await startRunner({ + ...defaultTestConfig, + httpPort: 0, + idleTimeoutMs: 600_000, + allowRequestOverrides: true, + }); + + try { + await connectClient({ + ["x-mongodb-mcp-idle-timeout-ms"]: "900000", + }); + expect.fail("Expected an error to be thrown"); + } catch (error) { + if (!(error instanceof Error)) { + throw new Error("Expected an error to be thrown"); + } + expect(error.message).toContain("Error POSTing to endpoint (HTTP 400)"); + expect(error.message).toContain( + "Cannot apply override for idleTimeoutMs: Can only set to a value lower than the base value" + ); + } + }); + + it("should reject override to equal value", async () => { + await startRunner({ + ...defaultTestConfig, + httpPort: 0, + idleTimeoutMs: 600_000, + allowRequestOverrides: true, + }); + + try { + await connectClient({ + ["x-mongodb-mcp-idle-timeout-ms"]: "600000", + }); + expect.fail("Expected an error to be thrown"); + } catch (error) { + if (!(error instanceof Error)) { + throw new Error("Expected an error to be thrown"); + } + expect(error.message).toContain("Error POSTing to endpoint (HTTP 400)"); + expect(error.message).toContain( + "Cannot apply override for idleTimeoutMs: Can only set to a value lower than the base value" + ); + } + }); + }); + + describe("onlySubsetOfBaseValueOverride behavior", () => { + describe("previewFeatures", () => { + it("should allow override to same value", async () => { + await startRunner({ + ...defaultTestConfig, + httpPort: 0, + previewFeatures: ["vectorSearch"], + allowRequestOverrides: true, + }); + + await connectClient({ + ["x-mongodb-mcp-preview-features"]: "vectorSearch", + }); + + const response = await client.listTools(); + expect(response).toBeDefined(); + }); + + it("should allow override to an empty array (subset of any array)", async () => { + await startRunner({ + ...defaultTestConfig, + httpPort: 0, + previewFeatures: ["vectorSearch"], + allowRequestOverrides: true, + }); + + await connectClient({ + ["x-mongodb-mcp-preview-features"]: "", + }); + + const response = await client.listTools(); + expect(response).toBeDefined(); + }); + + it("should reject override when base is empty array and trying to add items", async () => { + await startRunner({ + ...defaultTestConfig, + httpPort: 0, + previewFeatures: [], + allowRequestOverrides: true, + }); + + // Empty array trying to override with non-empty should fail (superset) + try { + await connectClient({ + ["x-mongodb-mcp-preview-features"]: "vectorSearch", + }); + expect.fail("Expected an error to be thrown"); + } catch (error) { + if (!(error instanceof Error)) { + throw new Error("Expected an error to be thrown"); + } + expect(error.message).toContain("Error POSTing to endpoint (HTTP 400)"); + expect(error.message).toContain( + "Cannot apply override for previewFeatures: Can only override to a subset of the base value" + ); + } + }); + }); + }); }); diff --git a/tests/unit/common/config.test.ts b/tests/unit/common/config.test.ts index 3c5667978..c9be45e7b 100644 --- a/tests/unit/common/config.test.ts +++ b/tests/unit/common/config.test.ts @@ -1,7 +1,12 @@ import { describe, it, expect, vi, beforeEach, afterEach, type MockedFunction } from "vitest"; import { type UserConfig, UserConfigSchema } from "../../../src/common/config/userConfig.js"; import { type CreateUserConfigHelpers, createUserConfig } from "../../../src/common/config/createUserConfig.js"; -import { getLogPath, getExportsPath } from "../../../src/common/config/configUtils.js"; +import { + getLogPath, + getExportsPath, + onlyLowerThanBaseValueOverride, + onlySubsetOfBaseValueOverride, +} from "../../../src/common/config/configUtils.js"; import { Keychain } from "../../../src/common/keychain.js"; import type { Secret } from "../../../src/common/keychain.js"; @@ -982,3 +987,88 @@ describe("keychain management", () => { }); } }); + +describe("custom override logic functions", () => { + describe("onlyLowerThanBaseValueOverride", () => { + it("should allow override to a lower value", () => { + const customLogic = onlyLowerThanBaseValueOverride(); + const result = customLogic(100, 50); + expect(result).toBe(50); + }); + + it("should reject override to a higher value", () => { + const customLogic = onlyLowerThanBaseValueOverride(); + expect(() => customLogic(100, 150)).toThrow("Can only set to a value lower than the base value"); + }); + + it("should reject override to equal value", () => { + const customLogic = onlyLowerThanBaseValueOverride(); + expect(() => customLogic(100, 100)).toThrow("Can only set to a value lower than the base value"); + }); + + it("should throw error if base value is not a number", () => { + const customLogic = onlyLowerThanBaseValueOverride(); + expect(() => customLogic("not a number", 50)).toThrow("Unsupported type for base value for override"); + }); + + it("should throw error if new value is not a number", () => { + const customLogic = onlyLowerThanBaseValueOverride(); + expect(() => customLogic(100, "not a number")).toThrow("Unsupported type for new value for override"); + }); + }); + + describe("onlySubsetOfBaseValueOverride", () => { + it("should allow override to a subset", () => { + const customLogic = onlySubsetOfBaseValueOverride(); + const result = customLogic(["a", "b", "c"], ["a", "b"]); + expect(result).toEqual(["a", "b"]); + }); + + it("should allow override to an empty array", () => { + const customLogic = onlySubsetOfBaseValueOverride(); + const result = customLogic(["a", "b", "c"], []); + expect(result).toEqual([]); + }); + + it("should allow override with same array", () => { + const customLogic = onlySubsetOfBaseValueOverride(); + const result = customLogic(["a", "b"], ["a", "b"]); + expect(result).toEqual(["a", "b"]); + }); + + it("should reject override to a superset", () => { + const customLogic = onlySubsetOfBaseValueOverride(); + expect(() => customLogic(["a", "b"], ["a", "b", "c"])).toThrow( + "Can only override to a subset of the base value" + ); + }); + + it("should reject override with items not in base value", () => { + const customLogic = onlySubsetOfBaseValueOverride(); + expect(() => customLogic(["a", "b"], ["c"])).toThrow("Can only override to a subset of the base value"); + }); + + it("should reject override when base is empty and new is not", () => { + const customLogic = onlySubsetOfBaseValueOverride(); + expect(() => customLogic([], ["a"])).toThrow("Can only override to a subset of the base value"); + }); + + it("should allow override when both arrays are empty", () => { + const customLogic = onlySubsetOfBaseValueOverride(); + const result = customLogic([], []); + expect(result).toEqual([]); + }); + + it("should throw error if base value is not an array", () => { + const customLogic = onlySubsetOfBaseValueOverride(); + expect(() => customLogic("not an array", ["a"])).toThrow("Unsupported type for base value for override"); + }); + + it("should throw error if new value is not an array", () => { + const customLogic = onlySubsetOfBaseValueOverride(); + expect(() => customLogic(["a", "b"], "not an array")).toThrow( + "Unsupported type for new value for override" + ); + }); + }); +}); From e6ab659fa20877f3e5dced4b1631604719da7d76 Mon Sep 17 00:00:00 2001 From: gagik Date: Tue, 25 Nov 2025 18:52:54 +0100 Subject: [PATCH 16/21] chore: exporttimeout should only be lower than base --- src/common/config/userConfig.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/config/userConfig.ts b/src/common/config/userConfig.ts index 3a52b9318..4352826ec 100644 --- a/src/common/config/userConfig.ts +++ b/src/common/config/userConfig.ts @@ -164,7 +164,7 @@ export const UserConfigSchema = z4.object({ .number() .default(300_000) .describe("Time in milliseconds after which an export is considered expired and eligible for cleanup.") - .register(configRegistry, { overrideBehavior: "override" }), + .register(configRegistry, { overrideBehavior: onlyLowerThanBaseValueOverride() }), exportCleanupIntervalMs: z4.coerce .number() .default(120_000) From e752751da0c5e3bb33bda0f53a8a7177421909c4 Mon Sep 17 00:00:00 2001 From: gagik Date: Tue, 25 Nov 2025 18:57:03 +0100 Subject: [PATCH 17/21] chore: fix tests --- .../common/config/configOverrides.test.ts | 44 +++++++++---------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/tests/unit/common/config/configOverrides.test.ts b/tests/unit/common/config/configOverrides.test.ts index 6a26c3b1d..44dc87909 100644 --- a/tests/unit/common/config/configOverrides.test.ts +++ b/tests/unit/common/config/configOverrides.test.ts @@ -123,27 +123,13 @@ describe("configOverrides", () => { expect(result.readOnly).toBe(true); }); - it("should override numeric values with override behavior", () => { - const request: RequestContext = { - headers: { - "x-mongodb-mcp-idle-timeout-ms": "300000", - "x-mongodb-mcp-export-timeout-ms": "600000", - }, - }; - const result = applyConfigOverrides({ baseConfig: baseConfig as UserConfig, request }); - expect(result.idleTimeoutMs).toBe(300000); - expect(result.exportTimeoutMs).toBe(600000); - }); - it("should override string values with override behavior", () => { const request: RequestContext = { headers: { - "x-mongodb-mcp-connection-string": "mongodb://newhost:27017", "x-mongodb-mcp-vector-search-similarity-function": "cosine", }, }; const result = applyConfigOverrides({ baseConfig: baseConfig as UserConfig, request }); - expect(result.connectionString).toBe("mongodb://newhost:27017"); expect(result.vectorSearchSimilarityFunction).toBe("cosine"); }); }); @@ -174,14 +160,15 @@ describe("configOverrides", () => { expect(result.previewFeatures).toEqual([]); }); - it("should merge loggers", () => { + it("should not be able to merge loggers", () => { const request: RequestContext = { headers: { "x-mongodb-mcp-loggers": "stderr", }, }; - const result = applyConfigOverrides({ baseConfig: baseConfig as UserConfig, request }); - expect(result.loggers).toEqual(["disk", "mcp", "stderr"]); + expect(() => applyConfigOverrides({ baseConfig: baseConfig as UserConfig, request })).toThrow( + "Config key loggers is not allowed to be overridden" + ); }); }); @@ -197,6 +184,8 @@ describe("configOverrides", () => { "apiBaseUrl", "apiClientId", "apiClientSecret", + "connectionString", + "loggers", "logPath", "telemetry", "transport", @@ -206,7 +195,7 @@ describe("configOverrides", () => { "maxBytesPerQuery", "maxDocumentsPerQuery", "exportsPath", - "voyageApiKey", + "exportCleanupIntervalMs", "allowRequestOverrides", ]); }); @@ -231,21 +220,21 @@ describe("configOverrides", () => { it("should allow overriding secret fields with headers if they have override behavior", () => { const request: RequestContext = { headers: { - "x-mongodb-mcp-connection-string": "mongodb://newhost:27017/", + "x-mongodb-mcp-voyage-api-key": "test", }, }; const result = applyConfigOverrides({ baseConfig: baseConfig as UserConfig, request }); - expect(result.connectionString).toBe("mongodb://newhost:27017/"); + expect(result.voyageApiKey).toBe("test"); }); it("should not allow overriding secret fields via query params", () => { const request: RequestContext = { query: { - mongodbMcpConnectionString: "mongodb://malicious.com/", + mongodbMcpVoyageApiKey: "test", }, }; expect(() => applyConfigOverrides({ baseConfig: baseConfig as UserConfig, request })).toThrow( - "Config key connectionString can only be overriden with headers" + "Config key voyageApiKey can only be overriden with headers" ); }); }); @@ -260,7 +249,16 @@ describe("configOverrides", () => { ]) .filter(([, behavior]) => typeof behavior === "function") .map(([key]) => key) - ).toEqual(["readOnly", "indexCheck", "disableEmbeddingsValidation"]); + ).toEqual([ + "readOnly", + "indexCheck", + "idleTimeoutMs", + "notificationTimeoutMs", + "exportTimeoutMs", + "atlasTemporaryDatabaseUserLifetimeMs", + "disableEmbeddingsValidation", + "previewFeatures", + ]); }); it("should allow readOnly override from false to true", () => { From d2731d820fadc1ad86dce74eb710da798389c8a5 Mon Sep 17 00:00:00 2001 From: gagik Date: Tue, 25 Nov 2025 19:11:00 +0100 Subject: [PATCH 18/21] chore: adjust override behavior --- src/common/config/configUtils.ts | 13 +++-- src/common/config/userConfig.ts | 5 +- .../common/config/configOverrides.test.ts | 53 +++++++++++++++---- 3 files changed, 56 insertions(+), 15 deletions(-) diff --git a/src/common/config/configUtils.ts b/src/common/config/configUtils.ts index f3e770771..e32617d12 100644 --- a/src/common/config/configUtils.ts +++ b/src/common/config/configUtils.ts @@ -112,12 +112,14 @@ export function commaSeparatedToArray(str: string | string[] * Zod's coerce.boolean() treats any non-empty string as true, which is not what we want. */ export function parseBoolean(val: unknown): unknown { + if (val === undefined) { + return undefined; + } if (typeof val === "string") { - const lower = val.toLowerCase().trim(); - if (lower === "false") { + if (val === "false") { return false; } - if (lower === "true") { + if (val === "true") { return true; } throw new Error(`Invalid boolean value: ${val}`); @@ -134,7 +136,10 @@ export function parseBoolean(val: unknown): unknown { /** Allow overriding only to the allowed value */ export function oneWayOverride(allowedValue: T): CustomOverrideLogic { return (oldValue, newValue) => { - // Only allow override if setting to allowed value + // Only allow override if setting to allowed value or current value + if (newValue === oldValue) { + return newValue; + } if (newValue === allowedValue) { return newValue; } diff --git a/src/common/config/userConfig.ts b/src/common/config/userConfig.ts index aebc3fbb7..6981af4c9 100644 --- a/src/common/config/userConfig.ts +++ b/src/common/config/userConfig.ts @@ -183,11 +183,12 @@ export const UserConfigSchema = z4.object({ .describe( "API key for Voyage AI embeddings service (required for vector search operations with text-to-embedding conversion)." ) - .register(configRegistry, { isSecret: true }), + .register(configRegistry, { isSecret: true, overrideBehavior: "override" }), embeddingsValidation: z4 .preprocess(parseBoolean, z4.boolean()) .default(true) - .describe("When set to false, disables validation of embeddings dimensions."), + .describe("When set to false, disables validation of embeddings dimensions.") + .register(configRegistry, { overrideBehavior: oneWayOverride(true) }), vectorSearchDimensions: z4.coerce .number() .default(1024) diff --git a/tests/unit/common/config/configOverrides.test.ts b/tests/unit/common/config/configOverrides.test.ts index 44dc87909..28ae044be 100644 --- a/tests/unit/common/config/configOverrides.test.ts +++ b/tests/unit/common/config/configOverrides.test.ts @@ -14,7 +14,7 @@ describe("configOverrides", () => { connectionString: "mongodb://localhost:27017", vectorSearchDimensions: 1024, vectorSearchSimilarityFunction: "euclidean", - disableEmbeddingsValidation: false, + embeddingsValidation: false, previewFeatures: [], loggers: ["disk", "mcp"], exportTimeoutMs: 300_000, @@ -63,6 +63,41 @@ describe("configOverrides", () => { expect(result).toEqual(baseConfig); }); + describe("boolean edge cases", () => { + it("should parse correctly for true value", () => { + const request: RequestContext = { + headers: { + "x-mongodb-mcp-read-only": "true", + }, + }; + const result = applyConfigOverrides({ baseConfig: baseConfig as UserConfig, request }); + expect(result.readOnly).toBe(true); + }); + + it("should parse correctly for false value", () => { + const request: RequestContext = { + headers: { + "x-mongodb-mcp-read-only": "false", + }, + }; + const result = applyConfigOverrides({ baseConfig: baseConfig as UserConfig, request }); + expect(result.readOnly).toBe(false); + }); + + for (const value of ["True", "False", "TRUE", "FALSE", "0", "1", ""]) { + it(`should throw an error for ${value}`, () => { + const request: RequestContext = { + headers: { + "x-mongodb-mcp-read-only": value, + }, + }; + expect(() => applyConfigOverrides({ baseConfig: baseConfig as UserConfig, request })).toThrow( + `Invalid boolean value: ${value}` + ); + }); + } + }); + it("should return base config when request has no headers or query", () => { const result = applyConfigOverrides({ baseConfig: baseConfig as UserConfig, request: {} }); expect(result).toEqual(baseConfig); @@ -256,7 +291,7 @@ describe("configOverrides", () => { "notificationTimeoutMs", "exportTimeoutMs", "atlasTemporaryDatabaseUserLifetimeMs", - "disableEmbeddingsValidation", + "embeddingsValidation", "previewFeatures", ]); }); @@ -294,22 +329,22 @@ describe("configOverrides", () => { }); it("should allow disableEmbeddingsValidation override from true to false", () => { - const request: RequestContext = { headers: { "x-mongodb-mcp-disable-embeddings-validation": "false" } }; + const request: RequestContext = { headers: { "x-mongodb-mcp-embeddings-validation": "true" } }; const result = applyConfigOverrides({ - baseConfig: { ...baseConfig, disableEmbeddingsValidation: true } as UserConfig, + baseConfig: { ...baseConfig, embeddingsValidation: true } as UserConfig, request, }); - expect(result.disableEmbeddingsValidation).toBe(false); + expect(result.embeddingsValidation).toBe(true); }); - it("should throw when trying to override disableEmbeddingsValidation from false to true", () => { - const request: RequestContext = { headers: { "x-mongodb-mcp-disable-embeddings-validation": "true" } }; + it("should throw when trying to override embeddingsValidation from false to true", () => { + const request: RequestContext = { headers: { "x-mongodb-mcp-embeddings-validation": "false" } }; expect(() => applyConfigOverrides({ - baseConfig: { ...baseConfig, disableEmbeddingsValidation: false } as UserConfig, + baseConfig: { ...baseConfig, embeddingsValidation: true } as UserConfig, request, }) - ).toThrow("Cannot apply override for disableEmbeddingsValidation: Can only set to false"); + ).toThrow("Cannot apply override for embeddingsValidation: Can only set to true"); }); }); From d11ec6b34c6f5ccd6c286b40a3f131b3230452e2 Mon Sep 17 00:00:00 2001 From: gagik Date: Tue, 25 Nov 2025 19:15:04 +0100 Subject: [PATCH 19/21] chore: only error if there exist overrides --- src/common/config/configOverrides.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/common/config/configOverrides.ts b/src/common/config/configOverrides.ts index a0249046a..69452a796 100644 --- a/src/common/config/configOverrides.ts +++ b/src/common/config/configOverrides.ts @@ -26,15 +26,18 @@ export function applyConfigOverrides({ return baseConfig; } - // Only apply overrides if allowRequestOverrides is enabled - if (!baseConfig.allowRequestOverrides) { - throw new Error("Request overrides are not enabled"); - } - const result: UserConfig = { ...baseConfig }; const overridesFromHeaders = extractConfigOverrides("header", request.headers); const overridesFromQuery = extractConfigOverrides("query", request.query); + // Only apply overrides if allowRequestOverrides is enabled + if ( + !baseConfig.allowRequestOverrides && + (Object.keys(overridesFromHeaders).length > 0 || Object.keys(overridesFromQuery).length > 0) + ) { + throw new Error("Request overrides are not enabled"); + } + // Apply header overrides first for (const [key, overrideValue] of Object.entries(overridesFromHeaders)) { assertValidConfigKey(key); From dbacbda3d82a51615dae2c190e56ff963f4e407e Mon Sep 17 00:00:00 2001 From: gagik Date: Wed, 26 Nov 2025 13:25:39 +0100 Subject: [PATCH 20/21] chore: post-merge adjustments --- tests/integration/transports/configOverrides.test.ts | 8 ++++---- tests/unit/common/config/configOverrides.test.ts | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/integration/transports/configOverrides.test.ts b/tests/integration/transports/configOverrides.test.ts index a60c378ca..7157339f2 100644 --- a/tests/integration/transports/configOverrides.test.ts +++ b/tests/integration/transports/configOverrides.test.ts @@ -493,12 +493,12 @@ describe("Config Overrides via HTTP", () => { await startRunner({ ...defaultTestConfig, httpPort: 0, - previewFeatures: ["vectorSearch"], + previewFeatures: ["search"], allowRequestOverrides: true, }); await connectClient({ - ["x-mongodb-mcp-preview-features"]: "vectorSearch", + ["x-mongodb-mcp-preview-features"]: "search", }); const response = await client.listTools(); @@ -509,7 +509,7 @@ describe("Config Overrides via HTTP", () => { await startRunner({ ...defaultTestConfig, httpPort: 0, - previewFeatures: ["vectorSearch"], + previewFeatures: ["search"], allowRequestOverrides: true, }); @@ -532,7 +532,7 @@ describe("Config Overrides via HTTP", () => { // Empty array trying to override with non-empty should fail (superset) try { await connectClient({ - ["x-mongodb-mcp-preview-features"]: "vectorSearch", + ["x-mongodb-mcp-preview-features"]: "search", }); expect.fail("Expected an error to be thrown"); } catch (error) { diff --git a/tests/unit/common/config/configOverrides.test.ts b/tests/unit/common/config/configOverrides.test.ts index 28ae044be..01a4256cd 100644 --- a/tests/unit/common/config/configOverrides.test.ts +++ b/tests/unit/common/config/configOverrides.test.ts @@ -232,6 +232,7 @@ describe("configOverrides", () => { "exportsPath", "exportCleanupIntervalMs", "allowRequestOverrides", + "dryRun", ]); }); From 3168bd52acdffb4541de24afce03ed0461589fe5 Mon Sep 17 00:00:00 2001 From: gagik Date: Wed, 26 Nov 2025 13:37:58 +0100 Subject: [PATCH 21/21] chore: fix expeced defaults --- tests/unit/common/config.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/common/config.test.ts b/tests/unit/common/config.test.ts index 332beb60b..c5f537d2d 100644 --- a/tests/unit/common/config.test.ts +++ b/tests/unit/common/config.test.ts @@ -65,6 +65,7 @@ const expectedDefaults = { embeddingsValidation: true, previewFeatures: [], dryRun: false, + allowRequestOverrides: false, }; describe("config", () => {