import path from "path";
import os from "os";
import argv from "yargs-parser";
import type { CliOptions, ConnectionInfo } from "@mongosh/arg-parser";
import { generateConnectionInfoFromCliArgs } from "@mongosh/arg-parser";
import { Keychain } from "./keychain.js";
import type { Secret } from "./keychain.js";
import * as levenshteinModule from "ts-levenshtein";
import type { Similarity } from "./search/vectorSearchEmbeddingsManager.js";
import { z } from "zod";
const levenshtein = levenshteinModule.default;
// From: https://github.com/mongodb-js/mongosh/blob/main/packages/cli-repl/src/arg-parser.ts
export const OPTIONS = {
number: ["maxDocumentsPerQuery", "maxBytesPerQuery"],
string: [
"apiBaseUrl",
"apiClientId",
"apiClientSecret",
"connectionString",
"httpHost",
"httpPort",
"idleTimeoutMs",
"logPath",
"notificationTimeoutMs",
"telemetry",
"transport",
"apiVersion",
"authenticationDatabase",
"authenticationMechanism",
"browser",
"db",
"gssapiHostName",
"gssapiServiceName",
"host",
"oidcFlows",
"oidcRedirectUri",
"password",
"port",
"sslCAFile",
"sslCRLFile",
"sslCertificateSelector",
"sslDisabledProtocols",
"sslPEMKeyFile",
"sslPEMKeyPassword",
"sspiHostnameCanonicalization",
"sspiRealmOverride",
"tlsCAFile",
"tlsCRLFile",
"tlsCertificateKeyFile",
"tlsCertificateKeyFilePassword",
"tlsCertificateSelector",
"tlsDisabledProtocols",
"username",
"atlasTemporaryDatabaseUserLifetimeMs",
"exportsPath",
"exportTimeoutMs",
"exportCleanupIntervalMs",
"voyageApiKey",
],
boolean: [
"apiDeprecationErrors",
"apiStrict",
"disableEmbeddingsValidation",
"help",
"indexCheck",
"ipv6",
"nodb",
"oidcIdTokenAsAccessToken",
"oidcNoNonce",
"oidcTrustedEndpoint",
"readOnly",
"retryWrites",
"ssl",
"sslAllowInvalidCertificates",
"sslAllowInvalidHostnames",
"sslFIPSMode",
"tls",
"tlsAllowInvalidCertificates",
"tlsAllowInvalidHostnames",
"tlsFIPSMode",
"version",
],
array: ["disabledTools", "loggers", "confirmationRequiredTools"],
alias: {
h: "help",
p: "password",
u: "username",
"build-info": "buildInfo",
browser: "browser",
oidcDumpTokens: "oidcDumpTokens",
oidcRedirectUrl: "oidcRedirectUri",
oidcIDTokenAsAccessToken: "oidcIdTokenAsAccessToken",
},
configuration: {
"camel-case-expansion": false,
"unknown-options-as-args": true,
"parse-positional-numbers": false,
"parse-numbers": false,
"greedy-arrays": true,
"short-option-groups": false,
},
} as Readonly<Options>;
interface Options {
string: string[];
number: string[];
boolean: string[];
array: string[];
alias: Record<string, string>;
configuration: Record<string, boolean>;
}
export const ALL_CONFIG_KEYS = new Set(
(OPTIONS.string as readonly string[])
.concat(OPTIONS.number)
.concat(OPTIONS.array)
.concat(OPTIONS.boolean)
.concat(Object.keys(OPTIONS.alias))
);
export function validateConfigKey(key: string): { valid: boolean; suggestion?: string } {
if (ALL_CONFIG_KEYS.has(key)) {
return { valid: true };
}
let minLev = Number.MAX_VALUE;
let suggestion = "";
// find the closest match for a suggestion
for (const validKey of ALL_CONFIG_KEYS) {
// check if there is an exact case-insensitive match
if (validKey.toLowerCase() === key.toLowerCase()) {
return { valid: false, suggestion: validKey };
}
// else, infer something using levenshtein so we suggest a valid key
const lev = levenshtein.get(key, validKey);
if (lev < minLev) {
minLev = lev;
suggestion = validKey;
}
}
if (minLev <= 2) {
// accept up to 2 typos
return { valid: false, suggestion };
}
return { valid: false };
}
function isConnectionSpecifier(arg: string | undefined): boolean {
return (
arg !== undefined &&
(arg.startsWith("mongodb://") ||
arg.startsWith("mongodb+srv://") ||
!(arg.endsWith(".js") || arg.endsWith(".mongodb")))
);
}
export const UserConfigSchema = z.object({
apiBaseUrl: z.string().default("https://cloud.mongodb.com/"),
apiClientId: z
.string()
.optional()
.describe("Atlas API client ID for authentication. Required for running Atlas tools."),
apiClientSecret: z
.string()
.optional()
.describe("Atlas API client secret for authentication. Required for running Atlas tools."),
connectionString: z
.string()
.optional()
.describe(
"MongoDB connection string for direct database connections. Optional, if not set, you'll need to call the connect tool before interacting with MongoDB data."
),
loggers: z
.array(z.enum(["stderr", "disk", "mcp"]))
.default(["disk", "mcp"])
.describe("Comma separated values, possible values are 'mcp', 'disk' and 'stderr'."),
logPath: z.string().describe("Folder to store logs."),
disabledTools: z
.array(z.string())
.default([])
.describe("An array of tool names, operation types, and/or categories of tools that will be disabled."),
confirmationRequiredTools: z
.array(z.string())
.default([
"atlas-create-access-list",
"atlas-create-db-user",
"drop-database",
"drop-collection",
"delete-many",
"drop-index",
])
.describe(
"An array of tool names that require user confirmation before execution. Requires the client to support elicitation."
),
readOnly: z
.boolean()
.default(false)
.describe(
"When set to true, only allows read, connect, and metadata operation types, disabling create/update/delete operations."
),
indexCheck: z
.boolean()
.default(false)
.describe(
"When set to true, enforces that query operations must use an index, rejecting queries that perform a collection scan."
),
telemetry: z
.enum(["enabled", "disabled"])
.default("enabled")
.describe("When set to disabled, disables telemetry collection."),
transport: z.enum(["stdio", "http"]).default("stdio").describe("Either 'stdio' or 'http'."),
httpPort: z
.number()
.default(3000)
.describe("Port number for the HTTP server (only used when transport is 'http')."),
httpHost: z
.string()
.default("127.0.0.1")
.describe("Host address to bind the HTTP server to (only used when transport is 'http')."),
httpHeaders: z
.record(z.string())
.default({})
.describe(
"Header that the HTTP server will validate when making requests (only used when transport is 'http')."
),
idleTimeoutMs: z
.number()
.default(600_000)
.describe("Idle timeout for a client to disconnect (only applies to http transport)."),
notificationTimeoutMs: z
.number()
.default(540_000)
.describe("Notification timeout for a client to be aware of disconnect (only applies to http transport)."),
maxBytesPerQuery: z
.number()
.default(16_777_216)
.describe(
"The maximum size in bytes for results from a find or aggregate tool call. This serves as an upper bound for the responseBytesLimit parameter in those tools."
),
maxDocumentsPerQuery: z
.number()
.default(100)
.describe(
"The maximum number of documents that can be returned by a find or aggregate tool call. For the find tool, the effective limit will be the smaller of this value and the tool's limit parameter."
),
exportsPath: z.string().describe("Folder to store exported data files."),
exportTimeoutMs: z
.number()
.default(300_000)
.describe("Time in milliseconds after which an export is considered expired and eligible for cleanup."),
exportCleanupIntervalMs: z
.number()
.default(120_000)
.describe("Time in milliseconds between export cleanup cycles that remove expired export files."),
atlasTemporaryDatabaseUserLifetimeMs: z
.number()
.default(14_400_000)
.describe(
"Time in milliseconds that temporary database users created when connecting to MongoDB Atlas clusters will remain active before being automatically deleted."
),
voyageApiKey: z
.string()
.default("")
.describe(
"API key for Voyage AI embeddings service (required for vector search operations with text-to-embedding conversion)."
),
disableEmbeddingsValidation: z
.boolean()
.optional()
.describe("When set to true, disables validation of embeddings dimensions."),
vectorSearchDimensions: z
.number()
.default(1024)
.describe("Default number of dimensions for vector search embeddings."),
vectorSearchSimilarityFunction: z
.custom<Similarity>()
.optional()
.default("euclidean")
.describe("Default similarity function for vector search: 'euclidean', 'cosine', or 'dotProduct'."),
});
export type UserConfig = z.infer<typeof UserConfigSchema> & CliOptions;
export const defaultUserConfig: UserConfig = {
apiBaseUrl: "https://cloud.mongodb.com/",
logPath: getLogPath(),
exportsPath: getExportsPath(),
exportTimeoutMs: 5 * 60 * 1000, // 5 minutes
exportCleanupIntervalMs: 2 * 60 * 1000, // 2 minutes
disabledTools: [],
telemetry: "enabled",
readOnly: false,
indexCheck: false,
confirmationRequiredTools: [
"atlas-create-access-list",
"atlas-create-db-user",
"drop-database",
"drop-collection",
"delete-many",
"drop-index",
],
transport: "stdio",
httpPort: 3000,
httpHost: "127.0.0.1",
loggers: ["disk", "mcp"],
idleTimeoutMs: 10 * 60 * 1000, // 10 minutes
notificationTimeoutMs: 9 * 60 * 1000, // 9 minutes
httpHeaders: {},
maxDocumentsPerQuery: 100, // By default, we only fetch a maximum 100 documents per query / aggregation
maxBytesPerQuery: 16 * 1024 * 1024, // By default, we only return ~16 mb of data per query / aggregation
atlasTemporaryDatabaseUserLifetimeMs: 4 * 60 * 60 * 1000, // 4 hours
voyageApiKey: "",
disableEmbeddingsValidation: false,
vectorSearchDimensions: 1024,
vectorSearchSimilarityFunction: "euclidean",
};
export const config = setupUserConfig({
defaults: defaultUserConfig,
cli: process.argv,
env: process.env,
});
function getLocalDataPath(): string {
return process.platform === "win32"
? path.join(process.env.LOCALAPPDATA || process.env.APPDATA || os.homedir(), "mongodb")
: path.join(os.homedir(), ".mongodb");
}
export type DriverOptions = ConnectionInfo["driverOptions"];
export const defaultDriverOptions: DriverOptions = {
readConcern: {
level: "local",
},
readPreference: "secondaryPreferred",
writeConcern: {
w: "majority",
},
timeoutMS: 30_000,
proxy: { useEnvironmentVariableProxies: true },
applyProxyToOIDC: true,
};
function getLogPath(): string {
const logPath = path.join(getLocalDataPath(), "mongodb-mcp", ".app-logs");
return logPath;
}
function getExportsPath(): string {
return path.join(getLocalDataPath(), "mongodb-mcp", "exports");
}
// Gets the config supplied by the user as environment variables. The variable names
// are prefixed with `MDB_MCP_` and the keys match the UserConfig keys, but are converted
// to SNAKE_UPPER_CASE.
function parseEnvConfig(env: Record<string, unknown>): Partial<UserConfig> {
const CONFIG_WITH_URLS: Set<string> = new Set<(typeof OPTIONS)["string"][number]>(["connectionString"]);
function setValue(obj: Record<string, unknown>, path: string[], value: string): void {
const currentField = path.shift();
if (!currentField) {
return;
}
if (path.length === 0) {
if (CONFIG_WITH_URLS.has(currentField)) {
obj[currentField] = value;
return;
}
const numberValue = Number(value);
if (!isNaN(numberValue)) {
obj[currentField] = numberValue;
return;
}
const booleanValue = value.toLocaleLowerCase();
if (booleanValue === "true" || booleanValue === "false") {
obj[currentField] = booleanValue === "true";
return;
}
// Try to parse an array of values
if (value.indexOf(",") !== -1) {
obj[currentField] = value.split(",").map((v) => v.trim());
return;
}
obj[currentField] = value;
return;
}
if (!obj[currentField]) {
obj[currentField] = {};
}
setValue(obj[currentField] as Record<string, unknown>, path, value);
}
const result: Record<string, unknown> = {};
const mcpVariables = Object.entries(env).filter(
([key, value]) => value !== undefined && key.startsWith("MDB_MCP_")
) as [string, string][];
for (const [key, value] of mcpVariables) {
const fieldPath = key
.replace("MDB_MCP_", "")
.split(".")
.map((part) => SNAKE_CASE_toCamelCase(part));
setValue(result, fieldPath, value);
}
return result;
}
function SNAKE_CASE_toCamelCase(str: string): string {
return str.toLowerCase().replace(/([-_][a-z])/g, (group) => group.toUpperCase().replace("_", ""));
}
// Right now we have arguments that are not compatible with the format used in mongosh.
// An example is using --connectionString and positional arguments.
// We will consolidate them in a way where the mongosh format takes precedence.
// We will warn users that previous configuration is deprecated in favour of
// whatever is in mongosh.
function parseCliConfig(args: string[]): CliOptions {
const programArgs = args.slice(2);
const parsed = argv(programArgs, OPTIONS as unknown as argv.Options) as unknown as CliOptions &
UserConfig & {
_?: string[];
};
const positionalArguments = parsed._ ?? [];
// we use console.warn here because we still don't have our logging system configured
// so we don't have a logger. For stdio, the warning will be received as a string in
// the client and IDEs like VSCode do show the message in the log window. For HTTP,
// it will be in the stdout of the process.
warnAboutDeprecatedOrUnknownCliArgs(
{ ...parsed, _: positionalArguments },
{
warn: (msg) => console.warn(msg),
exit: (status) => process.exit(status),
}
);
// if we have a positional argument that matches a connection string
// store it as the connection specifier and remove it from the argument
// list, so it doesn't get misunderstood by the mongosh args-parser
if (!parsed.nodb && isConnectionSpecifier(positionalArguments[0])) {
parsed.connectionSpecifier = positionalArguments.shift();
}
delete parsed._;
return parsed;
}
export function warnAboutDeprecatedOrUnknownCliArgs(
args: Record<string, unknown>,
{ warn, exit }: { warn: (msg: string) => void; exit: (status: number) => void | never }
): void {
let usedDeprecatedArgument = false;
let usedInvalidArgument = false;
const knownArgs = args as unknown as UserConfig & CliOptions;
// the first position argument should be used
// instead of --connectionString, as it's how the mongosh works.
if (knownArgs.connectionString) {
usedDeprecatedArgument = true;
warn(
"The --connectionString argument is deprecated. Prefer using the MDB_MCP_CONNECTION_STRING environment variable or the first positional argument for the connection string."
);
}
for (const providedKey of Object.keys(args)) {
if (providedKey === "_") {
// positional argument
continue;
}
const { valid, suggestion } = validateConfigKey(providedKey);
if (!valid) {
usedInvalidArgument = true;
if (suggestion) {
warn(`Invalid command line argument '${providedKey}'. Did you mean '${suggestion}'?`);
} else {
warn(`Invalid command line argument '${providedKey}'.`);
}
}
}
if (usedInvalidArgument || usedDeprecatedArgument) {
warn("Refer to https://www.mongodb.com/docs/mcp-server/get-started/ for setting up the MCP Server.");
}
if (usedInvalidArgument) {
exit(1);
}
}
function commaSeparatedToArray<T extends string[]>(str: string | string[] | undefined): T {
if (str === undefined) {
return [] as unknown as T;
}
if (!Array.isArray(str)) {
return [str] as T;
}
if (str.length === 0) {
return str as T;
}
if (str.length === 1) {
return str[0]
?.split(",")
.map((e) => e.trim())
.filter((e) => e.length > 0) as T;
}
return str as T;
}
export function registerKnownSecretsInRootKeychain(userConfig: Partial<UserConfig>): void {
const keychain = Keychain.root;
const maybeRegister = (value: string | undefined, kind: Secret["kind"]): void => {
if (value) {
keychain.register(value, kind);
}
};
maybeRegister(userConfig.apiClientId, "user");
maybeRegister(userConfig.apiClientSecret, "password");
maybeRegister(userConfig.awsAccessKeyId, "password");
maybeRegister(userConfig.awsIamSessionToken, "password");
maybeRegister(userConfig.awsSecretAccessKey, "password");
maybeRegister(userConfig.awsSessionToken, "password");
maybeRegister(userConfig.password, "password");
maybeRegister(userConfig.tlsCAFile, "url");
maybeRegister(userConfig.tlsCRLFile, "url");
maybeRegister(userConfig.tlsCertificateKeyFile, "url");
maybeRegister(userConfig.tlsCertificateKeyFilePassword, "password");
maybeRegister(userConfig.username, "user");
}
export function setupUserConfig({
cli,
env,
defaults,
}: {
cli: string[];
env: Record<string, unknown>;
defaults: Partial<UserConfig>;
}): UserConfig {
const userConfig: UserConfig = {
...defaults,
...parseEnvConfig(env),
...parseCliConfig(cli),
} as UserConfig;
userConfig.disabledTools = commaSeparatedToArray(userConfig.disabledTools);
userConfig.loggers = commaSeparatedToArray(userConfig.loggers);
userConfig.confirmationRequiredTools = commaSeparatedToArray(userConfig.confirmationRequiredTools);
if (userConfig.connectionString && userConfig.connectionSpecifier) {
const connectionInfo = generateConnectionInfoFromCliArgs(userConfig);
userConfig.connectionString = connectionInfo.connectionString;
}
const transport = userConfig.transport as string;
if (transport !== "http" && transport !== "stdio") {
throw new Error(`Invalid transport: ${transport}`);
}
const telemetry = userConfig.telemetry as string;
if (telemetry !== "enabled" && telemetry !== "disabled") {
throw new Error(`Invalid telemetry: ${telemetry}`);
}
const httpPort = +userConfig.httpPort;
if (httpPort < 1 || httpPort > 65535 || isNaN(httpPort)) {
throw new Error(`Invalid httpPort: ${userConfig.httpPort}`);
}
if (userConfig.loggers.length === 0) {
throw new Error("No loggers found in config");
}
const loggerTypes = new Set(userConfig.loggers);
if (loggerTypes.size !== userConfig.loggers.length) {
throw new Error("Duplicate loggers found in config");
}
for (const loggerType of userConfig.loggers as string[]) {
if (loggerType !== "mcp" && loggerType !== "disk" && loggerType !== "stderr") {
throw new Error(`Invalid logger: ${loggerType}`);
}
}
registerKnownSecretsInRootKeychain(userConfig);
return userConfig;
}
export function setupDriverConfig({
config,
defaults,
}: {
config: UserConfig;
defaults: Partial<DriverOptions>;
}): DriverOptions {
const { driverOptions } = generateConnectionInfoFromCliArgs(config);
return {
...defaults,
...driverOptions,
};
}