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

mongodb-js / mongodb-mcp-server / 17671857423

12 Sep 2025 10:28AM UTC coverage: 81.466% (-0.06%) from 81.528%
17671857423

Pull #550

github

web-flow
Merge 68f35722a into 02fe6a246
Pull Request #550: fix: add untrusted data wrapper to the export resource MCP-197

967 of 1284 branches covered (75.31%)

Branch coverage included in aggregate %.

44 of 46 new or added lines in 2 files covered. (95.65%)

31 existing lines in 3 files now uncovered.

4866 of 5876 relevant lines covered (82.81%)

44.64 hits per line

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

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

96
interface Options {
97
    string: string[];
98
    boolean: string[];
99
    array: string[];
100
    alias: Record<string, string>;
101
    configuration: Record<string, boolean>;
102
}
103

104
const ALL_CONFIG_KEYS = new Set(
2✔
105
    (OPTIONS.string as readonly string[])
2✔
106
        .concat(OPTIONS.array)
2✔
107
        .concat(OPTIONS.boolean)
2✔
108
        .concat(Object.keys(OPTIONS.alias))
2✔
109
);
2✔
110

111
export function validateConfigKey(key: string): { valid: boolean; suggestion?: string } {
2✔
112
    if (ALL_CONFIG_KEYS.has(key)) {
95✔
113
        return { valid: true };
91✔
114
    }
91✔
115

116
    let minLev = Number.MAX_VALUE;
4✔
117
    let suggestion = "";
4✔
118

119
    // find the closest match for a suggestion
120
    for (const validKey of ALL_CONFIG_KEYS) {
95✔
121
        // check if there is an exact case-insensitive match
122
        if (validKey.toLowerCase() === key.toLowerCase()) {
255✔
123
            return { valid: false, suggestion: validKey };
1✔
124
        }
1✔
125

126
        // else, infer something using levenshtein so we suggest a valid key
127
        const lev = levenshtein.get(key, validKey);
254✔
128
        if (lev < minLev) {
255✔
129
            minLev = lev;
17✔
130
            suggestion = validKey;
17✔
131
        }
17✔
132
    }
255✔
133

134
    if (minLev <= 2) {
95✔
135
        // accept up to 2 typos
136
        return { valid: false, suggestion };
1✔
137
    }
1✔
138

139
    return { valid: false };
2✔
140
}
2✔
141

142
function isConnectionSpecifier(arg: string | undefined): boolean {
149✔
143
    return (
149✔
144
        arg !== undefined &&
149!
145
        (arg.startsWith("mongodb://") ||
1!
UNCOV
146
            arg.startsWith("mongodb+srv://") ||
×
UNCOV
147
            !(arg.endsWith(".js") || arg.endsWith(".mongodb")))
×
148
    );
149
}
149✔
150

151
// If we decide to support non-string config options, we'll need to extend the mechanism for parsing
152
// env variables.
153
export interface UserConfig extends CliOptions {
154
    apiBaseUrl: string;
155
    apiClientId?: string;
156
    apiClientSecret?: string;
157
    telemetry: "enabled" | "disabled";
158
    logPath: string;
159
    exportsPath: string;
160
    exportTimeoutMs: number;
161
    exportCleanupIntervalMs: number;
162
    connectionString?: string;
163
    disabledTools: Array<string>;
164
    readOnly?: boolean;
165
    indexCheck?: boolean;
166
    transport: "stdio" | "http";
167
    httpPort: number;
168
    httpHost: string;
169
    httpHeaders: Record<string, string>;
170
    loggers: Array<"stderr" | "disk" | "mcp">;
171
    idleTimeoutMs: number;
172
    notificationTimeoutMs: number;
173
    atlasTemporaryDatabaseUserLifetimeMs: number;
174
}
175

176
export const defaultUserConfig: UserConfig = {
2✔
177
    apiBaseUrl: "https://cloud.mongodb.com/",
2✔
178
    logPath: getLogPath(),
2✔
179
    exportsPath: getExportsPath(),
2✔
180
    exportTimeoutMs: 5 * 60 * 1000, // 5 minutes
2✔
181
    exportCleanupIntervalMs: 2 * 60 * 1000, // 2 minutes
2✔
182
    disabledTools: [],
2✔
183
    telemetry: "enabled",
2✔
184
    readOnly: false,
2✔
185
    indexCheck: false,
2✔
186
    transport: "stdio",
2✔
187
    httpPort: 3000,
2✔
188
    httpHost: "127.0.0.1",
2✔
189
    loggers: ["disk", "mcp"],
2✔
190
    idleTimeoutMs: 10 * 60 * 1000, // 10 minutes
2✔
191
    notificationTimeoutMs: 9 * 60 * 1000, // 9 minutes
2✔
192
    httpHeaders: {},
2✔
193
    atlasTemporaryDatabaseUserLifetimeMs: 4 * 60 * 60 * 1000, // 4 hours
2✔
194
};
2✔
195

196
export const config = setupUserConfig({
2✔
197
    defaults: defaultUserConfig,
2✔
198
    cli: process.argv,
2✔
199
    env: process.env,
2✔
200
});
2✔
201

202
function getLocalDataPath(): string {
98✔
203
    return process.platform === "win32"
98!
UNCOV
204
        ? path.join(process.env.LOCALAPPDATA || process.env.APPDATA || os.homedir(), "mongodb")
×
205
        : path.join(os.homedir(), ".mongodb");
98✔
206
}
98✔
207

208
export type DriverOptions = ConnectionInfo["driverOptions"];
209
export const defaultDriverOptions: DriverOptions = {
2✔
210
    readConcern: {
2✔
211
        level: "local",
2✔
212
    },
2✔
213
    readPreference: "secondaryPreferred",
2✔
214
    writeConcern: {
2✔
215
        w: "majority",
2✔
216
    },
2✔
217
    timeoutMS: 30_000,
2✔
218
    proxy: { useEnvironmentVariableProxies: true },
2✔
219
    applyProxyToOIDC: true,
2✔
220
};
2✔
221

222
export const driverOptions = setupDriverConfig({
2✔
223
    config,
2✔
224
    defaults: defaultDriverOptions,
2✔
225
});
2✔
226

227
function getLogPath(): string {
49✔
228
    const logPath = path.join(getLocalDataPath(), "mongodb-mcp", ".app-logs");
49✔
229
    return logPath;
49✔
230
}
49✔
231

232
function getExportsPath(): string {
49✔
233
    return path.join(getLocalDataPath(), "mongodb-mcp", "exports");
49✔
234
}
49✔
235

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

242
    function setValue(obj: Record<string, unknown>, path: string[], value: string): void {
150✔
243
        const currentField = path.shift();
37✔
244
        if (!currentField) {
37!
UNCOV
245
            return;
×
UNCOV
246
        }
×
247
        if (path.length === 0) {
37✔
248
            if (CONFIG_WITH_URLS.has(currentField)) {
37!
249
                obj[currentField] = value;
4✔
250
                return;
4✔
251
            }
4✔
252

253
            const numberValue = Number(value);
33✔
254
            if (!isNaN(numberValue)) {
37!
255
                obj[currentField] = numberValue;
4✔
256
                return;
4✔
257
            }
4✔
258

259
            const booleanValue = value.toLocaleLowerCase();
29✔
260
            if (booleanValue === "true" || booleanValue === "false") {
37!
261
                obj[currentField] = booleanValue === "true";
2✔
262
                return;
2✔
263
            }
2✔
264

265
            // Try to parse an array of values
266
            if (value.indexOf(",") !== -1) {
37!
267
                obj[currentField] = value.split(",").map((v) => v.trim());
2✔
268
                return;
2✔
269
            }
2✔
270

271
            obj[currentField] = value;
25✔
272
            return;
25✔
273
        }
25!
274

UNCOV
275
        if (!obj[currentField]) {
×
UNCOV
276
            obj[currentField] = {};
×
UNCOV
277
        }
×
278

UNCOV
279
        setValue(obj[currentField] as Record<string, unknown>, path, value);
×
280
    }
37✔
281

282
    const result: Record<string, unknown> = {};
150✔
283
    const mcpVariables = Object.entries(env).filter(
150✔
284
        ([key, value]) => value !== undefined && key.startsWith("MDB_MCP_")
150✔
285
    ) as [string, string][];
150✔
286
    for (const [key, value] of mcpVariables) {
150✔
287
        const fieldPath = key
37✔
288
            .replace("MDB_MCP_", "")
37✔
289
            .split(".")
37✔
290
            .map((part) => SNAKE_CASE_toCamelCase(part));
37✔
291

292
        setValue(result, fieldPath, value);
37✔
293
    }
37✔
294

295
    return result;
150✔
296
}
150✔
297

298
function SNAKE_CASE_toCamelCase(str: string): string {
37✔
299
    return str.toLowerCase().replace(/([-_][a-z])/g, (group) => group.toUpperCase().replace("_", ""));
37✔
300
}
37✔
301

302
// Right now we have arguments that are not compatible with the format used in mongosh.
303
// An example is using --connectionString and positional arguments.
304
// We will consolidate them in a way where the mongosh format takes precedence.
305
// We will warn users that previous configuration is deprecated in favour of
306
// whatever is in mongosh.
307
function parseCliConfig(args: string[]): CliOptions {
150✔
308
    const programArgs = args.slice(2);
150✔
309
    const parsed = argv(programArgs, OPTIONS as unknown as argv.Options) as unknown as CliOptions &
150✔
310
        UserConfig & {
311
            _?: string[];
312
        };
313

314
    const positionalArguments = parsed._ ?? [];
150!
315

316
    // we use console.warn here because we still don't have our logging system configured
317
    // so we don't have a logger. For stdio, the warning will be received as a string in
318
    // the client and IDEs like VSCode do show the message in the log window. For HTTP,
319
    // it will be in the stdout of the process.
320
    warnAboutDeprecatedOrUnknownCliArgs(
150✔
321
        { ...parsed, _: positionalArguments },
150✔
322
        {
150✔
323
            warn: (msg) => console.warn(msg),
150✔
324
            exit: (status) => process.exit(status),
150✔
325
        }
150✔
326
    );
150✔
327

328
    // if we have a positional argument that matches a connection string
329
    // store it as the connection specifier and remove it from the argument
330
    // list, so it doesn't get misunderstood by the mongosh args-parser
331
    if (!parsed.nodb && isConnectionSpecifier(positionalArguments[0])) {
150!
332
        parsed.connectionSpecifier = positionalArguments.shift();
1✔
333
    }
1✔
334

335
    delete parsed._;
150✔
336
    return parsed;
150✔
337
}
150✔
338

339
export function warnAboutDeprecatedOrUnknownCliArgs(
2✔
340
    args: Record<string, unknown>,
157✔
341
    { warn, exit }: { warn: (msg: string) => void; exit: (status: number) => void | never }
157✔
342
): void {
157✔
343
    let usedDeprecatedArgument = false;
157✔
344
    let usedInvalidArgument = false;
157✔
345

346
    const knownArgs = args as unknown as UserConfig & CliOptions;
157✔
347
    // the first position argument should be used
348
    // instead of --connectionString, as it's how the mongosh works.
349
    if (knownArgs.connectionString) {
157!
350
        usedDeprecatedArgument = true;
8✔
351
        warn(
8✔
352
            "The --connectionString argument is deprecated. Prefer using the first positional argument for the connection string or the MDB_MCP_CONNECTION_STRING environment variable."
8✔
353
        );
8✔
354
    }
8✔
355

356
    for (const providedKey of Object.keys(args)) {
157✔
357
        if (providedKey === "_") {
245✔
358
            // positional argument
359
            continue;
150✔
360
        }
150!
361

362
        const { valid, suggestion } = validateConfigKey(providedKey);
95✔
363
        if (!valid) {
197✔
364
            usedInvalidArgument = true;
4✔
365
            if (suggestion) {
4✔
366
                warn(`Invalid command line argument '${providedKey}'. Did you mean '${suggestion}'?`);
2✔
367
            } else {
2✔
368
                warn(`Invalid command line argument '${providedKey}'.`);
2✔
369
            }
2✔
370
        }
4✔
371
    }
245✔
372

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

377
    if (usedInvalidArgument) {
157!
378
        exit(1);
4✔
379
    }
4✔
380
}
157✔
381

382
function commaSeparatedToArray<T extends string[]>(str: string | string[] | undefined): T {
300✔
383
    if (str === undefined) {
300!
UNCOV
384
        return [] as unknown as T;
×
UNCOV
385
    }
×
386

387
    if (!Array.isArray(str)) {
300!
UNCOV
388
        return [str] as T;
×
UNCOV
389
    }
×
390

391
    if (str.length === 0) {
300✔
392
        return str as T;
148✔
393
    }
148✔
394

395
    if (str.length === 1) {
300!
396
        return str[0]
7✔
397
            ?.split(",")
7✔
398
            .map((e) => e.trim())
7✔
399
            .filter((e) => e.length > 0) as T;
7✔
400
    }
7✔
401

402
    return str as T;
145✔
403
}
145✔
404

405
export function registerKnownSecretsInRootKeychain(userConfig: Partial<UserConfig>): void {
2✔
406
    const keychain = Keychain.root;
152✔
407

408
    const maybeRegister = (value: string | undefined, kind: Secret["kind"]): void => {
152✔
409
        if (value) {
1,824✔
410
            keychain.register(value, kind);
34✔
411
        }
34✔
412
    };
1,824✔
413

414
    maybeRegister(userConfig.apiClientId, "user");
152✔
415
    maybeRegister(userConfig.apiClientSecret, "password");
152✔
416
    maybeRegister(userConfig.awsAccessKeyId, "password");
152✔
417
    maybeRegister(userConfig.awsIamSessionToken, "password");
152✔
418
    maybeRegister(userConfig.awsSecretAccessKey, "password");
152✔
419
    maybeRegister(userConfig.awsSessionToken, "password");
152✔
420
    maybeRegister(userConfig.password, "password");
152✔
421
    maybeRegister(userConfig.tlsCAFile, "url");
152✔
422
    maybeRegister(userConfig.tlsCRLFile, "url");
152✔
423
    maybeRegister(userConfig.tlsCertificateKeyFile, "url");
152✔
424
    maybeRegister(userConfig.tlsCertificateKeyFilePassword, "password");
152✔
425
    maybeRegister(userConfig.username, "user");
152✔
426
}
152✔
427

428
export function setupUserConfig({
2✔
429
    cli,
150✔
430
    env,
150✔
431
    defaults,
150✔
432
}: {
150✔
433
    cli: string[];
434
    env: Record<string, unknown>;
435
    defaults: Partial<UserConfig>;
436
}): UserConfig {
150✔
437
    const userConfig: UserConfig = {
150✔
438
        ...defaults,
150✔
439
        ...parseEnvConfig(env),
150✔
440
        ...parseCliConfig(cli),
150✔
441
    } as UserConfig;
150✔
442

443
    userConfig.disabledTools = commaSeparatedToArray(userConfig.disabledTools);
150✔
444
    userConfig.loggers = commaSeparatedToArray(userConfig.loggers);
150✔
445

446
    if (userConfig.connectionString && userConfig.connectionSpecifier) {
150!
447
        const connectionInfo = generateConnectionInfoFromCliArgs(userConfig);
1✔
448
        userConfig.connectionString = connectionInfo.connectionString;
1✔
449
    }
1✔
450

451
    const transport = userConfig.transport as string;
150✔
452
    if (transport !== "http" && transport !== "stdio") {
150!
453
        throw new Error(`Invalid transport: ${transport}`);
2✔
454
    }
2✔
455

456
    const telemetry = userConfig.telemetry as string;
148✔
457
    if (telemetry !== "enabled" && telemetry !== "disabled") {
150!
458
        throw new Error(`Invalid telemetry: ${telemetry}`);
3✔
459
    }
3✔
460

461
    const httpPort = +userConfig.httpPort;
145✔
462
    if (httpPort < 1 || httpPort > 65535 || isNaN(httpPort)) {
150!
463
        throw new Error(`Invalid httpPort: ${userConfig.httpPort}`);
3✔
464
    }
3✔
465

466
    if (userConfig.loggers.length === 0) {
150!
467
        throw new Error("No loggers found in config");
1✔
468
    }
1✔
469

470
    const loggerTypes = new Set(userConfig.loggers);
141✔
471
    if (loggerTypes.size !== userConfig.loggers.length) {
150!
472
        throw new Error("Duplicate loggers found in config");
1✔
473
    }
1✔
474

475
    for (const loggerType of userConfig.loggers as string[]) {
150✔
476
        if (loggerType !== "mcp" && loggerType !== "disk" && loggerType !== "stderr") {
277!
UNCOV
477
            throw new Error(`Invalid logger: ${loggerType}`);
×
UNCOV
478
        }
×
479
    }
277✔
480

481
    registerKnownSecretsInRootKeychain(userConfig);
140✔
482
    return userConfig;
140✔
483
}
140✔
484

485
export function setupDriverConfig({
2✔
486
    config,
59✔
487
    defaults,
59✔
488
}: {
59✔
489
    config: UserConfig;
490
    defaults: Partial<DriverOptions>;
491
}): DriverOptions {
59✔
492
    const { driverOptions } = generateConnectionInfoFromCliArgs(config);
59✔
493
    return {
59✔
494
        ...defaults,
59✔
495
        ...driverOptions,
59✔
496
    };
59✔
497
}
59✔
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