Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 0912f39

Browse files
kmruizCopilot
andauthored
feat(cli): notify when a flag is wrong and suggest a fix MCP-121 (#517)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 1f68c3e commit 0912f39

File tree

5 files changed

+163
-19
lines changed

5 files changed

+163
-19
lines changed

‎package-lock.json

Lines changed: 13 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@
114114
"node-machine-id": "1.1.12",
115115
"oauth4webapi": "^3.8.0",
116116
"openapi-fetch": "^0.14.0",
117+
"ts-levenshtein": "^1.0.7",
117118
"yargs-parser": "^22.0.0",
118119
"zod": "^3.25.76"
119120
},

‎src/common/config.ts

Lines changed: 75 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { CliOptions, ConnectionInfo } from "@mongosh/arg-parser";
55
import { generateConnectionInfoFromCliArgs } from "@mongosh/arg-parser";
66
import { Keychain } from "./keychain.js";
77
import type { Secret } from "./keychain.js";
8+
import levenshtein from "ts-levenshtein";
89

910
// From: https://github.com/mongodb-js/mongosh/blob/main/packages/cli-repl/src/arg-parser.ts
1011
const OPTIONS = {
@@ -91,6 +92,44 @@ const OPTIONS = {
9192
},
9293
} as const;
9394

95+
const ALL_CONFIG_KEYS = new Set(
96+
(OPTIONS.string as readonly string[])
97+
.concat(OPTIONS.array)
98+
.concat(OPTIONS.boolean)
99+
.concat(Object.keys(OPTIONS.alias))
100+
);
101+
102+
export function validateConfigKey(key: string): { valid: boolean; suggestion?: string } {
103+
if (ALL_CONFIG_KEYS.has(key)) {
104+
return { valid: true };
105+
}
106+
107+
let minLev = Number.MAX_VALUE;
108+
let suggestion = "";
109+
110+
// find the closest match for a suggestion
111+
for (const validKey of ALL_CONFIG_KEYS) {
112+
// check if there is an exact case-insensitive match
113+
if (validKey.toLowerCase() === key.toLowerCase()) {
114+
return { valid: false, suggestion: validKey };
115+
}
116+
117+
// else, infer something using levenshtein so we suggest a valid key
118+
const lev = levenshtein.get(key, validKey);
119+
if (lev < minLev) {
120+
minLev = lev;
121+
suggestion = validKey;
122+
}
123+
}
124+
125+
if (minLev <= 2) {
126+
// accept up to 2 typos
127+
return { valid: false, suggestion };
128+
}
129+
130+
return { valid: false };
131+
}
132+
94133
function isConnectionSpecifier(arg: string | undefined): boolean {
95134
return (
96135
arg !== undefined &&
@@ -267,7 +306,13 @@ function parseCliConfig(args: string[]): CliOptions {
267306
// so we don't have a logger. For stdio, the warning will be received as a string in
268307
// the client and IDEs like VSCode do show the message in the log window. For HTTP,
269308
// it will be in the stdout of the process.
270-
warnAboutDeprecatedCliArgs({ ...parsed, _: positionalArguments }, console.warn);
309+
warnAboutDeprecatedOrUnknownCliArgs(
310+
{ ...parsed, _: positionalArguments },
311+
{
312+
warn: (msg) => console.warn(msg),
313+
exit: (status) => process.exit(status),
314+
}
315+
);
271316

272317
// if we have a positional argument that matches a connection string
273318
// store it as the connection specifier and remove it from the argument
@@ -280,26 +325,47 @@ function parseCliConfig(args: string[]): CliOptions {
280325
return parsed;
281326
}
282327

283-
export function warnAboutDeprecatedCliArgs(
284-
args: CliOptions &
285-
UserConfig & {
286-
_?: string[];
287-
},
288-
warn: (msg: string) => void
328+
export function warnAboutDeprecatedOrUnknownCliArgs(
329+
args: Record<string, unknown>,
330+
{ warn, exit }: { warn: (msg: string) => void; exit: (status: number) => void | never }
289331
): void {
290332
let usedDeprecatedArgument = false;
333+
let usedInvalidArgument = false;
334+
335+
const knownArgs = args as unknown as UserConfig & CliOptions;
291336
// the first position argument should be used
292337
// instead of --connectionString, as it's how the mongosh works.
293-
if (args.connectionString) {
338+
if (knownArgs.connectionString) {
294339
usedDeprecatedArgument = true;
295340
warn(
296341
"The --connectionString argument is deprecated. Prefer using the first positional argument for the connection string or the MDB_MCP_CONNECTION_STRING environment variable."
297342
);
298343
}
299344

300-
if (usedDeprecatedArgument) {
345+
for (const providedKey of Object.keys(args)) {
346+
if (providedKey === "_") {
347+
// positional argument
348+
continue;
349+
}
350+
351+
const { valid, suggestion } = validateConfigKey(providedKey);
352+
if (!valid) {
353+
usedInvalidArgument = true;
354+
if (suggestion) {
355+
warn(`Invalid command line argument '${providedKey}'. Did you mean '${suggestion}'?`);
356+
} else {
357+
warn(`Invalid command line argument '${providedKey}'.`);
358+
}
359+
}
360+
}
361+
362+
if (usedInvalidArgument || usedDeprecatedArgument) {
301363
warn("Refer to https://www.mongodb.com/docs/mcp-server/get-started/ for setting up the MCP Server.");
302364
}
365+
366+
if (usedInvalidArgument) {
367+
exit(1);
368+
}
303369
}
304370

305371
function commaSeparatedToArray<T extends string[]>(str: string | string[] | undefined): T {

‎tests/unit/common/config.test.ts

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import type { UserConfig } from "../../../src/common/config.js";
33
import {
44
setupUserConfig,
55
defaultUserConfig,
6-
warnAboutDeprecatedCliArgs,
76
registerKnownSecretsInRootKeychain,
7+
warnAboutDeprecatedOrUnknownCliArgs,
88
} from "../../../src/common/config.js";
99
import type { CliOptions } from "@mongosh/arg-parser";
1010
import { Keychain } from "../../../src/common/keychain.js";
@@ -638,7 +638,7 @@ describe("config", () => {
638638
});
639639
});
640640

641-
describe("Deprecated CLI arguments", () => {
641+
describe("CLI arguments", () => {
642642
const referDocMessage =
643643
"Refer to https://www.mongodb.com/docs/mcp-server/get-started/ for setting up the MCP Server.";
644644

@@ -655,12 +655,14 @@ describe("Deprecated CLI arguments", () => {
655655
describe(`deprecation behaviour of ${cliArg}`, () => {
656656
let cliArgs: CliOptions & UserConfig & { _?: string[] };
657657
let warn: (msg: string) => void;
658+
let exit: (status: number) => void | never;
658659

659660
beforeEach(() => {
660661
cliArgs = { [cliArg]: "RandomString" } as unknown as CliOptions & UserConfig & { _?: string[] };
661662
warn = vi.fn();
663+
exit = vi.fn();
662664

663-
warnAboutDeprecatedCliArgs(cliArgs,warn);
665+
warnAboutDeprecatedOrUnknownCliArgs(cliArgsasunknownasRecord<string,unknown>,{warn, exit });
664666
});
665667

666668
it(`warns the usage of ${cliArg} as it is deprecated`, () => {
@@ -670,9 +672,76 @@ describe("Deprecated CLI arguments", () => {
670672
it(`shows the reference message when ${cliArg} was passed`, () => {
671673
expect(warn).toHaveBeenCalledWith(referDocMessage);
672674
});
675+
676+
it(`should not exit the process`, () => {
677+
expect(exit).not.toHaveBeenCalled();
678+
});
673679
});
674680
}
675681

682+
describe("invalid arguments", () => {
683+
let warn: (msg: string) => void;
684+
let exit: (status: number) => void | never;
685+
686+
beforeEach(() => {
687+
warn = vi.fn();
688+
exit = vi.fn();
689+
});
690+
691+
it("should show a warning when an argument is not known", () => {
692+
warnAboutDeprecatedOrUnknownCliArgs(
693+
{
694+
wakanda: "",
695+
},
696+
{ warn, exit }
697+
);
698+
699+
expect(warn).toHaveBeenCalledWith("Invalid command line argument 'wakanda'.");
700+
expect(warn).toHaveBeenCalledWith(
701+
"Refer to https://www.mongodb.com/docs/mcp-server/get-started/ for setting up the MCP Server."
702+
);
703+
});
704+
705+
it("should exit the process on unknown cli args", () => {
706+
warnAboutDeprecatedOrUnknownCliArgs(
707+
{
708+
wakanda: "",
709+
},
710+
{ warn, exit }
711+
);
712+
713+
expect(exit).toHaveBeenCalledWith(1);
714+
});
715+
716+
it("should show a suggestion when is a simple typo", () => {
717+
warnAboutDeprecatedOrUnknownCliArgs(
718+
{
719+
readonli: "",
720+
},
721+
{ warn, exit }
722+
);
723+
724+
expect(warn).toHaveBeenCalledWith("Invalid command line argument 'readonli'. Did you mean 'readOnly'?");
725+
expect(warn).toHaveBeenCalledWith(
726+
"Refer to https://www.mongodb.com/docs/mcp-server/get-started/ for setting up the MCP Server."
727+
);
728+
});
729+
730+
it("should show a suggestion when the only change is on the case", () => {
731+
warnAboutDeprecatedOrUnknownCliArgs(
732+
{
733+
readonly: "",
734+
},
735+
{ warn, exit }
736+
);
737+
738+
expect(warn).toHaveBeenCalledWith("Invalid command line argument 'readonly'. Did you mean 'readOnly'?");
739+
expect(warn).toHaveBeenCalledWith(
740+
"Refer to https://www.mongodb.com/docs/mcp-server/get-started/ for setting up the MCP Server."
741+
);
742+
});
743+
});
744+
676745
describe("keychain management", () => {
677746
type TestCase = { readonly cliArg: keyof UserConfig; secretKind: Secret["kind"] };
678747
const testCases = [

‎tsconfig.build.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121
"paths": {
2222
"mongodb-connection-string-url": [
2323
"./node_modules/mongodb-connection-string-url/lib/index.d.ts"
24-
]
24+
],
25+
"ts-levenshtein": ["./node_modules/ts-levenshtein/dist/index.d.mts"]
2526
}
2627
},
2728
"include": ["src/**/*.ts"]

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /