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

mongodb-js / mongodb-mcp-server / 18871399446

28 Oct 2025 10:15AM UTC coverage: 80.225% (+0.1%) from 80.103%
18871399446

Pull #693

github

web-flow
Merge f0d4a3804 into 34c9c68ca
Pull Request #693: chore: check that a vector search index exists with indexCheck

1353 of 1827 branches covered (74.06%)

Branch coverage included in aggregate %.

51 of 55 new or added lines in 2 files covered. (92.73%)

28 existing lines in 2 files now uncovered.

6355 of 7781 relevant lines covered (81.67%)

71.23 hits per line

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

89.7
/src/common/config.ts
1
import path from "path";
3✔
2
import os from "os";
3✔
3
import argv from "yargs-parser";
3✔
4
import type { CliOptions, ConnectionInfo } from "@mongosh/arg-parser";
5
import { generateConnectionInfoFromCliArgs } from "@mongosh/arg-parser";
3✔
6
import { Keychain } from "./keychain.js";
3✔
7
import type { Secret } from "./keychain.js";
8
import * as levenshteinModule from "ts-levenshtein";
3✔
9
import type { Similarity } from "./search/vectorSearchEmbeddingsManager.js";
10
import { z } from "zod";
3✔
11
const levenshtein = levenshteinModule.default;
3✔
12

13
const previewFeatures = z.enum(["vectorSearch"]);
3✔
14
export type PreviewFeature = z.infer<typeof previewFeatures>;
15

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

108
interface Options {
109
    string: string[];
110
    number: string[];
111
    boolean: string[];
112
    array: string[];
113
    alias: Record<string, string>;
114
    configuration: Record<string, boolean>;
115
}
116

117
export const ALL_CONFIG_KEYS = new Set(
3✔
118
    (OPTIONS.string as readonly string[])
3✔
119
        .concat(OPTIONS.number)
3✔
120
        .concat(OPTIONS.array)
3✔
121
        .concat(OPTIONS.boolean)
3✔
122
        .concat(Object.keys(OPTIONS.alias))
3✔
123
);
3✔
124

125
function validateConfigKey(key: string): { valid: boolean; suggestion?: string } {
95✔
126
    if (ALL_CONFIG_KEYS.has(key)) {
95✔
127
        return { valid: true };
91✔
128
    }
91✔
129

130
    let minLev = Number.MAX_VALUE;
4✔
131
    let suggestion = "";
4✔
132

133
    // find the closest match for a suggestion
134
    for (const validKey of ALL_CONFIG_KEYS) {
95✔
135
        // check if there is an exact case-insensitive match
136
        if (validKey.toLowerCase() === key.toLowerCase()) {
291✔
137
            return { valid: false, suggestion: validKey };
1✔
138
        }
1✔
139

140
        // else, infer something using levenshtein so we suggest a valid key
141
        const lev = levenshtein.get(key, validKey);
290✔
142
        if (lev < minLev) {
291✔
143
            minLev = lev;
17✔
144
            suggestion = validKey;
17✔
145
        }
17✔
146
    }
291✔
147

148
    if (minLev <= 2) {
95✔
149
        // accept up to 2 typos
150
        return { valid: false, suggestion };
1✔
151
    }
1✔
152

153
    return { valid: false };
2✔
154
}
2✔
155

156
function isConnectionSpecifier(arg: string | undefined): boolean {
149✔
157
    return (
149✔
158
        arg !== undefined &&
149!
159
        (arg.startsWith("mongodb://") ||
1!
UNCOV
160
            arg.startsWith("mongodb+srv://") ||
×
UNCOV
161
            !(arg.endsWith(".js") || arg.endsWith(".mongodb")))
×
162
    );
163
}
149✔
164

165
export const UserConfigSchema = z.object({
3✔
166
    apiBaseUrl: z.string().default("https://cloud.mongodb.com/"),
3✔
167
    apiClientId: z
3✔
168
        .string()
3✔
169
        .optional()
3✔
170
        .describe("Atlas API client ID for authentication. Required for running Atlas tools."),
3✔
171
    apiClientSecret: z
3✔
172
        .string()
3✔
173
        .optional()
3✔
174
        .describe("Atlas API client secret for authentication. Required for running Atlas tools."),
3✔
175
    connectionString: z
3✔
176
        .string()
3✔
177
        .optional()
3✔
178
        .describe(
3✔
179
            "MongoDB connection string for direct database connections. Optional, if not set, you'll need to call the connect tool before interacting with MongoDB data."
3✔
180
        ),
3✔
181
    loggers: z
3✔
182
        .array(z.enum(["stderr", "disk", "mcp"]))
3✔
183
        .default(["disk", "mcp"])
3✔
184
        .describe("Comma separated values, possible values are 'mcp', 'disk' and 'stderr'."),
3✔
185
    logPath: z.string().describe("Folder to store logs."),
3✔
186
    disabledTools: z
3✔
187
        .array(z.string())
3✔
188
        .default([])
3✔
189
        .describe("An array of tool names, operation types, and/or categories of tools that will be disabled."),
3✔
190
    confirmationRequiredTools: z
3✔
191
        .array(z.string())
3✔
192
        .default([
3✔
193
            "atlas-create-access-list",
3✔
194
            "atlas-create-db-user",
3✔
195
            "drop-database",
3✔
196
            "drop-collection",
3✔
197
            "delete-many",
3✔
198
            "drop-index",
3✔
199
        ])
3✔
200
        .describe(
3✔
201
            "An array of tool names that require user confirmation before execution. Requires the client to support elicitation."
3✔
202
        ),
3✔
203
    readOnly: z
3✔
204
        .boolean()
3✔
205
        .default(false)
3✔
206
        .describe(
3✔
207
            "When set to true, only allows read, connect, and metadata operation types, disabling create/update/delete operations."
3✔
208
        ),
3✔
209
    indexCheck: z
3✔
210
        .boolean()
3✔
211
        .default(false)
3✔
212
        .describe(
3✔
213
            "When set to true, enforces that query operations must use an index, rejecting queries that perform a collection scan."
3✔
214
        ),
3✔
215
    telemetry: z
3✔
216
        .enum(["enabled", "disabled"])
3✔
217
        .default("enabled")
3✔
218
        .describe("When set to disabled, disables telemetry collection."),
3✔
219
    transport: z.enum(["stdio", "http"]).default("stdio").describe("Either 'stdio' or 'http'."),
3✔
220
    httpPort: z
3✔
221
        .number()
3✔
222
        .default(3000)
3✔
223
        .describe("Port number for the HTTP server (only used when transport is 'http')."),
3✔
224
    httpHost: z
3✔
225
        .string()
3✔
226
        .default("127.0.0.1")
3✔
227
        .describe("Host address to bind the HTTP server to (only used when transport is 'http')."),
3✔
228
    httpHeaders: z
3✔
229
        .record(z.string())
3✔
230
        .default({})
3✔
231
        .describe(
3✔
232
            "Header that the HTTP server will validate when making requests (only used when transport is 'http')."
3✔
233
        ),
3✔
234
    idleTimeoutMs: z
3✔
235
        .number()
3✔
236
        .default(600_000)
3✔
237
        .describe("Idle timeout for a client to disconnect (only applies to http transport)."),
3✔
238
    notificationTimeoutMs: z
3✔
239
        .number()
3✔
240
        .default(540_000)
3✔
241
        .describe("Notification timeout for a client to be aware of disconnect (only applies to http transport)."),
3✔
242
    maxBytesPerQuery: z
3✔
243
        .number()
3✔
244
        .default(16_777_216)
3✔
245
        .describe(
3✔
246
            "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."
3✔
247
        ),
3✔
248
    maxDocumentsPerQuery: z
3✔
249
        .number()
3✔
250
        .default(100)
3✔
251
        .describe(
3✔
252
            "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."
3✔
253
        ),
3✔
254
    exportsPath: z.string().describe("Folder to store exported data files."),
3✔
255
    exportTimeoutMs: z
3✔
256
        .number()
3✔
257
        .default(300_000)
3✔
258
        .describe("Time in milliseconds after which an export is considered expired and eligible for cleanup."),
3✔
259
    exportCleanupIntervalMs: z
3✔
260
        .number()
3✔
261
        .default(120_000)
3✔
262
        .describe("Time in milliseconds between export cleanup cycles that remove expired export files."),
3✔
263
    atlasTemporaryDatabaseUserLifetimeMs: z
3✔
264
        .number()
3✔
265
        .default(14_400_000)
3✔
266
        .describe(
3✔
267
            "Time in milliseconds that temporary database users created when connecting to MongoDB Atlas clusters will remain active before being automatically deleted."
3✔
268
        ),
3✔
269
    voyageApiKey: z
3✔
270
        .string()
3✔
271
        .default("")
3✔
272
        .describe(
3✔
273
            "API key for Voyage AI embeddings service (required for vector search operations with text-to-embedding conversion)."
3✔
274
        ),
3✔
275
    disableEmbeddingsValidation: z
3✔
276
        .boolean()
3✔
277
        .optional()
3✔
278
        .describe("When set to true, disables validation of embeddings dimensions."),
3✔
279
    vectorSearchDimensions: z
3✔
280
        .number()
3✔
281
        .default(1024)
3✔
282
        .describe("Default number of dimensions for vector search embeddings."),
3✔
283
    vectorSearchSimilarityFunction: z
3✔
284
        .custom<Similarity>()
3✔
285
        .optional()
3✔
286
        .default("euclidean")
3✔
287
        .describe("Default similarity function for vector search: 'euclidean', 'cosine', or 'dotProduct'."),
3✔
288
    previewFeatures: z.array(previewFeatures).default([]).describe("An array of preview features that are enabled."),
3✔
289
});
3✔
290

291
export type UserConfig = z.infer<typeof UserConfigSchema> & CliOptions;
292

293
export const defaultUserConfig: UserConfig = {
3✔
294
    apiBaseUrl: "https://cloud.mongodb.com/",
3✔
295
    logPath: getLogPath(),
3✔
296
    exportsPath: getExportsPath(),
3✔
297
    exportTimeoutMs: 5 * 60 * 1000, // 5 minutes
3✔
298
    exportCleanupIntervalMs: 2 * 60 * 1000, // 2 minutes
3✔
299
    disabledTools: [],
3✔
300
    telemetry: "enabled",
3✔
301
    readOnly: false,
3✔
302
    indexCheck: false,
3✔
303
    confirmationRequiredTools: [
3✔
304
        "atlas-create-access-list",
3✔
305
        "atlas-create-db-user",
3✔
306
        "drop-database",
3✔
307
        "drop-collection",
3✔
308
        "delete-many",
3✔
309
        "drop-index",
3✔
310
    ],
3✔
311
    transport: "stdio",
3✔
312
    httpPort: 3000,
3✔
313
    httpHost: "127.0.0.1",
3✔
314
    loggers: ["disk", "mcp"],
3✔
315
    idleTimeoutMs: 10 * 60 * 1000, // 10 minutes
3✔
316
    notificationTimeoutMs: 9 * 60 * 1000, // 9 minutes
3✔
317
    httpHeaders: {},
3✔
318
    maxDocumentsPerQuery: 100, // By default, we only fetch a maximum 100 documents per query / aggregation
3✔
319
    maxBytesPerQuery: 16 * 1024 * 1024, // By default, we only return ~16 mb of data per query / aggregation
3✔
320
    atlasTemporaryDatabaseUserLifetimeMs: 4 * 60 * 60 * 1000, // 4 hours
3✔
321
    voyageApiKey: "",
3✔
322
    disableEmbeddingsValidation: false,
3✔
323
    vectorSearchDimensions: 1024,
3✔
324
    vectorSearchSimilarityFunction: "euclidean",
3✔
325
    previewFeatures: [],
3✔
326
};
3✔
327

328
export const config = setupUserConfig({
3✔
329
    defaults: defaultUserConfig,
3✔
330
    cli: process.argv,
3✔
331
    env: process.env,
3✔
332
});
3✔
333

334
function getLocalDataPath(): string {
98✔
335
    return process.platform === "win32"
98!
UNCOV
336
        ? path.join(process.env.LOCALAPPDATA || process.env.APPDATA || os.homedir(), "mongodb")
×
337
        : path.join(os.homedir(), ".mongodb");
98✔
338
}
98✔
339

340
export type DriverOptions = ConnectionInfo["driverOptions"];
341
export const defaultDriverOptions: DriverOptions = {
3✔
342
    readConcern: {
3✔
343
        level: "local",
3✔
344
    },
3✔
345
    readPreference: "secondaryPreferred",
3✔
346
    writeConcern: {
3✔
347
        w: "majority",
3✔
348
    },
3✔
349
    timeoutMS: 30_000,
3✔
350
    proxy: { useEnvironmentVariableProxies: true },
3✔
351
    applyProxyToOIDC: true,
3✔
352
};
3✔
353

354
function getLogPath(): string {
49✔
355
    const logPath = path.join(getLocalDataPath(), "mongodb-mcp", ".app-logs");
49✔
356
    return logPath;
49✔
357
}
49✔
358

359
function getExportsPath(): string {
49✔
360
    return path.join(getLocalDataPath(), "mongodb-mcp", "exports");
49✔
361
}
49✔
362

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

369
    function setValue(obj: Record<string, unknown>, path: string[], value: string): void {
150✔
370
        const currentField = path.shift();
37✔
371
        if (!currentField) {
37!
UNCOV
372
            return;
×
UNCOV
373
        }
×
374
        if (path.length === 0) {
37✔
375
            if (CONFIG_WITH_URLS.has(currentField)) {
37!
376
                obj[currentField] = value;
4✔
377
                return;
4✔
378
            }
4✔
379

380
            const numberValue = Number(value);
33✔
381
            if (!isNaN(numberValue)) {
37!
382
                obj[currentField] = numberValue;
4✔
383
                return;
4✔
384
            }
4✔
385

386
            const booleanValue = value.toLocaleLowerCase();
29✔
387
            if (booleanValue === "true" || booleanValue === "false") {
37!
388
                obj[currentField] = booleanValue === "true";
2✔
389
                return;
2✔
390
            }
2✔
391

392
            // Try to parse an array of values
393
            if (value.indexOf(",") !== -1) {
37!
394
                obj[currentField] = value.split(",").map((v) => v.trim());
2✔
395
                return;
2✔
396
            }
2✔
397

398
            obj[currentField] = value;
25✔
399
            return;
25✔
400
        }
25!
401

UNCOV
402
        if (!obj[currentField]) {
×
UNCOV
403
            obj[currentField] = {};
×
UNCOV
404
        }
×
405

UNCOV
406
        setValue(obj[currentField] as Record<string, unknown>, path, value);
×
407
    }
37✔
408

409
    const result: Record<string, unknown> = {};
150✔
410
    const mcpVariables = Object.entries(env).filter(
150✔
411
        ([key, value]) => value !== undefined && key.startsWith("MDB_MCP_")
150✔
412
    ) as [string, string][];
150✔
413
    for (const [key, value] of mcpVariables) {
150✔
414
        const fieldPath = key
37✔
415
            .replace("MDB_MCP_", "")
37✔
416
            .split(".")
37✔
417
            .map((part) => SNAKE_CASE_toCamelCase(part));
37✔
418

419
        setValue(result, fieldPath, value);
37✔
420
    }
37✔
421

422
    return result;
150✔
423
}
150✔
424

425
function SNAKE_CASE_toCamelCase(str: string): string {
37✔
426
    return str.toLowerCase().replace(/([-_][a-z])/g, (group) => group.toUpperCase().replace("_", ""));
37✔
427
}
37✔
428

429
// Right now we have arguments that are not compatible with the format used in mongosh.
430
// An example is using --connectionString and positional arguments.
431
// We will consolidate them in a way where the mongosh format takes precedence.
432
// We will warn users that previous configuration is deprecated in favour of
433
// whatever is in mongosh.
434
function parseCliConfig(args: string[]): CliOptions {
150✔
435
    const programArgs = args.slice(2);
150✔
436
    const parsed = argv(programArgs, OPTIONS as unknown as argv.Options) as unknown as CliOptions &
150✔
437
        UserConfig & {
438
            _?: string[];
439
        };
440

441
    const positionalArguments = parsed._ ?? [];
150!
442

443
    // we use console.warn here because we still don't have our logging system configured
444
    // so we don't have a logger. For stdio, the warning will be received as a string in
445
    // the client and IDEs like VSCode do show the message in the log window. For HTTP,
446
    // it will be in the stdout of the process.
447
    warnAboutDeprecatedOrUnknownCliArgs(
150✔
448
        { ...parsed, _: positionalArguments },
150✔
449
        {
150✔
450
            warn: (msg) => console.warn(msg),
150✔
451
            exit: (status) => process.exit(status),
150✔
452
        }
150✔
453
    );
150✔
454

455
    // if we have a positional argument that matches a connection string
456
    // store it as the connection specifier and remove it from the argument
457
    // list, so it doesn't get misunderstood by the mongosh args-parser
458
    if (!parsed.nodb && isConnectionSpecifier(positionalArguments[0])) {
150!
459
        parsed.connectionSpecifier = positionalArguments.shift();
1✔
460
    }
1✔
461

462
    delete parsed._;
150✔
463
    return parsed;
150✔
464
}
150✔
465

466
export function warnAboutDeprecatedOrUnknownCliArgs(
3✔
467
    args: Record<string, unknown>,
157✔
468
    { warn, exit }: { warn: (msg: string) => void; exit: (status: number) => void | never }
157✔
469
): void {
157✔
470
    let usedDeprecatedArgument = false;
157✔
471
    let usedInvalidArgument = false;
157✔
472

473
    const knownArgs = args as unknown as UserConfig & CliOptions;
157✔
474
    // the first position argument should be used
475
    // instead of --connectionString, as it's how the mongosh works.
476
    if (knownArgs.connectionString) {
157!
477
        usedDeprecatedArgument = true;
8✔
478
        warn(
8✔
479
            "The --connectionString argument is deprecated. Prefer using the MDB_MCP_CONNECTION_STRING environment variable or the first positional argument for the connection string."
8✔
480
        );
8✔
481
    }
8✔
482

483
    for (const providedKey of Object.keys(args)) {
157✔
484
        if (providedKey === "_") {
245✔
485
            // positional argument
486
            continue;
150✔
487
        }
150!
488

489
        const { valid, suggestion } = validateConfigKey(providedKey);
95✔
490
        if (!valid) {
197✔
491
            usedInvalidArgument = true;
4✔
492
            if (suggestion) {
4✔
493
                warn(`Invalid command line argument '${providedKey}'. Did you mean '${suggestion}'?`);
2✔
494
            } else {
2✔
495
                warn(`Invalid command line argument '${providedKey}'.`);
2✔
496
            }
2✔
497
        }
4✔
498
    }
245✔
499

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

504
    if (usedInvalidArgument) {
157!
505
        exit(1);
4✔
506
    }
4✔
507
}
157✔
508

509
function commaSeparatedToArray<T extends string[]>(str: string | string[] | undefined): T {
450✔
510
    if (str === undefined) {
450!
511
        return [] as unknown as T;
×
UNCOV
512
    }
×
513

514
    if (!Array.isArray(str)) {
450!
UNCOV
515
        return [str] as T;
×
UNCOV
516
    }
×
517

518
    if (str.length === 0) {
450✔
519
        return str as T;
148✔
520
    }
148✔
521

522
    if (str.length === 1) {
450!
523
        return str[0]
7✔
524
            ?.split(",")
7✔
525
            .map((e) => e.trim())
7✔
526
            .filter((e) => e.length > 0) as T;
7✔
527
    }
7✔
528

529
    return str as T;
295✔
530
}
295✔
531

532
export function registerKnownSecretsInRootKeychain(userConfig: Partial<UserConfig>): void {
3✔
533
    const keychain = Keychain.root;
152✔
534

535
    const maybeRegister = (value: string | undefined, kind: Secret["kind"]): void => {
152✔
536
        if (value) {
1,824!
537
            keychain.register(value, kind);
34✔
538
        }
34✔
539
    };
1,824✔
540

541
    maybeRegister(userConfig.apiClientId, "user");
152✔
542
    maybeRegister(userConfig.apiClientSecret, "password");
152✔
543
    maybeRegister(userConfig.awsAccessKeyId, "password");
152✔
544
    maybeRegister(userConfig.awsIamSessionToken, "password");
152✔
545
    maybeRegister(userConfig.awsSecretAccessKey, "password");
152✔
546
    maybeRegister(userConfig.awsSessionToken, "password");
152✔
547
    maybeRegister(userConfig.password, "password");
152✔
548
    maybeRegister(userConfig.tlsCAFile, "url");
152✔
549
    maybeRegister(userConfig.tlsCRLFile, "url");
152✔
550
    maybeRegister(userConfig.tlsCertificateKeyFile, "url");
152✔
551
    maybeRegister(userConfig.tlsCertificateKeyFilePassword, "password");
152✔
552
    maybeRegister(userConfig.username, "user");
152✔
553
}
152✔
554

555
export function setupUserConfig({
3✔
556
    cli,
150✔
557
    env,
150✔
558
    defaults,
150✔
559
}: {
150✔
560
    cli: string[];
561
    env: Record<string, unknown>;
562
    defaults: UserConfig;
563
}): UserConfig {
150✔
564
    const userConfig = {
150✔
565
        ...defaults,
150✔
566
        ...parseEnvConfig(env),
150✔
567
        ...parseCliConfig(cli),
150✔
568
    } satisfies UserConfig;
150✔
569

570
    userConfig.disabledTools = commaSeparatedToArray(userConfig.disabledTools);
150✔
571
    userConfig.loggers = commaSeparatedToArray(userConfig.loggers);
150✔
572
    userConfig.confirmationRequiredTools = commaSeparatedToArray(userConfig.confirmationRequiredTools);
150✔
573

574
    if (userConfig.connectionString && userConfig.connectionSpecifier) {
150!
575
        const connectionInfo = generateConnectionInfoFromCliArgs(userConfig);
1✔
576
        userConfig.connectionString = connectionInfo.connectionString;
1✔
577
    }
1✔
578

579
    const transport = userConfig.transport as string;
150✔
580
    if (transport !== "http" && transport !== "stdio") {
150!
581
        throw new Error(`Invalid transport: ${transport}`);
2✔
582
    }
2✔
583

584
    const telemetry = userConfig.telemetry as string;
148✔
585
    if (telemetry !== "enabled" && telemetry !== "disabled") {
150!
586
        throw new Error(`Invalid telemetry: ${telemetry}`);
3✔
587
    }
3✔
588

589
    const httpPort = +userConfig.httpPort;
145✔
590
    if (httpPort < 1 || httpPort > 65535 || isNaN(httpPort)) {
150!
591
        throw new Error(`Invalid httpPort: ${userConfig.httpPort}`);
3✔
592
    }
3✔
593

594
    if (userConfig.loggers.length === 0) {
150!
595
        throw new Error("No loggers found in config");
1✔
596
    }
1✔
597

598
    const loggerTypes = new Set(userConfig.loggers);
141✔
599
    if (loggerTypes.size !== userConfig.loggers.length) {
150!
600
        throw new Error("Duplicate loggers found in config");
1✔
601
    }
1✔
602

603
    for (const loggerType of userConfig.loggers as string[]) {
150✔
604
        if (loggerType !== "mcp" && loggerType !== "disk" && loggerType !== "stderr") {
277!
UNCOV
605
            throw new Error(`Invalid logger: ${loggerType}`);
×
UNCOV
606
        }
×
607
    }
277✔
608

609
    registerKnownSecretsInRootKeychain(userConfig);
140✔
610
    return userConfig;
140✔
611
}
140✔
612

613
export function setupDriverConfig({
3✔
614
    config,
55✔
615
    defaults,
55✔
616
}: {
55✔
617
    config: UserConfig;
618
    defaults: Partial<DriverOptions>;
619
}): DriverOptions {
55✔
620
    const { driverOptions } = generateConnectionInfoFromCliArgs(config);
55✔
621
    return {
55✔
622
        ...defaults,
55✔
623
        ...driverOptions,
55✔
624
    };
55✔
625
}
55✔
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