-
Notifications
You must be signed in to change notification settings - Fork 226
Expand file tree
/
Copy pathcreateUserConfig.ts
More file actions
188 lines (170 loc) · 8.15 KB
/
createUserConfig.ts
File metadata and controls
188 lines (170 loc) · 8.15 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
import argv from "yargs-parser";
import { generateConnectionInfoFromCliArgs } from "@mongosh/arg-parser";
import { Keychain } from "../keychain.js";
import type { Secret } from "../keychain.js";
import { isConnectionSpecifier, matchingConfigKey } from "./configUtils.js";
import { OPTIONS } from "./argsParserOptions.js";
import { UserConfigSchema, type UserConfig } from "./userConfig.js";
export type CreateUserConfigHelpers = {
onWarning: (message: string) => void;
onError: (message: string) => void;
closeProcess: (exitCode: number) => never;
cliArguments: string[];
};
export const defaultUserConfigHelpers: CreateUserConfigHelpers = {
onWarning(message) {
console.warn(message);
},
onError(message) {
console.error(message);
},
closeProcess(exitCode) {
process.exit(exitCode);
},
cliArguments: process.argv.slice(2),
};
export function createUserConfig({
onWarning = defaultUserConfigHelpers.onWarning,
onError = defaultUserConfigHelpers.onError,
closeProcess = defaultUserConfigHelpers.closeProcess,
cliArguments = defaultUserConfigHelpers.cliArguments,
}: Partial<CreateUserConfigHelpers> = defaultUserConfigHelpers): UserConfig {
const { unknownCliArgumentErrors, deprecatedCliArgumentWarning, userAndArgsParserConfig, connectionSpecifier } =
parseUserConfigSources(cliArguments);
if (unknownCliArgumentErrors.length) {
const errorMessage = `
${unknownCliArgumentErrors.join("\n")}
- Refer to https://www.mongodb.com/docs/mcp-server/get-started/ for setting up the MCP Server.
`;
onError(errorMessage);
return closeProcess(1);
}
if (deprecatedCliArgumentWarning) {
const deprecatedMessages = `
${deprecatedCliArgumentWarning}
- Refer to https://www.mongodb.com/docs/mcp-server/get-started/ for setting up the MCP Server.
`;
onWarning(deprecatedMessages);
}
// If we have a connectionSpecifier, which can only appear as the positional
// argument, then that has to be used on priority to construct the
// connection string. In this case, if there is a connection string provided
// by the env variable or config file, that will be overridden.
if (connectionSpecifier) {
const connectionInfo = generateConnectionInfoFromCliArgs({ ...userAndArgsParserConfig, connectionSpecifier });
userAndArgsParserConfig.connectionString = connectionInfo.connectionString;
}
const configParseResult = UserConfigSchema.safeParse(userAndArgsParserConfig);
if (configParseResult.error) {
onError(
`Invalid configuration for the following fields:\n${configParseResult.error.issues.map((issue) => `${issue.path.join(".")} - ${issue.message}`).join("\n")}`
);
return closeProcess(1);
}
// TODO: Separate correctly parsed user config from all other valid
// arguments relevant to mongosh's args-parser.
const userConfig: UserConfig = { ...userAndArgsParserConfig, ...configParseResult.data };
warnIfVectorSearchNotEnabledCorrectly(userConfig, onWarning);
registerKnownSecretsInRootKeychain(userConfig);
return userConfig;
}
function parseUserConfigSources(cliArguments: string[]): {
unknownCliArgumentErrors: string[];
deprecatedCliArgumentWarning: string | undefined;
userAndArgsParserConfig: Record<string, unknown>;
connectionSpecifier: string | undefined;
} {
const {
_: positionalAndUnknownArguments,
// We don't make use of end of flag arguments but also don't want them to
// end up alongside unknown arguments so we are extracting them and having a
// no-op statement so ESLint does not complain.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
"--": _endOfFlagArguments,
...parsedUserAndArgsParserConfig
} = argv(cliArguments, {
...OPTIONS,
// This helps parse the relevant environment variables.
envPrefix: "MDB_MCP_",
configuration: {
...OPTIONS.configuration,
// Setting this to true will populate `_` variable which is
// originally used for positional arguments, now with the unknown
// arguments as well. The order of arguments are maintained.
"unknown-options-as-args": true,
// To avoid populating `_` with end-of-flag arguments we explicitly
// populate `--` variable and altogether ignore them later.
"populate--": true,
},
});
// A connectionSpecifier can be one of:
// - database name
// - host name
// - ip address
// - replica set specifier
// - complete connection string
let connectionSpecifier: string | undefined = undefined;
const [maybeConnectionSpecifier, ...unknownArguments] = positionalAndUnknownArguments;
if (typeof maybeConnectionSpecifier === "string" && isConnectionSpecifier(maybeConnectionSpecifier)) {
connectionSpecifier = maybeConnectionSpecifier;
} else if (maybeConnectionSpecifier !== undefined) {
// If the extracted connection specifier is not a connection specifier
// indeed, then we push it back to the unknown arguments list. This might
// happen for example when an unknown argument is provided without ever
// specifying a positional argument.
unknownArguments.unshift(maybeConnectionSpecifier);
}
return {
unknownCliArgumentErrors: unknownArguments
.filter((argument): argument is string => typeof argument === "string" && argument.startsWith("--"))
.map((argument) => {
const argumentKey = argument.replace(/^(--)/, "");
const matchingKey = matchingConfigKey(argumentKey);
if (matchingKey) {
return `Error: Invalid command line argument '${argument}'. Did you mean '--${matchingKey}'?`;
}
return `Error: Invalid command line argument '${argument}'.`;
}),
deprecatedCliArgumentWarning: cliArguments.find((argument) => argument.startsWith("--connectionString"))
? "Warning: The --connectionString argument is deprecated. Prefer using the MDB_MCP_CONNECTION_STRING environment variable or the first positional argument for the connection string."
: undefined,
userAndArgsParserConfig: parsedUserAndArgsParserConfig,
connectionSpecifier,
};
}
function registerKnownSecretsInRootKeychain(userConfig: Partial<UserConfig>): void {
const keychain = Keychain.root;
const maybeRegister = (value: string | undefined, kind: Secret["kind"]): void => {
if (value) {
keychain.register(value, kind);
}
};
maybeRegister(userConfig.apiClientId, "user");
maybeRegister(userConfig.apiClientSecret, "password");
maybeRegister(userConfig.awsAccessKeyId, "password");
maybeRegister(userConfig.awsIamSessionToken, "password");
maybeRegister(userConfig.awsSecretAccessKey, "password");
maybeRegister(userConfig.awsSessionToken, "password");
maybeRegister(userConfig.password, "password");
maybeRegister(userConfig.tlsCAFile, "url");
maybeRegister(userConfig.tlsCRLFile, "url");
maybeRegister(userConfig.tlsCertificateKeyFile, "url");
maybeRegister(userConfig.tlsCertificateKeyFilePassword, "password");
maybeRegister(userConfig.username, "user");
}
function warnIfVectorSearchNotEnabledCorrectly(config: UserConfig, warn: (message: string) => void): void {
const searchEnabled = config.previewFeatures.includes("search");
const embeddingsProviderConfigured = !!config.voyageApiKey;
if (searchEnabled && !embeddingsProviderConfigured) {
warn(`\
Warning: Vector search is enabled but no embeddings provider is configured.
- Set an embeddings provider configuration option to enable auto-embeddings during document insertion and text-based queries with $vectorSearch.\
`);
}
if (!searchEnabled && embeddingsProviderConfigured) {
warn(`\
Warning: An embeddings provider is configured but the 'search' preview feature is not enabled.
- Enable vector search by adding 'search' to the 'previewFeatures' configuration option, or remove the embeddings provider configuration if not needed.\
`);
}
}