Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -114,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"
},
Expand Down
84 changes: 75 additions & 9 deletions src/common/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 levenshtein from "ts-levenshtein";

// From: https://github.com/mongodb-js/mongosh/blob/main/packages/cli-repl/src/arg-parser.ts
const OPTIONS = {
Expand Down Expand Up @@ -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))
);

export function validateConfigKey(key: string): { valid: boolean; suggestion?: string } {
if (ALL_CONFIG_KEYS.has(key)) {
return { valid: true };
}

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()) {
return { valid: false, suggestion: validKey };
}

// else, infer something using levenshtein so we suggest a valid key
const lev = levenshtein.get(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 &&
Expand Down Expand Up @@ -267,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.
warnAboutDeprecatedCliArgs({ ...parsed, _: positionalArguments }, console.warn);
warnAboutDeprecatedOrUnknownCliArgs(
{ ...parsed, _: positionalArguments },
{
warn: (msg) => console.warn(msg),
exit: (status) => process.exit(status),
}
);

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

export function warnAboutDeprecatedCliArgs(
args: CliOptions &
UserConfig & {
_?: string[];
},
warn: (msg: string) => void
export function warnAboutDeprecatedOrUnknownCliArgs(
args: Record<string, unknown>,
{ warn, exit }: { warn: (msg: string) => void; exit: (status: number) => void | never }
): void {
let usedDeprecatedArgument = false;
let usedInvalidArgument = false;

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 (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."
);
}

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.");
}

if (usedInvalidArgument) {
exit(1);
}
}

function commaSeparatedToArray<T extends string[]>(str: string | string[] | undefined): T {
Expand Down
75 changes: 72 additions & 3 deletions tests/unit/common/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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.";

Expand All @@ -655,12 +655,14 @@ describe("Deprecated 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();

warnAboutDeprecatedCliArgs(cliArgs, warn);
warnAboutDeprecatedOrUnknownCliArgs(cliArgs as unknown as Record<string, unknown>, { warn, exit });
});

it(`warns the usage of ${cliArg} as it is deprecated`, () => {
Expand All @@ -670,9 +672,76 @@ describe("Deprecated 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", () => {
warnAboutDeprecatedOrUnknownCliArgs(
{
wakanda: "",
},
{ warn, exit }
);

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 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, exit }
);

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, exit }
);

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 = [
Expand Down
3 changes: 2 additions & 1 deletion tsconfig.build.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
Loading