• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mongodb-js / mongodb-mcp-server / 18342272475

08 Oct 2025 10:52AM UTC coverage: 82.531% (+0.03%) from 82.505%
18342272475

Pull #621

github

web-flow
Merge 33ba4d126 into 543301c2d
Pull Request #621: feat: add ability to create vector search indexes MCP-234

1101 of 1447 branches covered (76.09%)

Branch coverage included in aggregate %.

63 of 73 new or added lines in 1 file covered. (86.3%)

37 existing lines in 2 files now uncovered.

5362 of 6384 relevant lines covered (83.99%)

67.49 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.23
/src/common/config.ts
1
import path from "path";
2✔
2
import os from "os";
2✔
3
import argv from "yargs-parser";
2✔
4
import type { CliOptions, ConnectionInfo } from "@mongosh/arg-parser";
5
import { generateConnectionInfoFromCliArgs } from "@mongosh/arg-parser";
2✔
6
import { Keychain } from "./keychain.js";
2✔
7
import type { Secret } from "./keychain.js";
8
import levenshtein from "ts-levenshtein";
2✔
9

10
// From: https://github.com/mongodb-js/mongosh/blob/main/packages/cli-repl/src/arg-parser.ts
11
const OPTIONS = {
2✔
12
    number: ["maxDocumentsPerQuery", "maxBytesPerQuery"],
2✔
13
    string: [
2✔
14
        "apiBaseUrl",
2✔
15
        "apiClientId",
2✔
16
        "apiClientSecret",
2✔
17
        "connectionString",
2✔
18
        "httpHost",
2✔
19
        "httpPort",
2✔
20
        "idleTimeoutMs",
2✔
21
        "logPath",
2✔
22
        "notificationTimeoutMs",
2✔
23
        "telemetry",
2✔
24
        "transport",
2✔
25
        "apiVersion",
2✔
26
        "authenticationDatabase",
2✔
27
        "authenticationMechanism",
2✔
28
        "browser",
2✔
29
        "db",
2✔
30
        "gssapiHostName",
2✔
31
        "gssapiServiceName",
2✔
32
        "host",
2✔
33
        "oidcFlows",
2✔
34
        "oidcRedirectUri",
2✔
35
        "password",
2✔
36
        "port",
2✔
37
        "sslCAFile",
2✔
38
        "sslCRLFile",
2✔
39
        "sslCertificateSelector",
2✔
40
        "sslDisabledProtocols",
2✔
41
        "sslPEMKeyFile",
2✔
42
        "sslPEMKeyPassword",
2✔
43
        "sspiHostnameCanonicalization",
2✔
44
        "sspiRealmOverride",
2✔
45
        "tlsCAFile",
2✔
46
        "tlsCRLFile",
2✔
47
        "tlsCertificateKeyFile",
2✔
48
        "tlsCertificateKeyFilePassword",
2✔
49
        "tlsCertificateSelector",
2✔
50
        "tlsDisabledProtocols",
2✔
51
        "username",
2✔
52
        "atlasTemporaryDatabaseUserLifetimeMs",
2✔
53
        "exportsPath",
2✔
54
        "exportTimeoutMs",
2✔
55
        "exportCleanupIntervalMs",
2✔
56
        "voyageApiKey",
2✔
57
    ],
2✔
58
    boolean: [
2✔
59
        "apiDeprecationErrors",
2✔
60
        "apiStrict",
2✔
61
        "help",
2✔
62
        "indexCheck",
2✔
63
        "ipv6",
2✔
64
        "nodb",
2✔
65
        "oidcIdTokenAsAccessToken",
2✔
66
        "oidcNoNonce",
2✔
67
        "oidcTrustedEndpoint",
2✔
68
        "readOnly",
2✔
69
        "retryWrites",
2✔
70
        "ssl",
2✔
71
        "sslAllowInvalidCertificates",
2✔
72
        "sslAllowInvalidHostnames",
2✔
73
        "sslFIPSMode",
2✔
74
        "tls",
2✔
75
        "tlsAllowInvalidCertificates",
2✔
76
        "tlsAllowInvalidHostnames",
2✔
77
        "tlsFIPSMode",
2✔
78
        "version",
2✔
79
    ],
2✔
80
    array: ["disabledTools", "loggers", "confirmationRequiredTools"],
2✔
81
    alias: {
2✔
82
        h: "help",
2✔
83
        p: "password",
2✔
84
        u: "username",
2✔
85
        "build-info": "buildInfo",
2✔
86
        browser: "browser",
2✔
87
        oidcDumpTokens: "oidcDumpTokens",
2✔
88
        oidcRedirectUrl: "oidcRedirectUri",
2✔
89
        oidcIDTokenAsAccessToken: "oidcIdTokenAsAccessToken",
2✔
90
    },
2✔
91
    configuration: {
2✔
92
        "camel-case-expansion": false,
2✔
93
        "unknown-options-as-args": true,
2✔
94
        "parse-positional-numbers": false,
2✔
95
        "parse-numbers": false,
2✔
96
        "greedy-arrays": true,
2✔
97
        "short-option-groups": false,
2✔
98
    },
2✔
99
} as Readonly<Options>;
2✔
100

101
interface Options {
102
    string: string[];
103
    number: string[];
104
    boolean: string[];
105
    array: string[];
106
    alias: Record<string, string>;
107
    configuration: Record<string, boolean>;
108
}
109

110
export const ALL_CONFIG_KEYS = new Set(
2✔
111
    (OPTIONS.string as readonly string[])
2✔
112
        .concat(OPTIONS.number)
2✔
113
        .concat(OPTIONS.array)
2✔
114
        .concat(OPTIONS.boolean)
2✔
115
        .concat(Object.keys(OPTIONS.alias))
2✔
116
);
2✔
117

118
export function validateConfigKey(key: string): { valid: boolean; suggestion?: string } {
2✔
119
    if (ALL_CONFIG_KEYS.has(key)) {
95✔
120
        return { valid: true };
91✔
121
    }
91✔
122

123
    let minLev = Number.MAX_VALUE;
4✔
124
    let suggestion = "";
4✔
125

126
    // find the closest match for a suggestion
127
    for (const validKey of ALL_CONFIG_KEYS) {
95✔
128
        // check if there is an exact case-insensitive match
129
        if (validKey.toLowerCase() === key.toLowerCase()) {
283✔
130
            return { valid: false, suggestion: validKey };
1✔
131
        }
1✔
132

133
        // else, infer something using levenshtein so we suggest a valid key
134
        const lev = levenshtein.get(key, validKey);
282✔
135
        if (lev < minLev) {
283✔
136
            minLev = lev;
17✔
137
            suggestion = validKey;
17✔
138
        }
17✔
139
    }
283✔
140

141
    if (minLev <= 2) {
95✔
142
        // accept up to 2 typos
143
        return { valid: false, suggestion };
1✔
144
    }
1✔
145

146
    return { valid: false };
2✔
147
}
2✔
148

149
function isConnectionSpecifier(arg: string | undefined): boolean {
149✔
150
    return (
149✔
151
        arg !== undefined &&
149!
152
        (arg.startsWith("mongodb://") ||
1!
153
            arg.startsWith("mongodb+srv://") ||
×
UNCOV
154
            !(arg.endsWith(".js") || arg.endsWith(".mongodb")))
×
155
    );
156
}
149✔
157

158
// If we decide to support non-string config options, we'll need to extend the mechanism for parsing
159
// env variables.
160
export interface UserConfig extends CliOptions {
161
    apiBaseUrl: string;
162
    apiClientId?: string;
163
    apiClientSecret?: string;
164
    telemetry: "enabled" | "disabled";
165
    logPath: string;
166
    exportsPath: string;
167
    exportTimeoutMs: number;
168
    exportCleanupIntervalMs: number;
169
    connectionString?: string;
170
    // TODO: Use a type tracking all tool names.
171
    disabledTools: Array<string>;
172
    confirmationRequiredTools: Array<string>;
173
    readOnly?: boolean;
174
    indexCheck?: boolean;
175
    transport: "stdio" | "http";
176
    httpPort: number;
177
    httpHost: string;
178
    httpHeaders: Record<string, string>;
179
    loggers: Array<"stderr" | "disk" | "mcp">;
180
    idleTimeoutMs: number;
181
    notificationTimeoutMs: number;
182
    maxDocumentsPerQuery: number;
183
    maxBytesPerQuery: number;
184
    atlasTemporaryDatabaseUserLifetimeMs: number;
185
    voyageApiKey: string;
186
}
187

188
export const defaultUserConfig: UserConfig = {
2✔
189
    apiBaseUrl: "https://cloud.mongodb.com/",
2✔
190
    logPath: getLogPath(),
2✔
191
    exportsPath: getExportsPath(),
2✔
192
    exportTimeoutMs: 5 * 60 * 1000, // 5 minutes
2✔
193
    exportCleanupIntervalMs: 2 * 60 * 1000, // 2 minutes
2✔
194
    disabledTools: [],
2✔
195
    telemetry: "enabled",
2✔
196
    readOnly: false,
2✔
197
    indexCheck: false,
2✔
198
    confirmationRequiredTools: [
2✔
199
        "atlas-create-access-list",
2✔
200
        "atlas-create-db-user",
2✔
201
        "drop-database",
2✔
202
        "drop-collection",
2✔
203
        "delete-many",
2✔
204
    ],
2✔
205
    transport: "stdio",
2✔
206
    httpPort: 3000,
2✔
207
    httpHost: "127.0.0.1",
2✔
208
    loggers: ["disk", "mcp"],
2✔
209
    idleTimeoutMs: 10 * 60 * 1000, // 10 minutes
2✔
210
    notificationTimeoutMs: 9 * 60 * 1000, // 9 minutes
2✔
211
    httpHeaders: {},
2✔
212
    maxDocumentsPerQuery: 100, // By default, we only fetch a maximum 100 documents per query / aggregation
2✔
213
    maxBytesPerQuery: 16 * 1024 * 1024, // By default, we only return ~16 mb of data per query / aggregation
2✔
214
    atlasTemporaryDatabaseUserLifetimeMs: 4 * 60 * 60 * 1000, // 4 hours
2✔
215
    voyageApiKey: "",
2✔
216
};
2✔
217

218
export const config = setupUserConfig({
2✔
219
    defaults: defaultUserConfig,
2✔
220
    cli: process.argv,
2✔
221
    env: process.env,
2✔
222
});
2✔
223

224
function getLocalDataPath(): string {
98✔
225
    return process.platform === "win32"
98!
UNCOV
226
        ? path.join(process.env.LOCALAPPDATA || process.env.APPDATA || os.homedir(), "mongodb")
×
227
        : path.join(os.homedir(), ".mongodb");
98✔
228
}
98✔
229

230
export type DriverOptions = ConnectionInfo["driverOptions"];
231
export const defaultDriverOptions: DriverOptions = {
2✔
232
    readConcern: {
2✔
233
        level: "local",
2✔
234
    },
2✔
235
    readPreference: "secondaryPreferred",
2✔
236
    writeConcern: {
2✔
237
        w: "majority",
2✔
238
    },
2✔
239
    timeoutMS: 30_000,
2✔
240
    proxy: { useEnvironmentVariableProxies: true },
2✔
241
    applyProxyToOIDC: true,
2✔
242
};
2✔
243

244
function getLogPath(): string {
49✔
245
    const logPath = path.join(getLocalDataPath(), "mongodb-mcp", ".app-logs");
49✔
246
    return logPath;
49✔
247
}
49✔
248

249
function getExportsPath(): string {
49✔
250
    return path.join(getLocalDataPath(), "mongodb-mcp", "exports");
49✔
251
}
49✔
252

253
// Gets the config supplied by the user as environment variables. The variable names
254
// are prefixed with `MDB_MCP_` and the keys match the UserConfig keys, but are converted
255
// to SNAKE_UPPER_CASE.
256
function parseEnvConfig(env: Record<string, unknown>): Partial<UserConfig> {
150✔
257
    const CONFIG_WITH_URLS: Set<string> = new Set<(typeof OPTIONS)["string"][number]>(["connectionString"]);
150✔
258

259
    function setValue(obj: Record<string, unknown>, path: string[], value: string): void {
150✔
260
        const currentField = path.shift();
37✔
261
        if (!currentField) {
37!
UNCOV
262
            return;
×
UNCOV
263
        }
×
264
        if (path.length === 0) {
37✔
265
            if (CONFIG_WITH_URLS.has(currentField)) {
37!
266
                obj[currentField] = value;
4✔
267
                return;
4✔
268
            }
4✔
269

270
            const numberValue = Number(value);
33✔
271
            if (!isNaN(numberValue)) {
37!
272
                obj[currentField] = numberValue;
4✔
273
                return;
4✔
274
            }
4✔
275

276
            const booleanValue = value.toLocaleLowerCase();
29✔
277
            if (booleanValue === "true" || booleanValue === "false") {
37!
278
                obj[currentField] = booleanValue === "true";
2✔
279
                return;
2✔
280
            }
2✔
281

282
            // Try to parse an array of values
283
            if (value.indexOf(",") !== -1) {
37!
284
                obj[currentField] = value.split(",").map((v) => v.trim());
2✔
285
                return;
2✔
286
            }
2✔
287

288
            obj[currentField] = value;
25✔
289
            return;
25✔
290
        }
25!
291

UNCOV
292
        if (!obj[currentField]) {
×
293
            obj[currentField] = {};
×
UNCOV
294
        }
×
295

UNCOV
296
        setValue(obj[currentField] as Record<string, unknown>, path, value);
×
297
    }
37✔
298

299
    const result: Record<string, unknown> = {};
150✔
300
    const mcpVariables = Object.entries(env).filter(
150✔
301
        ([key, value]) => value !== undefined && key.startsWith("MDB_MCP_")
150✔
302
    ) as [string, string][];
150✔
303
    for (const [key, value] of mcpVariables) {
150✔
304
        const fieldPath = key
37✔
305
            .replace("MDB_MCP_", "")
37✔
306
            .split(".")
37✔
307
            .map((part) => SNAKE_CASE_toCamelCase(part));
37✔
308

309
        setValue(result, fieldPath, value);
37✔
310
    }
37✔
311

312
    return result;
150✔
313
}
150✔
314

315
function SNAKE_CASE_toCamelCase(str: string): string {
37✔
316
    return str.toLowerCase().replace(/([-_][a-z])/g, (group) => group.toUpperCase().replace("_", ""));
37✔
317
}
37✔
318

319
// Right now we have arguments that are not compatible with the format used in mongosh.
320
// An example is using --connectionString and positional arguments.
321
// We will consolidate them in a way where the mongosh format takes precedence.
322
// We will warn users that previous configuration is deprecated in favour of
323
// whatever is in mongosh.
324
function parseCliConfig(args: string[]): CliOptions {
150✔
325
    const programArgs = args.slice(2);
150✔
326
    const parsed = argv(programArgs, OPTIONS as unknown as argv.Options) as unknown as CliOptions &
150✔
327
        UserConfig & {
328
            _?: string[];
329
        };
330

331
    const positionalArguments = parsed._ ?? [];
150!
332

333
    // we use console.warn here because we still don't have our logging system configured
334
    // so we don't have a logger. For stdio, the warning will be received as a string in
335
    // the client and IDEs like VSCode do show the message in the log window. For HTTP,
336
    // it will be in the stdout of the process.
337
    warnAboutDeprecatedOrUnknownCliArgs(
150✔
338
        { ...parsed, _: positionalArguments },
150✔
339
        {
150✔
340
            warn: (msg) => console.warn(msg),
150✔
341
            exit: (status) => process.exit(status),
150✔
342
        }
150✔
343
    );
150✔
344

345
    // if we have a positional argument that matches a connection string
346
    // store it as the connection specifier and remove it from the argument
347
    // list, so it doesn't get misunderstood by the mongosh args-parser
348
    if (!parsed.nodb && isConnectionSpecifier(positionalArguments[0])) {
150!
349
        parsed.connectionSpecifier = positionalArguments.shift();
1✔
350
    }
1✔
351

352
    delete parsed._;
150✔
353
    return parsed;
150✔
354
}
150✔
355

356
export function warnAboutDeprecatedOrUnknownCliArgs(
2✔
357
    args: Record<string, unknown>,
157✔
358
    { warn, exit }: { warn: (msg: string) => void; exit: (status: number) => void | never }
157✔
359
): void {
157✔
360
    let usedDeprecatedArgument = false;
157✔
361
    let usedInvalidArgument = false;
157✔
362

363
    const knownArgs = args as unknown as UserConfig & CliOptions;
157✔
364
    // the first position argument should be used
365
    // instead of --connectionString, as it's how the mongosh works.
366
    if (knownArgs.connectionString) {
157!
367
        usedDeprecatedArgument = true;
8✔
368
        warn(
8✔
369
            "The --connectionString argument is deprecated. Prefer using the MDB_MCP_CONNECTION_STRING environment variable or the first positional argument for the connection string."
8✔
370
        );
8✔
371
    }
8✔
372

373
    for (const providedKey of Object.keys(args)) {
157✔
374
        if (providedKey === "_") {
245✔
375
            // positional argument
376
            continue;
150✔
377
        }
150!
378

379
        const { valid, suggestion } = validateConfigKey(providedKey);
95✔
380
        if (!valid) {
197✔
381
            usedInvalidArgument = true;
4✔
382
            if (suggestion) {
4✔
383
                warn(`Invalid command line argument '${providedKey}'. Did you mean '${suggestion}'?`);
2✔
384
            } else {
2✔
385
                warn(`Invalid command line argument '${providedKey}'.`);
2✔
386
            }
2✔
387
        }
4✔
388
    }
245✔
389

390
    if (usedInvalidArgument || usedDeprecatedArgument) {
157!
391
        warn("Refer to https://www.mongodb.com/docs/mcp-server/get-started/ for setting up the MCP Server.");
12✔
392
    }
12✔
393

394
    if (usedInvalidArgument) {
157!
395
        exit(1);
4✔
396
    }
4✔
397
}
157✔
398

399
function commaSeparatedToArray<T extends string[]>(str: string | string[] | undefined): T {
450✔
400
    if (str === undefined) {
450!
UNCOV
401
        return [] as unknown as T;
×
402
    }
×
403

404
    if (!Array.isArray(str)) {
450!
UNCOV
405
        return [str] as T;
×
UNCOV
406
    }
×
407

408
    if (str.length === 0) {
450✔
409
        return str as T;
148✔
410
    }
148✔
411

412
    if (str.length === 1) {
450!
413
        return str[0]
7✔
414
            ?.split(",")
7✔
415
            .map((e) => e.trim())
7✔
416
            .filter((e) => e.length > 0) as T;
7✔
417
    }
7✔
418

419
    return str as T;
295✔
420
}
295✔
421

422
export function registerKnownSecretsInRootKeychain(userConfig: Partial<UserConfig>): void {
2✔
423
    const keychain = Keychain.root;
152✔
424

425
    const maybeRegister = (value: string | undefined, kind: Secret["kind"]): void => {
152✔
426
        if (value) {
1,824✔
427
            keychain.register(value, kind);
34✔
428
        }
34✔
429
    };
1,824✔
430

431
    maybeRegister(userConfig.apiClientId, "user");
152✔
432
    maybeRegister(userConfig.apiClientSecret, "password");
152✔
433
    maybeRegister(userConfig.awsAccessKeyId, "password");
152✔
434
    maybeRegister(userConfig.awsIamSessionToken, "password");
152✔
435
    maybeRegister(userConfig.awsSecretAccessKey, "password");
152✔
436
    maybeRegister(userConfig.awsSessionToken, "password");
152✔
437
    maybeRegister(userConfig.password, "password");
152✔
438
    maybeRegister(userConfig.tlsCAFile, "url");
152✔
439
    maybeRegister(userConfig.tlsCRLFile, "url");
152✔
440
    maybeRegister(userConfig.tlsCertificateKeyFile, "url");
152✔
441
    maybeRegister(userConfig.tlsCertificateKeyFilePassword, "password");
152✔
442
    maybeRegister(userConfig.username, "user");
152✔
443
}
152✔
444

445
export function setupUserConfig({
2✔
446
    cli,
150✔
447
    env,
150✔
448
    defaults,
150✔
449
}: {
150✔
450
    cli: string[];
451
    env: Record<string, unknown>;
452
    defaults: Partial<UserConfig>;
453
}): UserConfig {
150✔
454
    const userConfig: UserConfig = {
150✔
455
        ...defaults,
150✔
456
        ...parseEnvConfig(env),
150✔
457
        ...parseCliConfig(cli),
150✔
458
    } as UserConfig;
150✔
459

460
    userConfig.disabledTools = commaSeparatedToArray(userConfig.disabledTools);
150✔
461
    userConfig.loggers = commaSeparatedToArray(userConfig.loggers);
150✔
462
    userConfig.confirmationRequiredTools = commaSeparatedToArray(userConfig.confirmationRequiredTools);
150✔
463

464
    if (userConfig.connectionString && userConfig.connectionSpecifier) {
150!
465
        const connectionInfo = generateConnectionInfoFromCliArgs(userConfig);
1✔
466
        userConfig.connectionString = connectionInfo.connectionString;
1✔
467
    }
1✔
468

469
    const transport = userConfig.transport as string;
150✔
470
    if (transport !== "http" && transport !== "stdio") {
150!
471
        throw new Error(`Invalid transport: ${transport}`);
2✔
472
    }
2✔
473

474
    const telemetry = userConfig.telemetry as string;
148✔
475
    if (telemetry !== "enabled" && telemetry !== "disabled") {
150!
476
        throw new Error(`Invalid telemetry: ${telemetry}`);
3✔
477
    }
3✔
478

479
    const httpPort = +userConfig.httpPort;
145✔
480
    if (httpPort < 1 || httpPort > 65535 || isNaN(httpPort)) {
150!
481
        throw new Error(`Invalid httpPort: ${userConfig.httpPort}`);
3✔
482
    }
3✔
483

484
    if (userConfig.loggers.length === 0) {
150!
485
        throw new Error("No loggers found in config");
1✔
486
    }
1✔
487

488
    const loggerTypes = new Set(userConfig.loggers);
141✔
489
    if (loggerTypes.size !== userConfig.loggers.length) {
150!
490
        throw new Error("Duplicate loggers found in config");
1✔
491
    }
1✔
492

493
    for (const loggerType of userConfig.loggers as string[]) {
150✔
494
        if (loggerType !== "mcp" && loggerType !== "disk" && loggerType !== "stderr") {
277!
UNCOV
495
            throw new Error(`Invalid logger: ${loggerType}`);
×
UNCOV
496
        }
×
497
    }
277✔
498

499
    registerKnownSecretsInRootKeychain(userConfig);
140✔
500
    return userConfig;
140✔
501
}
140✔
502

503
export function setupDriverConfig({
2✔
504
    config,
56✔
505
    defaults,
56✔
506
}: {
56✔
507
    config: UserConfig;
508
    defaults: Partial<DriverOptions>;
509
}): DriverOptions {
56✔
510
    const { driverOptions } = generateConnectionInfoFromCliArgs(config);
56✔
511
    return {
56✔
512
        ...defaults,
56✔
513
        ...driverOptions,
56✔
514
    };
56✔
515
}
56✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc