From 784affd58f65ec044d3a11daa51b9364635973a5 Mon Sep 17 00:00:00 2001 From: Kevin Mas Ruiz Date: Thu, 4 Sep 2025 16:00:26 +0200 Subject: [PATCH 1/8] feat: notify when a flag is wrong and suggest a fix --- package-lock.json | 35 ++++++++++++++-- package.json | 2 + src/common/config.ts | 70 ++++++++++++++++++++++++++++++-- tests/unit/common/config.test.ts | 56 +++++++++++++++++++++++-- 4 files changed, 152 insertions(+), 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1385e8ca..41bf3657 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "@vitest/eslint-plugin": "^1.3.4", "bson": "^6.10.4", "express": "^5.1.0", + "fast-levenshtein": "^3.0.0", "lru-cache": "^11.1.0", "mongodb": "^6.19.0", "mongodb-connection-string-url": "^3.0.2", @@ -43,6 +44,7 @@ "@mongodb-js/oidc-mock-provider": "^0.11.3", "@redocly/cli": "^2.0.8", "@types/express": "^5.0.3", + "@types/fast-levenshtein": "^0.0.4", "@types/http-proxy": "^1.17.16", "@types/node": "^24.3.0", "@types/proper-lockfile": "^4.1.4", @@ -5383,6 +5385,13 @@ "@types/send": "*" } }, + "node_modules/@types/fast-levenshtein": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@types/fast-levenshtein/-/fast-levenshtein-0.0.4.tgz", + "integrity": "sha512-tkDveuitddQCxut1Db8eEFfMahTjOumTJGPHmT9E7KUH+DkVq9WTpVvlfenf3S+uCBeu8j5FP2xik/KfxOEjeA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/http-errors": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", @@ -8408,10 +8417,13 @@ "license": "MIT" }, "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "license": "MIT" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-3.0.0.tgz", + "integrity": "sha512-hKKNajm46uNmTlhHSyZkmToAc56uZJwYq7yrciZjqOxnlfQwERDQJmHPUp7m1m9wx8vgOe8IaCKZ5Kv2k1DdCQ==", + "license": "MIT", + "dependencies": { + "fastest-levenshtein": "^1.0.7" + } }, "node_modules/fast-safe-stringify": { "version": "2.1.1", @@ -8456,6 +8468,15 @@ "fxparser": "src/cli/cli.js" } }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "license": "MIT", + "engines": { + "node": ">= 4.9.1" + } + }, "node_modules/fastq": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", @@ -11412,6 +11433,12 @@ "node": ">= 0.8.0" } }, + "node_modules/optionator/node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "license": "MIT" + }, "node_modules/os-dns-native": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/os-dns-native/-/os-dns-native-1.2.1.tgz", diff --git a/package.json b/package.json index cc7e68cc..29c1973f 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "@mongodb-js/oidc-mock-provider": "^0.11.3", "@redocly/cli": "^2.0.8", "@types/express": "^5.0.3", + "@types/fast-levenshtein": "^0.0.4", "@types/http-proxy": "^1.17.16", "@types/node": "^24.3.0", "@types/proper-lockfile": "^4.1.4", @@ -104,6 +105,7 @@ "@vitest/eslint-plugin": "^1.3.4", "bson": "^6.10.4", "express": "^5.1.0", + "fast-levenshtein": "^3.0.0", "lru-cache": "^11.1.0", "mongodb": "^6.19.0", "mongodb-connection-string-url": "^3.0.2", diff --git a/src/common/config.ts b/src/common/config.ts index 9fa78ec0..2732685f 100644 --- a/src/common/config.ts +++ b/src/common/config.ts @@ -5,6 +5,7 @@ import type { CliOptions, ConnectionInfo } from "@mongosh/arg-parser"; import { generateConnectionInfoFromCliArgs } from "@mongosh/arg-parser"; import { Keychain } from "./keychain.js"; import type { Secret } from "./keychain.js"; +import { get as levenshtein } from "fast-levenshtein"; // From: https://github.com/mongodb-js/mongosh/blob/main/packages/cli-repl/src/arg-parser.ts const OPTIONS = { @@ -91,6 +92,44 @@ const OPTIONS = { }, } as const; +const ALL_CONFIG_KEYS = new Set( + (OPTIONS.string as readonly string[]) + .concat(OPTIONS.array) + .concat(OPTIONS.boolean) + .concat(Object.keys(OPTIONS.alias)) +) as Set; + +export function validateConfigKey(key: string): { valid: boolean; suggestion?: string } { + if (ALL_CONFIG_KEYS.has(key)) { + return { valid: true }; + } + + // find the closest match for a suggestion + let minLev = Number.MAX_VALUE; + let suggestion = ""; + + for (const validKey of ALL_CONFIG_KEYS) { + // check if there is an exact case-insensitive match + if (validKey.toLowerCase() === key.toLowerCase()) { + return { valid: false, suggestion: validKey }; + } + + // else, infer something using levenhstein so we suggest a valid key + const lev = levenshtein(key, validKey); + if (lev < minLev) { + minLev = lev; + suggestion = validKey; + } + } + + if (minLev <= 2) { + // accept up to 2 typos + return { valid: false, suggestion }; + } + + return { valid: false }; +} + function isConnectionSpecifier(arg: string | undefined): boolean { return ( arg !== undefined && @@ -249,6 +288,10 @@ function SNAKE_CASE_toCamelCase(str: string): string { return str.toLowerCase().replace(/([-_][a-z])/g, (group) => group.toUpperCase().replace("_", "")); } +function camelCaseTo_SNAKE_CASE(str: string): string { + return str.replace(/([a-z])([A-Z])/g, "$1_$2").toUpperCase(); +} + // Right now we have arguments that are not compatible with the format used in mongosh. // An example is using --connectionString and positional arguments. // We will consolidate them in a way where the mongosh format takes precedence. @@ -267,7 +310,7 @@ function parseCliConfig(args: string[]): CliOptions { // so we don't have a logger. For stdio, the warning will be received as a string in // the client and IDEs like VSCode do show the message in the log window. For HTTP, // it will be in the stdout of the process. - warnAboutDeprecatedCliArgs({ ...parsed, _: positionalArguments }, console.warn); + warnAboutDeprecatedOrUnknownCliArgs({ ...parsed, _: positionalArguments }, console.warn); // if we have a positional argument that matches a connection string // store it as the connection specifier and remove it from the argument @@ -280,14 +323,16 @@ function parseCliConfig(args: string[]): CliOptions { return parsed; } -export function warnAboutDeprecatedCliArgs( +export function warnAboutDeprecatedOrUnknownCliArgs( args: CliOptions & UserConfig & { _?: string[]; - }, + } & any, warn: (msg: string) => void ): void { let usedDeprecatedArgument = false; + let usedInvalidArgument = false; + // the first position argument should be used // instead of --connectionString, as it's how the mongosh works. if (args.connectionString) { @@ -297,7 +342,24 @@ export function warnAboutDeprecatedCliArgs( ); } - if (usedDeprecatedArgument) { + for (const providedKey of Object.keys(args)) { + if (providedKey === "_") { + // positional argument + continue; + } + + const { valid, suggestion } = validateConfigKey(providedKey); + if (!valid) { + usedInvalidArgument = true; + if (suggestion) { + warn(`Invalid command line argument '${providedKey}'. Did you mean '${suggestion}'?`); + } else { + warn(`Invalid command line argument '${providedKey}'.`); + } + } + } + + if (usedInvalidArgument || usedDeprecatedArgument) { warn("Refer to https://www.mongodb.com/docs/mcp-server/get-started/ for setting up the MCP Server."); } } diff --git a/tests/unit/common/config.test.ts b/tests/unit/common/config.test.ts index f325461d..c02c89b6 100644 --- a/tests/unit/common/config.test.ts +++ b/tests/unit/common/config.test.ts @@ -3,8 +3,8 @@ import type { UserConfig } from "../../../src/common/config.js"; import { setupUserConfig, defaultUserConfig, - warnAboutDeprecatedCliArgs, registerKnownSecretsInRootKeychain, + warnAboutDeprecatedOrUnknownCliArgs, } from "../../../src/common/config.js"; import type { CliOptions } from "@mongosh/arg-parser"; import { Keychain } from "../../../src/common/keychain.js"; @@ -638,7 +638,7 @@ describe("config", () => { }); }); -describe("Deprecated CLI arguments", () => { +describe("CLI arguments", () => { const referDocMessage = "Refer to https://www.mongodb.com/docs/mcp-server/get-started/ for setting up the MCP Server."; @@ -660,7 +660,7 @@ describe("Deprecated CLI arguments", () => { cliArgs = { [cliArg]: "RandomString" } as unknown as CliOptions & UserConfig & { _?: string[] }; warn = vi.fn(); - warnAboutDeprecatedCliArgs(cliArgs, warn); + warnAboutDeprecatedOrUnknownCliArgs(cliArgs, warn); }); it(`warns the usage of ${cliArg} as it is deprecated`, () => { @@ -673,6 +673,56 @@ describe("Deprecated CLI arguments", () => { }); } + describe("invalid arguments", () => { + let warn: (msg: string) => void; + + beforeEach(() => { + warn = vi.fn(); + }); + + it("should show a warning when an argument is not known", () => { + warnAboutDeprecatedOrUnknownCliArgs( + { + wakanda: "", + }, + warn + ); + + expect(warn).toHaveBeenCalledWith("Invalid command line argument 'wakanda'."); + expect(warn).toHaveBeenCalledWith( + "Refer to https://www.mongodb.com/docs/mcp-server/get-started/ for setting up the MCP Server." + ); + }); + + it("should show a suggestion when is a simple typo", () => { + warnAboutDeprecatedOrUnknownCliArgs( + { + readonli: "", + }, + warn + ); + + expect(warn).toHaveBeenCalledWith("Invalid command line argument 'readonli'. Did you mean 'readOnly'?"); + expect(warn).toHaveBeenCalledWith( + "Refer to https://www.mongodb.com/docs/mcp-server/get-started/ for setting up the MCP Server." + ); + }); + + it("should show a suggestion when the only change is on the case", () => { + warnAboutDeprecatedOrUnknownCliArgs( + { + readonly: "", + }, + warn + ); + + expect(warn).toHaveBeenCalledWith("Invalid command line argument 'readonly'. Did you mean 'readOnly'?"); + expect(warn).toHaveBeenCalledWith( + "Refer to https://www.mongodb.com/docs/mcp-server/get-started/ for setting up the MCP Server." + ); + }); + }); + describe("keychain management", () => { type TestCase = { readonly cliArg: keyof UserConfig; secretKind: Secret["kind"] }; const testCases = [ From 66c8d2f8e6575db95862d69b99064579ecf7f175 Mon Sep 17 00:00:00 2001 From: Kevin Mas Ruiz Date: Thu, 4 Sep 2025 16:03:42 +0200 Subject: [PATCH 2/8] chore: remove unused code --- src/common/config.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/common/config.ts b/src/common/config.ts index 2732685f..1d2c6d05 100644 --- a/src/common/config.ts +++ b/src/common/config.ts @@ -104,10 +104,10 @@ export function validateConfigKey(key: string): { valid: boolean; suggestion?: s return { valid: true }; } - // find the closest match for a suggestion let minLev = Number.MAX_VALUE; let suggestion = ""; + // find the closest match for a suggestion for (const validKey of ALL_CONFIG_KEYS) { // check if there is an exact case-insensitive match if (validKey.toLowerCase() === key.toLowerCase()) { @@ -288,10 +288,6 @@ function SNAKE_CASE_toCamelCase(str: string): string { return str.toLowerCase().replace(/([-_][a-z])/g, (group) => group.toUpperCase().replace("_", "")); } -function camelCaseTo_SNAKE_CASE(str: string): string { - return str.replace(/([a-z])([A-Z])/g, "$1_$2").toUpperCase(); -} - // Right now we have arguments that are not compatible with the format used in mongosh. // An example is using --connectionString and positional arguments. // We will consolidate them in a way where the mongosh format takes precedence. From a57376fbacf5fa9f0006558d7519431f8a2b1389 Mon Sep 17 00:00:00 2001 From: Kevin Mas Ruiz Date: Thu, 4 Sep 2025 16:08:10 +0200 Subject: [PATCH 3/8] chore: fix linter complains --- src/common/config.ts | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/common/config.ts b/src/common/config.ts index 1d2c6d05..76f686f0 100644 --- a/src/common/config.ts +++ b/src/common/config.ts @@ -97,7 +97,7 @@ const ALL_CONFIG_KEYS = new Set( .concat(OPTIONS.array) .concat(OPTIONS.boolean) .concat(Object.keys(OPTIONS.alias)) -) as Set; +); export function validateConfigKey(key: string): { valid: boolean; suggestion?: string } { if (ALL_CONFIG_KEYS.has(key)) { @@ -319,19 +319,14 @@ function parseCliConfig(args: string[]): CliOptions { return parsed; } -export function warnAboutDeprecatedOrUnknownCliArgs( - args: CliOptions & - UserConfig & { - _?: string[]; - } & any, - warn: (msg: string) => void -): void { +export function warnAboutDeprecatedOrUnknownCliArgs(args: object, warn: (msg: string) => void): void { let usedDeprecatedArgument = false; let usedInvalidArgument = false; + const knownArgs = args as UserConfig & CliOptions; // the first position argument should be used // instead of --connectionString, as it's how the mongosh works. - if (args.connectionString) { + if (knownArgs.connectionString) { usedDeprecatedArgument = true; warn( "The --connectionString argument is deprecated. Prefer using the first positional argument for the connection string or the MDB_MCP_CONNECTION_STRING environment variable." From e09582376c971e6c5e0b19e54456b3e67fff5703 Mon Sep 17 00:00:00 2001 From: Kevin Mas Ruiz Date: Thu, 4 Sep 2025 16:38:24 +0200 Subject: [PATCH 4/8] chore: fix levenshtein dependency --- package-lock.json | 34 +++++++--------------------------- package.json | 3 +-- src/common/config.ts | 4 ++-- tsconfig.build.json | 3 ++- 4 files changed, 12 insertions(+), 32 deletions(-) diff --git a/package-lock.json b/package-lock.json index 41bf3657..290e9600 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,6 @@ "@vitest/eslint-plugin": "^1.3.4", "bson": "^6.10.4", "express": "^5.1.0", - "fast-levenshtein": "^3.0.0", "lru-cache": "^11.1.0", "mongodb": "^6.19.0", "mongodb-connection-string-url": "^3.0.2", @@ -29,6 +28,7 @@ "node-machine-id": "1.1.12", "oauth4webapi": "^3.8.0", "openapi-fetch": "^0.14.0", + "ts-levenshtein": "^1.0.7", "yargs-parser": "^22.0.0", "zod": "^3.25.76" }, @@ -44,7 +44,6 @@ "@mongodb-js/oidc-mock-provider": "^0.11.3", "@redocly/cli": "^2.0.8", "@types/express": "^5.0.3", - "@types/fast-levenshtein": "^0.0.4", "@types/http-proxy": "^1.17.16", "@types/node": "^24.3.0", "@types/proper-lockfile": "^4.1.4", @@ -5385,13 +5384,6 @@ "@types/send": "*" } }, - "node_modules/@types/fast-levenshtein": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/@types/fast-levenshtein/-/fast-levenshtein-0.0.4.tgz", - "integrity": "sha512-tkDveuitddQCxut1Db8eEFfMahTjOumTJGPHmT9E7KUH+DkVq9WTpVvlfenf3S+uCBeu8j5FP2xik/KfxOEjeA==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/http-errors": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", @@ -8416,15 +8408,6 @@ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "license": "MIT" }, - "node_modules/fast-levenshtein": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-3.0.0.tgz", - "integrity": "sha512-hKKNajm46uNmTlhHSyZkmToAc56uZJwYq7yrciZjqOxnlfQwERDQJmHPUp7m1m9wx8vgOe8IaCKZ5Kv2k1DdCQ==", - "license": "MIT", - "dependencies": { - "fastest-levenshtein": "^1.0.7" - } - }, "node_modules/fast-safe-stringify": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", @@ -8468,15 +8451,6 @@ "fxparser": "src/cli/cli.js" } }, - "node_modules/fastest-levenshtein": { - "version": "1.0.16", - "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", - "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", - "license": "MIT", - "engines": { - "node": ">= 4.9.1" - } - }, "node_modules/fastq": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", @@ -14370,6 +14344,12 @@ "typescript": ">=4.8.4" } }, + "node_modules/ts-levenshtein": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/ts-levenshtein/-/ts-levenshtein-1.0.7.tgz", + "integrity": "sha512-wautEf7gl2ITJuRTTYxnlrLjzUUcwFSdg46bcu4RlzoE/zQM++TJjBFRf2Xhil49GiHqKCqmpjf1lBkWnAHj0A==", + "license": "MIT" + }, "node_modules/ts-node": { "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", diff --git a/package.json b/package.json index 29c1973f..7a9fc15f 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,6 @@ "@mongodb-js/oidc-mock-provider": "^0.11.3", "@redocly/cli": "^2.0.8", "@types/express": "^5.0.3", - "@types/fast-levenshtein": "^0.0.4", "@types/http-proxy": "^1.17.16", "@types/node": "^24.3.0", "@types/proper-lockfile": "^4.1.4", @@ -105,7 +104,6 @@ "@vitest/eslint-plugin": "^1.3.4", "bson": "^6.10.4", "express": "^5.1.0", - "fast-levenshtein": "^3.0.0", "lru-cache": "^11.1.0", "mongodb": "^6.19.0", "mongodb-connection-string-url": "^3.0.2", @@ -116,6 +114,7 @@ "node-machine-id": "1.1.12", "oauth4webapi": "^3.8.0", "openapi-fetch": "^0.14.0", + "ts-levenshtein": "^1.0.7", "yargs-parser": "^22.0.0", "zod": "^3.25.76" }, diff --git a/src/common/config.ts b/src/common/config.ts index 76f686f0..565e1356 100644 --- a/src/common/config.ts +++ b/src/common/config.ts @@ -5,7 +5,7 @@ import type { CliOptions, ConnectionInfo } from "@mongosh/arg-parser"; import { generateConnectionInfoFromCliArgs } from "@mongosh/arg-parser"; import { Keychain } from "./keychain.js"; import type { Secret } from "./keychain.js"; -import { get as levenshtein } from "fast-levenshtein"; +import levenshtein from "ts-levenshtein"; // From: https://github.com/mongodb-js/mongosh/blob/main/packages/cli-repl/src/arg-parser.ts const OPTIONS = { @@ -115,7 +115,7 @@ export function validateConfigKey(key: string): { valid: boolean; suggestion?: s } // else, infer something using levenhstein so we suggest a valid key - const lev = levenshtein(key, validKey); + const lev = levenshtein.get(key, validKey); if (lev < minLev) { minLev = lev; suggestion = validKey; diff --git a/tsconfig.build.json b/tsconfig.build.json index aa40521b..06089861 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -21,7 +21,8 @@ "paths": { "mongodb-connection-string-url": [ "./node_modules/mongodb-connection-string-url/lib/index.d.ts" - ] + ], + "ts-levenshtein": ["./node_modules/ts-levenshtein/dist/index.d.mts"] } }, "include": ["src/**/*.ts"] From 42a042d8987e9c6606312a46aa9759055a5eac41 Mon Sep 17 00:00:00 2001 From: Kevin Mas Ruiz Date: Thu, 4 Sep 2025 16:41:07 +0200 Subject: [PATCH 5/8] chore: use record instead of object --- src/common/config.ts | 4 ++-- tests/unit/common/config.test.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/common/config.ts b/src/common/config.ts index 565e1356..e5511a4b 100644 --- a/src/common/config.ts +++ b/src/common/config.ts @@ -319,11 +319,11 @@ function parseCliConfig(args: string[]): CliOptions { return parsed; } -export function warnAboutDeprecatedOrUnknownCliArgs(args: object, warn: (msg: string) => void): void { +export function warnAboutDeprecatedOrUnknownCliArgs(args: Record, warn: (msg: string) => void): void { let usedDeprecatedArgument = false; let usedInvalidArgument = false; - const knownArgs = args as UserConfig & CliOptions; + const knownArgs = args as unknown as UserConfig & CliOptions; // the first position argument should be used // instead of --connectionString, as it's how the mongosh works. if (knownArgs.connectionString) { diff --git a/tests/unit/common/config.test.ts b/tests/unit/common/config.test.ts index c02c89b6..b4a4bb91 100644 --- a/tests/unit/common/config.test.ts +++ b/tests/unit/common/config.test.ts @@ -660,7 +660,7 @@ describe("CLI arguments", () => { cliArgs = { [cliArg]: "RandomString" } as unknown as CliOptions & UserConfig & { _?: string[] }; warn = vi.fn(); - warnAboutDeprecatedOrUnknownCliArgs(cliArgs, warn); + warnAboutDeprecatedOrUnknownCliArgs(cliArgs as unknown as Record, warn); }); it(`warns the usage of ${cliArg} as it is deprecated`, () => { From d46b44b98304260fd02f4a1353475e8124d1b2dc Mon Sep 17 00:00:00 2001 From: Kevin Mas Ruiz Date: Thu, 4 Sep 2025 16:55:33 +0200 Subject: [PATCH 6/8] Update src/common/config.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/common/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/config.ts b/src/common/config.ts index e5511a4b..9a0f69b8 100644 --- a/src/common/config.ts +++ b/src/common/config.ts @@ -114,7 +114,7 @@ export function validateConfigKey(key: string): { valid: boolean; suggestion?: s return { valid: false, suggestion: validKey }; } - // else, infer something using levenhstein so we suggest a valid key + // else, infer something using levenshtein so we suggest a valid key const lev = levenshtein.get(key, validKey); if (lev < minLev) { minLev = lev; From 46ec49f81bd809c3e9dd4d2e6ba2a07e3da90f8b Mon Sep 17 00:00:00 2001 From: Kevin Mas Ruiz Date: Fri, 5 Sep 2025 10:00:25 +0200 Subject: [PATCH 7/8] chore: exit with an error on unknown cli arguments --- src/common/config.ts | 17 +++++++++++++++-- tests/unit/common/config.test.ts | 27 +++++++++++++++++++++++---- 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/src/common/config.ts b/src/common/config.ts index 9a0f69b8..ed9abb12 100644 --- a/src/common/config.ts +++ b/src/common/config.ts @@ -306,7 +306,13 @@ function parseCliConfig(args: string[]): CliOptions { // so we don't have a logger. For stdio, the warning will be received as a string in // the client and IDEs like VSCode do show the message in the log window. For HTTP, // it will be in the stdout of the process. - warnAboutDeprecatedOrUnknownCliArgs({ ...parsed, _: positionalArguments }, console.warn); + warnAboutDeprecatedOrUnknownCliArgs( + { ...parsed, _: positionalArguments }, + { + warn: console.warn, + exit: process.exit, + } + ); // if we have a positional argument that matches a connection string // store it as the connection specifier and remove it from the argument @@ -319,7 +325,10 @@ function parseCliConfig(args: string[]): CliOptions { return parsed; } -export function warnAboutDeprecatedOrUnknownCliArgs(args: Record, warn: (msg: string) => void): void { +export function warnAboutDeprecatedOrUnknownCliArgs( + args: Record, + { warn, exit }: { warn: (msg: string) => void; exit: (status: number) => void | never } +): void { let usedDeprecatedArgument = false; let usedInvalidArgument = false; @@ -353,6 +362,10 @@ export function warnAboutDeprecatedOrUnknownCliArgs(args: Record(str: string | string[] | undefined): T { diff --git a/tests/unit/common/config.test.ts b/tests/unit/common/config.test.ts index b4a4bb91..a497cb33 100644 --- a/tests/unit/common/config.test.ts +++ b/tests/unit/common/config.test.ts @@ -655,12 +655,14 @@ describe("CLI arguments", () => { describe(`deprecation behaviour of ${cliArg}`, () => { let cliArgs: CliOptions & UserConfig & { _?: string[] }; let warn: (msg: string) => void; + let exit: (status: number) => void | never; beforeEach(() => { cliArgs = { [cliArg]: "RandomString" } as unknown as CliOptions & UserConfig & { _?: string[] }; warn = vi.fn(); + exit = vi.fn(); - warnAboutDeprecatedOrUnknownCliArgs(cliArgs as unknown as Record, warn); + warnAboutDeprecatedOrUnknownCliArgs(cliArgs as unknown as Record, { warn, exit }); }); it(`warns the usage of ${cliArg} as it is deprecated`, () => { @@ -670,14 +672,20 @@ describe("CLI arguments", () => { it(`shows the reference message when ${cliArg} was passed`, () => { expect(warn).toHaveBeenCalledWith(referDocMessage); }); + + it(`should not exit the process`, () => { + expect(exit).not.toHaveBeenCalled(); + }); }); } describe("invalid arguments", () => { let warn: (msg: string) => void; + let exit: (status: number) => void | never; beforeEach(() => { warn = vi.fn(); + exit = vi.fn(); }); it("should show a warning when an argument is not known", () => { @@ -685,7 +693,7 @@ describe("CLI arguments", () => { { wakanda: "", }, - warn + { warn, exit } ); expect(warn).toHaveBeenCalledWith("Invalid command line argument 'wakanda'."); @@ -694,12 +702,23 @@ describe("CLI arguments", () => { ); }); + it("should exit the process on unknown cli args", () => { + warnAboutDeprecatedOrUnknownCliArgs( + { + wakanda: "", + }, + { warn, exit } + ); + + expect(exit).toHaveBeenCalledWith(1); + }); + it("should show a suggestion when is a simple typo", () => { warnAboutDeprecatedOrUnknownCliArgs( { readonli: "", }, - warn + { warn, exit } ); expect(warn).toHaveBeenCalledWith("Invalid command line argument 'readonli'. Did you mean 'readOnly'?"); @@ -713,7 +732,7 @@ describe("CLI arguments", () => { { readonly: "", }, - warn + { warn, exit } ); expect(warn).toHaveBeenCalledWith("Invalid command line argument 'readonly'. Did you mean 'readOnly'?"); From d5c4b205b6bfb0ef0cd259fbba6b12058011f699 Mon Sep 17 00:00:00 2001 From: Kevin Mas Ruiz Date: Fri, 5 Sep 2025 10:06:18 +0200 Subject: [PATCH 8/8] chore: linter, of course --- src/common/config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/common/config.ts b/src/common/config.ts index ed9abb12..9132a6c6 100644 --- a/src/common/config.ts +++ b/src/common/config.ts @@ -309,8 +309,8 @@ function parseCliConfig(args: string[]): CliOptions { warnAboutDeprecatedOrUnknownCliArgs( { ...parsed, _: positionalArguments }, { - warn: console.warn, - exit: process.exit, + warn: (msg) => console.warn(msg), + exit: (status) => process.exit(status), } );