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

mongodb-js / mongodb-mcp-server / 19226901281

10 Nov 2025 09:27AM UTC coverage: 80.11% (-0.04%) from 80.151%
19226901281

Pull #717

github

web-flow
Merge d9bd87037 into 15fb57f55
Pull Request #717: chore: adds field embeddings validation for quantization "none" and warn when vectorSearch is not configured correctly

1371 of 1826 branches covered (75.08%)

Branch coverage included in aggregate %.

49 of 55 new or added lines in 7 files covered. (89.09%)

1 existing line in 1 file now uncovered.

6507 of 8008 relevant lines covered (81.26%)

69.35 hits per line

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

89.25
/src/common/config.ts
1
import argv from "yargs-parser";
3✔
2
import type { CliOptions, ConnectionInfo } from "@mongosh/arg-parser";
3
import { generateConnectionInfoFromCliArgs } from "@mongosh/arg-parser";
3✔
4
import { Keychain } from "./keychain.js";
3✔
5
import type { Secret } from "./keychain.js";
6
import { z as z4 } from "zod/v4";
3✔
7
import {
3✔
8
    commaSeparatedToArray,
9
    type ConfigFieldMeta,
10
    getExportsPath,
11
    getLogPath,
12
    isConnectionSpecifier,
13
    validateConfigKey,
14
} from "./configUtils.js";
15
import { OPTIONS } from "./argsParserOptions.js";
3✔
16
import { similarityValues, previewFeatureValues } from "./schemas.js";
3✔
17

18
export const configRegistry = z4.registry<ConfigFieldMeta>();
3✔
19

20
export const UserConfigSchema = z4.object({
3✔
21
    apiBaseUrl: z4.string().default("https://cloud.mongodb.com/"),
3✔
22
    apiClientId: z4
3✔
23
        .string()
3✔
24
        .optional()
3✔
25
        .describe("Atlas API client ID for authentication. Required for running Atlas tools.")
3✔
26
        .register(configRegistry, { isSecret: true }),
3✔
27
    apiClientSecret: z4
3✔
28
        .string()
3✔
29
        .optional()
3✔
30
        .describe("Atlas API client secret for authentication. Required for running Atlas tools.")
3✔
31
        .register(configRegistry, { isSecret: true }),
3✔
32
    connectionString: z4
3✔
33
        .string()
3✔
34
        .optional()
3✔
35
        .describe(
3✔
36
            "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✔
37
        )
3✔
38
        .register(configRegistry, { isSecret: true }),
3✔
39
    loggers: z4
3✔
40
        .preprocess(
3✔
41
            (val: string | string[] | undefined) => commaSeparatedToArray(val),
3✔
42
            z4.array(z4.enum(["stderr", "disk", "mcp"]))
3✔
43
        )
3✔
44
        .check(
3✔
45
            z4.minLength(1, "Cannot be an empty array"),
3✔
46
            z4.refine((val) => new Set(val).size === val.length, {
3✔
47
                message: "Duplicate loggers found in config",
3✔
48
            })
3✔
49
        )
3✔
50
        .default(["disk", "mcp"])
3✔
51
        .describe("An array of logger types.")
3✔
52
        .register(configRegistry, {
3✔
53
            defaultValueDescription: '`"disk,mcp"` see below*',
3✔
54
        }),
3✔
55
    logPath: z4
3✔
56
        .string()
3✔
57
        .default(getLogPath())
3✔
58
        .describe("Folder to store logs.")
3✔
59
        .register(configRegistry, { defaultValueDescription: "see below*" }),
3✔
60
    disabledTools: z4
3✔
61
        .preprocess((val: string | string[] | undefined) => commaSeparatedToArray(val), z4.array(z4.string()))
3✔
62
        .default([])
3✔
63
        .describe("An array of tool names, operation types, and/or categories of tools that will be disabled."),
3✔
64
    confirmationRequiredTools: z4
3✔
65
        .preprocess((val: string | string[] | undefined) => commaSeparatedToArray(val), z4.array(z4.string()))
3✔
66
        .default([
3✔
67
            "atlas-create-access-list",
3✔
68
            "atlas-create-db-user",
3✔
69
            "drop-database",
3✔
70
            "drop-collection",
3✔
71
            "delete-many",
3✔
72
            "drop-index",
3✔
73
        ])
3✔
74
        .describe(
3✔
75
            "An array of tool names that require user confirmation before execution. Requires the client to support elicitation."
3✔
76
        ),
3✔
77
    readOnly: z4
3✔
78
        .boolean()
3✔
79
        .default(false)
3✔
80
        .describe(
3✔
81
            "When set to true, only allows read, connect, and metadata operation types, disabling create/update/delete operations."
3✔
82
        ),
3✔
83
    indexCheck: z4
3✔
84
        .boolean()
3✔
85
        .default(false)
3✔
86
        .describe(
3✔
87
            "When set to true, enforces that query operations must use an index, rejecting queries that perform a collection scan."
3✔
88
        ),
3✔
89
    telemetry: z4
3✔
90
        .enum(["enabled", "disabled"])
3✔
91
        .default("enabled")
3✔
92
        .describe("When set to disabled, disables telemetry collection."),
3✔
93
    transport: z4.enum(["stdio", "http"]).default("stdio").describe("Either 'stdio' or 'http'."),
3✔
94
    httpPort: z4.coerce
3✔
95
        .number()
3✔
96
        .int()
3✔
97
        .min(1, "Invalid httpPort: must be at least 1")
3✔
98
        .max(65535, "Invalid httpPort: must be at most 65535")
3✔
99
        .default(3000)
3✔
100
        .describe("Port number for the HTTP server (only used when transport is 'http')."),
3✔
101
    httpHost: z4
3✔
102
        .string()
3✔
103
        .default("127.0.0.1")
3✔
104
        .describe("Host address to bind the HTTP server to (only used when transport is 'http')."),
3✔
105
    httpHeaders: z4
3✔
106
        .object({})
3✔
107
        .passthrough()
3✔
108
        .default({})
3✔
109
        .describe(
3✔
110
            "Header that the HTTP server will validate when making requests (only used when transport is 'http')."
3✔
111
        ),
3✔
112
    idleTimeoutMs: z4.coerce
3✔
113
        .number()
3✔
114
        .default(600_000)
3✔
115
        .describe("Idle timeout for a client to disconnect (only applies to http transport)."),
3✔
116
    notificationTimeoutMs: z4.coerce
3✔
117
        .number()
3✔
118
        .default(540_000)
3✔
119
        .describe("Notification timeout for a client to be aware of disconnect (only applies to http transport)."),
3✔
120
    maxBytesPerQuery: z4.coerce
3✔
121
        .number()
3✔
122
        .default(16_777_216)
3✔
123
        .describe(
3✔
124
            "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✔
125
        ),
3✔
126
    maxDocumentsPerQuery: z4.coerce
3✔
127
        .number()
3✔
128
        .default(100)
3✔
129
        .describe(
3✔
130
            "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✔
131
        ),
3✔
132
    exportsPath: z4
3✔
133
        .string()
3✔
134
        .default(getExportsPath())
3✔
135
        .describe("Folder to store exported data files.")
3✔
136
        .register(configRegistry, { defaultValueDescription: "see below*" }),
3✔
137
    exportTimeoutMs: z4.coerce
3✔
138
        .number()
3✔
139
        .default(300_000)
3✔
140
        .describe("Time in milliseconds after which an export is considered expired and eligible for cleanup."),
3✔
141
    exportCleanupIntervalMs: z4.coerce
3✔
142
        .number()
3✔
143
        .default(120_000)
3✔
144
        .describe("Time in milliseconds between export cleanup cycles that remove expired export files."),
3✔
145
    atlasTemporaryDatabaseUserLifetimeMs: z4.coerce
3✔
146
        .number()
3✔
147
        .default(14_400_000)
3✔
148
        .describe(
3✔
149
            "Time in milliseconds that temporary database users created when connecting to MongoDB Atlas clusters will remain active before being automatically deleted."
3✔
150
        ),
3✔
151
    voyageApiKey: z4
3✔
152
        .string()
3✔
153
        .default("")
3✔
154
        .describe(
3✔
155
            "API key for Voyage AI embeddings service (required for vector search operations with text-to-embedding conversion)."
3✔
156
        )
3✔
157
        .register(configRegistry, { isSecret: true }),
3✔
158
    disableEmbeddingsValidation: z4
3✔
159
        .boolean()
3✔
160
        .default(false)
3✔
161
        .describe("When set to true, disables validation of embeddings dimensions."),
3✔
162
    vectorSearchDimensions: z4.coerce
3✔
163
        .number()
3✔
164
        .default(1024)
3✔
165
        .describe("Default number of dimensions for vector search embeddings."),
3✔
166
    vectorSearchSimilarityFunction: z4
3✔
167
        .enum(similarityValues)
3✔
168
        .default("euclidean")
3✔
169
        .describe("Default similarity function for vector search: 'euclidean', 'cosine', or 'dotProduct'."),
3✔
170
    previewFeatures: z4
3✔
171
        .preprocess(
3✔
172
            (val: string | string[] | undefined) => commaSeparatedToArray(val),
3✔
173
            z4.array(z4.enum(previewFeatureValues))
3✔
174
        )
3✔
175
        .default([])
3✔
176
        .describe("An array of preview features that are enabled."),
3✔
177
});
3✔
178

179
export type UserConfig = z4.infer<typeof UserConfigSchema> & CliOptions;
180

181
export const config = setupUserConfig({
3✔
182
    cli: process.argv,
3✔
183
    env: process.env,
3✔
184
});
3✔
185

186
export type DriverOptions = ConnectionInfo["driverOptions"];
187
export const defaultDriverOptions: DriverOptions = {
3✔
188
    readConcern: {
3✔
189
        level: "local",
3✔
190
    },
3✔
191
    readPreference: "secondaryPreferred",
3✔
192
    writeConcern: {
3✔
193
        w: "majority",
3✔
194
    },
3✔
195
    timeoutMS: 30_000,
3✔
196
    proxy: { useEnvironmentVariableProxies: true },
3✔
197
    applyProxyToOIDC: true,
3✔
198
};
3✔
199

200
// Gets the config supplied by the user as environment variables. The variable names
201
// are prefixed with `MDB_MCP_` and the keys match the UserConfig keys, but are converted
202
// to SNAKE_UPPER_CASE.
203
function parseEnvConfig(env: Record<string, unknown>): Partial<UserConfig> {
154✔
204
    const CONFIG_WITH_URLS: Set<string> = new Set<(typeof OPTIONS)["string"][number]>(["connectionString"]);
154✔
205

206
    function setValue(
154✔
207
        obj: Record<string, string | string[] | boolean | number | Record<string, unknown> | undefined>,
37✔
208
        path: string[],
37✔
209
        value: string
37✔
210
    ): void {
37✔
211
        const currentField = path.shift();
37✔
212
        if (!currentField) {
37!
213
            return;
×
214
        }
×
215
        if (path.length === 0) {
37✔
216
            if (CONFIG_WITH_URLS.has(currentField)) {
37!
217
                obj[currentField] = value;
4✔
218
                return;
4✔
219
            }
4✔
220

221
            const numberValue = Number(value);
33✔
222
            if (!isNaN(numberValue)) {
37!
223
                obj[currentField] = numberValue;
4✔
224
                return;
4✔
225
            }
4✔
226

227
            const booleanValue = value.toLocaleLowerCase();
29✔
228
            if (booleanValue === "true" || booleanValue === "false") {
37!
229
                obj[currentField] = booleanValue === "true";
2✔
230
                return;
2✔
231
            }
2✔
232

233
            // Try to parse an array of values
234
            if (value.indexOf(",") !== -1) {
37!
235
                obj[currentField] = value.split(",").map((v) => v.trim());
2✔
236
                return;
2✔
237
            }
2✔
238

239
            obj[currentField] = value;
25✔
240
            return;
25✔
241
        }
25!
242

243
        if (!obj[currentField]) {
×
244
            obj[currentField] = {};
×
245
        }
×
246

247
        setValue(obj[currentField] as Record<string, string | string[] | boolean | number | undefined>, path, value);
×
248
    }
37✔
249

250
    const result: Record<string, string | string[] | boolean | number | undefined> = {};
154✔
251
    const mcpVariables = Object.entries(env).filter(
154✔
252
        ([key, value]) => value !== undefined && key.startsWith("MDB_MCP_")
154✔
253
    ) as [string, string][];
154✔
254
    for (const [key, value] of mcpVariables) {
154✔
255
        const fieldPath = key
37✔
256
            .replace("MDB_MCP_", "")
37✔
257
            .split(".")
37✔
258
            .map((part) => SNAKE_CASE_toCamelCase(part));
37✔
259

260
        setValue(result, fieldPath, value);
37✔
261
    }
37✔
262

263
    return result;
154✔
264
}
154✔
265

266
function SNAKE_CASE_toCamelCase(str: string): string {
37✔
267
    return str.toLowerCase().replace(/([-_][a-z])/g, (group) => group.toUpperCase().replace("_", ""));
37✔
268
}
37✔
269

270
// Right now we have arguments that are not compatible with the format used in mongosh.
271
// An example is using --connectionString and positional arguments.
272
// We will consolidate them in a way where the mongosh format takes precedence.
273
// We will warn users that previous configuration is deprecated in favour of
274
// whatever is in mongosh.
275
function parseCliConfig(args: string[]): Partial<Record<keyof CliOptions, string | number | undefined>> {
154✔
276
    const programArgs = args.slice(2);
154✔
277
    const parsed = argv(programArgs, OPTIONS as unknown as argv.Options) as unknown as Record<
154✔
278
        keyof CliOptions,
279
        string | number | undefined
280
    > & {
281
        _?: string[];
282
    };
283

284
    const positionalArguments = parsed._ ?? [];
154!
285

286
    // we use console.warn here because we still don't have our logging system configured
287
    // so we don't have a logger. For stdio, the warning will be received as a string in
288
    // the client and IDEs like VSCode do show the message in the log window. For HTTP,
289
    // it will be in the stdout of the process.
290
    warnAboutDeprecatedOrUnknownCliArgs(
154✔
291
        { ...parsed, _: positionalArguments },
154✔
292
        {
154✔
293
            warn: (msg) => console.warn(msg),
154✔
294
            exit: (status) => process.exit(status),
154✔
295
        }
154✔
296
    );
154✔
297

298
    // if we have a positional argument that matches a connection string
299
    // store it as the connection specifier and remove it from the argument
300
    // list, so it doesn't get misunderstood by the mongosh args-parser
301
    if (!parsed.nodb && isConnectionSpecifier(positionalArguments[0])) {
154!
302
        parsed.connectionSpecifier = positionalArguments.shift();
1✔
303
    }
1✔
304

305
    delete parsed._;
154✔
306
    return parsed;
154✔
307
}
154✔
308

309
export function warnAboutDeprecatedOrUnknownCliArgs(
3✔
310
    args: Record<string, unknown>,
161✔
311
    { warn, exit }: { warn: (msg: string) => void; exit: (status: number) => void | never }
161✔
312
): void {
161✔
313
    let usedDeprecatedArgument = false;
161✔
314
    let usedInvalidArgument = false;
161✔
315

316
    const knownArgs = args as unknown as UserConfig & CliOptions;
161✔
317
    // the first position argument should be used
318
    // instead of --connectionString, as it's how the mongosh works.
319
    if (knownArgs.connectionString) {
161!
320
        usedDeprecatedArgument = true;
8✔
321
        warn(
8✔
322
            "Warning: The --connectionString argument is deprecated. Prefer using the MDB_MCP_CONNECTION_STRING environment variable or the first positional argument for the connection string."
8✔
323
        );
8✔
324
    }
8✔
325

326
    for (const providedKey of Object.keys(args)) {
161✔
327
        if (providedKey === "_") {
255✔
328
            // positional argument
329
            continue;
154✔
330
        }
154!
331

332
        const { valid, suggestion } = validateConfigKey(providedKey);
101✔
333
        if (!valid) {
206✔
334
            usedInvalidArgument = true;
4✔
335
            if (suggestion) {
4✔
336
                warn(`Warning: Invalid command line argument '${providedKey}'. Did you mean '${suggestion}'?`);
2✔
337
            } else {
2✔
338
                warn(`Warning: Invalid command line argument '${providedKey}'.`);
2✔
339
            }
2✔
340
        }
4✔
341
    }
255✔
342

343
    if (usedInvalidArgument || usedDeprecatedArgument) {
161!
344
        warn("- Refer to https://www.mongodb.com/docs/mcp-server/get-started/ for setting up the MCP Server.");
12✔
345
    }
12✔
346

347
    if (usedInvalidArgument) {
161!
348
        exit(1);
4✔
349
    }
4✔
350
}
161✔
351

352
export function registerKnownSecretsInRootKeychain(userConfig: Partial<UserConfig>): void {
3✔
353
    const keychain = Keychain.root;
156✔
354

355
    const maybeRegister = (value: string | undefined, kind: Secret["kind"]): void => {
156✔
356
        if (value) {
1,872!
357
            keychain.register(value, kind);
36✔
358
        }
36✔
359
    };
1,872✔
360

361
    maybeRegister(userConfig.apiClientId, "user");
156✔
362
    maybeRegister(userConfig.apiClientSecret, "password");
156✔
363
    maybeRegister(userConfig.awsAccessKeyId, "password");
156✔
364
    maybeRegister(userConfig.awsIamSessionToken, "password");
156✔
365
    maybeRegister(userConfig.awsSecretAccessKey, "password");
156✔
366
    maybeRegister(userConfig.awsSessionToken, "password");
156✔
367
    maybeRegister(userConfig.password, "password");
156✔
368
    maybeRegister(userConfig.tlsCAFile, "url");
156✔
369
    maybeRegister(userConfig.tlsCRLFile, "url");
156✔
370
    maybeRegister(userConfig.tlsCertificateKeyFile, "url");
156✔
371
    maybeRegister(userConfig.tlsCertificateKeyFilePassword, "password");
156✔
372
    maybeRegister(userConfig.username, "user");
156✔
373
}
156✔
374

375
function warnIfVectorSearchNotEnabledCorrectly(config: UserConfig): void {
144✔
376
    const vectorSearchEnabled = config.previewFeatures.includes("vectorSearch");
144✔
377
    const embeddingsProviderConfigured = !!config.voyageApiKey;
144✔
378
    if (vectorSearchEnabled && !embeddingsProviderConfigured) {
144!
NEW
379
        console.warn(`\
×
380
Warning: Vector search is enabled but no embeddings provider is configured.
381
- Set an embeddings provider configuration option to enable auto-embeddings during document insertion and text-based queries with $vectorSearch.\
NEW
382
`);
×
NEW
383
    }
×
384

385
    if (!vectorSearchEnabled && embeddingsProviderConfigured) {
144!
NEW
386
        console.warn(`\
×
387
Warning: An embeddings provider is configured but the 'vectorSearch' preview feature is not enabled.
388
- Enable vector search by adding 'vectorSearch' to the 'previewFeatures' configuration option, or remove the embeddings provider configuration if not needed.\
NEW
389
`);
×
NEW
390
    }
×
391
}
144✔
392

393
export function setupUserConfig({ cli, env }: { cli: string[]; env: Record<string, unknown> }): UserConfig {
3✔
394
    const rawConfig = {
154✔
395
        ...parseEnvConfig(env),
154✔
396
        ...parseCliConfig(cli),
154✔
397
    };
154✔
398

399
    if (rawConfig.connectionString && rawConfig.connectionSpecifier) {
154!
400
        const connectionInfo = generateConnectionInfoFromCliArgs(rawConfig as UserConfig);
1✔
401
        rawConfig.connectionString = connectionInfo.connectionString;
1✔
402
    }
1✔
403

404
    const parseResult = UserConfigSchema.safeParse(rawConfig);
154✔
405
    if (parseResult.error) {
154!
406
        throw new Error(
10✔
407
            `Invalid configuration for the following fields:\n${parseResult.error.issues.map((issue) => `${issue.path.join(".")} - ${issue.message}`).join("\n")}`
10✔
408
        );
10✔
409
    }
10✔
410
    // We don't have as schema defined for all args-parser arguments so we need to merge the raw config with the parsed config.
411
    const userConfig = { ...rawConfig, ...parseResult.data } as UserConfig;
144✔
412

413
    warnIfVectorSearchNotEnabledCorrectly(userConfig);
144✔
414
    registerKnownSecretsInRootKeychain(userConfig);
144✔
415
    return userConfig;
144✔
416
}
144✔
417

418
export function setupDriverConfig({
3✔
419
    config,
56✔
420
    defaults,
56✔
421
}: {
56✔
422
    config: UserConfig;
423
    defaults: Partial<DriverOptions>;
424
}): DriverOptions {
56✔
425
    const { driverOptions } = generateConnectionInfoFromCliArgs(config);
56✔
426
    return {
56✔
427
        ...defaults,
56✔
428
        ...driverOptions,
56✔
429
    };
56✔
430
}
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