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

mongodb-js / mongodb-mcp-server / 16905560379

12 Aug 2025 10:02AM UTC coverage: 81.998% (+0.2%) from 81.763%
16905560379

Pull #429

github

web-flow
Merge bec33709a into 974fa3623
Pull Request #429: chore: add arg-parser and put the config under test MCP-86

779 of 980 branches covered (79.49%)

Branch coverage included in aggregate %.

192 of 249 new or added lines in 6 files covered. (77.11%)

1 existing line in 1 file now uncovered.

4145 of 5025 relevant lines covered (82.49%)

129.58 hits per line

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

92.79
/src/common/config.ts
1
import path from "path";
4✔
2
import os from "os";
4✔
3
import argv from "yargs-parser";
4✔
4
import type { CliOptions, ConnectionInfo } from "@mongosh/arg-parser";
5
import { generateConnectionInfoFromCliArgs } from "@mongosh/arg-parser";
4✔
6

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

92
function isConnectionSpecifier(arg: string | undefined): boolean {
544✔
93
    return (
544✔
94
        arg !== undefined &&
544✔
95
        (arg.startsWith("mongodb://") ||
4!
NEW
96
            arg.startsWith("mongodb+srv://") ||
×
NEW
97
            !(arg.endsWith(".js") || arg.endsWith(".mongodb")))
×
98
    );
99
}
544✔
100

101
// If we decide to support non-string config options, we'll need to extend the mechanism for parsing
102
// env variables.
103
export interface UserConfig extends CliOptions {
104
    apiBaseUrl: string;
105
    apiClientId?: string;
106
    apiClientSecret?: string;
107
    telemetry: "enabled" | "disabled";
108
    logPath: string;
109
    exportsPath: string;
110
    exportTimeoutMs: number;
111
    exportCleanupIntervalMs: number;
112
    connectionString?: string;
113
    disabledTools: Array<string>;
114
    readOnly?: boolean;
115
    indexCheck?: boolean;
116
    transport: "stdio" | "http";
117
    httpPort: number;
118
    httpHost: string;
119
    loggers: Array<"stderr" | "disk" | "mcp">;
120
    idleTimeoutMs: number;
121
    notificationTimeoutMs: number;
122
}
123

124
export const defaultUserConfig: UserConfig = {
4✔
125
    apiBaseUrl: "https://cloud.mongodb.com/",
4✔
126
    logPath: getLogPath(),
4✔
127
    exportsPath: getExportsPath(),
4✔
128
    exportTimeoutMs: 300000, // 5 minutes
4✔
129
    exportCleanupIntervalMs: 120000, // 2 minutes
4✔
130
    disabledTools: [],
4✔
131
    telemetry: "enabled",
4✔
132
    readOnly: false,
4✔
133
    indexCheck: false,
4✔
134
    transport: "stdio",
4✔
135
    httpPort: 3000,
4✔
136
    httpHost: "127.0.0.1",
4✔
137
    loggers: ["disk", "mcp"],
4✔
138
    idleTimeoutMs: 600000, // 10 minutes
4✔
139
    notificationTimeoutMs: 540000, // 9 minutes
4✔
140
};
4✔
141

142
export const config = setupUserConfig({
4✔
143
    defaults: defaultUserConfig,
4✔
144
    cli: process.argv,
4✔
145
    env: process.env,
4✔
146
});
4✔
147

148
function getLocalDataPath(): string {
320✔
149
    return process.platform === "win32"
320!
150
        ? path.join(process.env.LOCALAPPDATA || process.env.APPDATA || os.homedir(), "mongodb")
×
151
        : path.join(os.homedir(), ".mongodb");
320✔
152
}
320✔
153

154
export const defaultDriverOptions: ConnectionInfo["driverOptions"] = {
4✔
155
    readConcern: {
4✔
156
        level: "local",
4✔
157
    },
4✔
158
    readPreference: "secondaryPreferred",
4✔
159
    writeConcern: {
4✔
160
        w: "majority",
4✔
161
    },
4✔
162
    timeoutMS: 30_000,
4✔
163
    proxy: { useEnvironmentVariableProxies: true },
4✔
164
    applyProxyToOIDC: true,
4✔
165
};
4✔
166

167
export const driverOptions = setupDriverConfig({
4✔
168
    config,
4✔
169
    defaults: defaultDriverOptions,
4✔
170
});
4✔
171

172
function getLogPath(): string {
160✔
173
    const logPath = path.join(getLocalDataPath(), "mongodb-mcp", ".app-logs");
160✔
174
    return logPath;
160✔
175
}
160✔
176

177
function getExportsPath(): string {
160✔
178
    return path.join(getLocalDataPath(), "mongodb-mcp", "exports");
160✔
179
}
160✔
180

181
// Gets the config supplied by the user as environment variables. The variable names
182
// are prefixed with `MDB_MCP_` and the keys match the UserConfig keys, but are converted
183
// to SNAKE_UPPER_CASE.
184
function parseEnvConfig(env: Record<string, unknown>): Partial<UserConfig> {
548✔
185
    function setValue(obj: Record<string, unknown>, path: string[], value: string): void {
548✔
186
        const currentField = path.shift();
308✔
187
        if (!currentField) {
308!
188
            return;
×
189
        }
×
190
        if (path.length === 0) {
308✔
191
            const numberValue = Number(value);
308✔
192
            if (!isNaN(numberValue)) {
308✔
193
                obj[currentField] = numberValue;
12✔
194
                return;
12✔
195
            }
12✔
196

197
            const booleanValue = value.toLocaleLowerCase();
296✔
198
            if (booleanValue === "true" || booleanValue === "false") {
308✔
199
                obj[currentField] = booleanValue === "true";
8✔
200
                return;
8✔
201
            }
8✔
202

203
            // Try to parse an array of values
204
            if (value.indexOf(",") !== -1) {
308✔
205
                obj[currentField] = value.split(",").map((v) => v.trim());
8✔
206
                return;
8✔
207
            }
8✔
208

209
            obj[currentField] = value;
280✔
210
            return;
280✔
211
        }
280!
212

213
        if (!obj[currentField]) {
×
214
            obj[currentField] = {};
×
215
        }
×
216

217
        setValue(obj[currentField] as Record<string, unknown>, path, value);
×
218
    }
308✔
219

220
    const result: Record<string, unknown> = {};
548✔
221
    const mcpVariables = Object.entries(env).filter(
548✔
222
        ([key, value]) => value !== undefined && key.startsWith("MDB_MCP_")
548✔
223
    ) as [string, string][];
548✔
224
    for (const [key, value] of mcpVariables) {
548✔
225
        const fieldPath = key
308✔
226
            .replace("MDB_MCP_", "")
308✔
227
            .split(".")
308✔
228
            .map((part) => SNAKE_CASE_toCamelCase(part));
308✔
229

230
        setValue(result, fieldPath, value);
308✔
231
    }
308✔
232

233
    return result;
548✔
234
}
548✔
235

236
function SNAKE_CASE_toCamelCase(str: string): string {
308✔
237
    return str.toLowerCase().replace(/([-_][a-z])/g, (group) => group.toUpperCase().replace("_", ""));
308✔
238
}
308✔
239

240
// Right now we have arguments that are not compatible with the format used in mongosh.
241
// An example is using --connectionString and positional arguments.
242
// We will consolidate them in a way where the mongosh format takes precedence.
243
// We will warn users that previous configuration is deprecated in favour of
244
// whatever is in mongosh.
245
function parseCliConfig(args: string[]): CliOptions {
548✔
246
    const programArgs = args.slice(2);
548✔
247
    const parsed = argv(programArgs, OPTIONS) as unknown as CliOptions &
548✔
248
        UserConfig & {
249
            _?: string[];
250
        };
251

252
    const positionalArguments = parsed._ ?? [];
548!
253
    // if we have a positional argument that matches a connection string
254
    // store it as the connection specifier and remove it from the argument
255
    // list, so it doesn't get misunderstood by the mongosh args-parser
256
    if (!parsed.nodb && isConnectionSpecifier(positionalArguments[0])) {
548✔
257
        parsed.connectionSpecifier = positionalArguments.shift();
4✔
258
    }
4✔
259

260
    delete parsed._;
548✔
261
    return parsed;
548✔
262
}
548✔
263

264
function commaSeparatedToArray<T extends string[]>(str: string | string[] | undefined): T {
1,096✔
265
    if (str === undefined) {
1,096!
NEW
266
        return [] as unknown as T;
×
NEW
267
    }
×
268

269
    if (!Array.isArray(str)) {
1,096!
NEW
270
        return [str] as T;
×
NEW
271
    }
×
272

273
    if (str.length === 0) {
1,096✔
274
        return str as T;
540✔
275
    }
540✔
276

277
    if (str.length === 1) {
1,096✔
278
        return str[0]
28✔
279
            ?.split(",")
28✔
280
            .map((e) => e.trim())
28✔
281
            .filter((e) => e.length > 0) as T;
28✔
282
    }
28✔
283

284
    return str as T;
528✔
285
}
528✔
286

287
export function setupUserConfig({
4✔
288
    cli,
548✔
289
    env,
548✔
290
    defaults,
548✔
291
}: {
548✔
292
    cli: string[];
293
    env: Record<string, unknown>;
294
    defaults: Partial<UserConfig>;
295
}): UserConfig {
548✔
296
    const userConfig: UserConfig = {
548✔
297
        ...defaults,
548✔
298
        ...parseEnvConfig(env),
548✔
299
        ...parseCliConfig(cli),
548✔
300
    } as UserConfig;
548✔
301

302
    userConfig.disabledTools = commaSeparatedToArray(userConfig.disabledTools);
548✔
303
    userConfig.loggers = commaSeparatedToArray(userConfig.loggers);
548✔
304

305
    if (userConfig.connectionString && userConfig.connectionSpecifier) {
548✔
306
        const connectionInfo = generateConnectionInfoFromCliArgs(userConfig);
4✔
307
        userConfig.connectionString = connectionInfo.connectionString;
4✔
308
    }
4✔
309

310
    const transport = userConfig.transport as string;
548✔
311
    if (transport !== "http" && transport !== "stdio") {
548✔
312
        throw new Error(`Invalid transport: ${transport}`);
8✔
313
    }
8✔
314

315
    const telemetry = userConfig.telemetry as string;
540✔
316
    if (telemetry !== "enabled" && telemetry !== "disabled") {
548✔
317
        throw new Error(`Invalid telemetry: ${telemetry}`);
12✔
318
    }
12✔
319

320
    const httpPort = +userConfig.httpPort;
528✔
321
    if (httpPort < 1 || httpPort > 65535 || isNaN(httpPort)) {
548✔
322
        throw new Error(`Invalid httpPort: ${userConfig.httpPort}`);
12✔
323
    }
12✔
324

325
    if (userConfig.loggers.length === 0) {
548✔
326
        throw new Error("No loggers found in config");
4✔
327
    }
4✔
328

329
    const loggerTypes = new Set(userConfig.loggers);
512✔
330
    if (loggerTypes.size !== userConfig.loggers.length) {
548✔
331
        throw new Error("Duplicate loggers found in config");
4✔
332
    }
4✔
333

334
    for (const loggerType of userConfig.loggers as string[]) {
548✔
335
        if (loggerType !== "mcp" && loggerType !== "disk" && loggerType !== "stderr") {
1,004!
NEW
336
            throw new Error(`Invalid logger: ${loggerType}`);
×
NEW
337
        }
×
338
    }
1,004✔
339

340
    return userConfig;
508✔
341
}
508✔
342

343
export function setupDriverConfig({
4✔
344
    config,
160✔
345
    defaults,
160✔
346
}: {
160✔
347
    config: UserConfig;
348
    defaults: ConnectionInfo["driverOptions"];
349
}): ConnectionInfo["driverOptions"] {
160✔
350
    const { driverOptions } = generateConnectionInfoFromCliArgs(config);
160✔
351
    return {
160✔
352
        ...defaults,
160✔
353
        ...driverOptions,
160✔
354
    };
160✔
355
}
160✔
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