Skip to content

Commit 09a9757

Browse files
committed
feat: add support for supplying config options
1 parent 280ceaf commit 09a9757

File tree

5 files changed

+93
-16
lines changed

5 files changed

+93
-16
lines changed

package-lock.json

Lines changed: 9 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"@redocly/cli": "^1.34.2",
3939
"@types/node": "^22.14.0",
4040
"@types/simple-oauth2": "^5.0.7",
41+
"@types/yargs-parser": "^21.0.3",
4142
"eslint": "^9.24.0",
4243
"eslint-config-prettier": "^10.1.1",
4344
"globals": "^16.0.0",
@@ -58,6 +59,7 @@
5859
"mongodb-log-writer": "^2.4.1",
5960
"mongodb-redact": "^1.1.6",
6061
"mongodb-schema": "^12.6.2",
62+
"yargs-parser": "^21.1.1",
6163
"zod": "^3.24.2"
6264
},
6365
"engines": {

src/common/atlas/apiClient.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ export class ApiClient {
9191
throw new Error("Not authenticated. Please run the auth tool first.");
9292
}
9393

94-
const url = new URL(`api/atlas/v2${endpoint}`, `${config.apiBaseURL}`);
94+
const url = new URL(`api/atlas/v2${endpoint}`, `${config.apiBaseUrl}`);
9595

9696
if (!this.checkTokenExpiry()) {
9797
await this.refreshToken();
@@ -119,7 +119,7 @@ export class ApiClient {
119119
async authenticate(): Promise<OauthDeviceCode> {
120120
const endpoint = "api/private/unauth/account/device/authorize";
121121

122-
const authUrl = new URL(endpoint, config.apiBaseURL);
122+
const authUrl = new URL(endpoint, config.apiBaseUrl);
123123

124124
const response = await fetch(authUrl, {
125125
method: "POST",
@@ -128,7 +128,7 @@ export class ApiClient {
128128
Accept: "application/json",
129129
},
130130
body: new URLSearchParams({
131-
client_id: config.clientID,
131+
client_id: config.clientId,
132132
scope: "openid profile offline_access",
133133
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
134134
}).toString(),
@@ -143,14 +143,14 @@ export class ApiClient {
143143

144144
async retrieveToken(device_code: string): Promise<OAuthToken> {
145145
const endpoint = "api/private/unauth/account/device/token";
146-
const url = new URL(endpoint, config.apiBaseURL);
146+
const url = new URL(endpoint, config.apiBaseUrl);
147147
const response = await fetch(url, {
148148
method: "POST",
149149
headers: {
150150
"Content-Type": "application/x-www-form-urlencoded",
151151
},
152152
body: new URLSearchParams({
153-
client_id: config.clientID,
153+
client_id: config.clientId,
154154
device_code: device_code,
155155
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
156156
}).toString(),
@@ -179,15 +179,15 @@ export class ApiClient {
179179

180180
async refreshToken(token?: OAuthToken): Promise<OAuthToken | null> {
181181
const endpoint = "api/private/unauth/account/device/token";
182-
const url = new URL(endpoint, config.apiBaseURL);
182+
const url = new URL(endpoint, config.apiBaseUrl);
183183
const response = await fetch(url, {
184184
method: "POST",
185185
headers: {
186186
"Content-Type": "application/x-www-form-urlencoded",
187187
Accept: "application/json",
188188
},
189189
body: new URLSearchParams({
190-
client_id: config.clientID,
190+
client_id: config.clientId,
191191
refresh_token: (token || this.token)?.refresh_token || "",
192192
grant_type: "refresh_token",
193193
scope: "openid profile offline_access",
@@ -213,7 +213,7 @@ export class ApiClient {
213213

214214
async revokeToken(token?: OAuthToken): Promise<void> {
215215
const endpoint = "api/private/unauth/account/device/token";
216-
const url = new URL(endpoint, config.apiBaseURL);
216+
const url = new URL(endpoint, config.apiBaseUrl);
217217
const response = await fetch(url, {
218218
method: "POST",
219219
headers: {
@@ -222,7 +222,7 @@ export class ApiClient {
222222
"User-Agent": config.userAgent,
223223
},
224224
body: new URLSearchParams({
225-
client_id: config.clientID,
225+
client_id: config.clientId,
226226
token: (token || this.token)?.access_token || "",
227227
token_type_hint: "refresh_token",
228228
}).toString(),

src/config.ts

Lines changed: 72 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,45 @@ import fs from "fs";
33
import { fileURLToPath } from "url";
44
import os from "os";
55

6+
import argv from "yargs-parser";
7+
8+
// If we decide to support non-string config options, we'll need to extend the mechanism for parsing
9+
// env variables.
10+
interface UserConfig extends Record<string, string> {
11+
apiBaseUrl: string;
12+
clientId: string;
13+
stateFile: string;
14+
projectId: string;
15+
}
16+
17+
const cliConfig = argv(process.argv.slice(2)) as unknown as Partial<UserConfig>;
18+
19+
const defaults: UserConfig = {
20+
apiBaseUrl: "https://cloud.mongodb.com/",
21+
clientId: "0oabtxactgS3gHIR0297",
22+
stateFile: path.resolve("./state.json"),
23+
projectId: "",
24+
};
25+
26+
const mergedUserConfig = mergeConfigs(defaults, getFileConfig(), getEnvConfig(), cliConfig);
27+
628
const __filename = fileURLToPath(import.meta.url);
729
const __dirname = path.dirname(__filename);
830

931
const packageMetadata = fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8");
1032
const packageJson = JSON.parse(packageMetadata);
1133

12-
export const config = {
34+
const config = {
35+
...mergedUserConfig,
1336
atlasApiVersion: `2025-03-12`,
1437
version: packageJson.version,
15-
apiBaseURL: process.env.API_BASE_URL || "https://cloud.mongodb.com/",
16-
clientID: process.env.CLIENT_ID || "0oabtxactgS3gHIR0297",
17-
stateFile: process.env.STATE_FILE || path.resolve("./state.json"),
1838
userAgent: `AtlasMCP/${packageJson.version} (${process.platform}; ${process.arch}; ${process.env.HOSTNAME || "unknown"})`,
1939
localDataPath: getLocalDataPath(),
2040
};
2141

2242
export default config;
2343

24-
function getLocalDataPath() {
44+
function getLocalDataPath(): string {
2545
if (process.platform === "win32") {
2646
const appData = process.env.APPDATA;
2747
const localAppData = process.env.LOCALAPPDATA ?? process.env.APPDATA;
@@ -32,3 +52,50 @@ function getLocalDataPath() {
3252

3353
return path.join(os.homedir(), ".mongodb", "mongodb-mcp");
3454
}
55+
56+
// Gets the config supplied by the user as environment variables. The variable names
57+
// are prefixed with `MDB_MCP_` and the keys match the UserConfig keys, but are converted
58+
// to SNAKE_UPPER_CASE.
59+
function getEnvConfig(): Partial<UserConfig> {
60+
const camelCaseToSNAKE_UPPER_CASE = (str: string): string => {
61+
return str.replace(/([a-z])([A-Z])/g, "$1_$2").toUpperCase();
62+
};
63+
64+
const result: Partial<UserConfig> = {};
65+
Object.keys(defaults).forEach((key) => {
66+
const envVarName = `MDB_MCP_${camelCaseToSNAKE_UPPER_CASE(key)}`;
67+
if (process.env[envVarName]) {
68+
result[key as keyof UserConfig] = process.env[envVarName];
69+
}
70+
});
71+
72+
return result;
73+
}
74+
75+
// Gets the config supplied by the user as a JSON file. The file is expected to be located in the local data path
76+
// and named `config.json`.
77+
function getFileConfig(): Partial<UserConfig> {
78+
const configPath = path.join(getLocalDataPath(), "config.json");
79+
80+
try {
81+
const config = fs.readFileSync(configPath, "utf8");
82+
return JSON.parse(config);
83+
} catch {
84+
return {};
85+
}
86+
}
87+
88+
// Merges several user-supplied configs into one. The precedence is from right to left where the last
89+
// config in the `partialConfigs` array overrides the previous ones. The `defaults` config is used as a base.
90+
function mergeConfigs(defaults: UserConfig, ...partialConfigs: Array<Partial<UserConfig>>): UserConfig {
91+
const mergedConfig: UserConfig = { ...defaults };
92+
for (const key of Object.keys(defaults)) {
93+
for (const partialConfig of partialConfigs) {
94+
if (partialConfig[key]) {
95+
mergedConfig[key] = partialConfig[key];
96+
}
97+
}
98+
}
99+
100+
return mergedConfig;
101+
}

src/server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { State, saveState, loadState } from "./state.js";
44
import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
55
import { registerAtlasTools } from "./tools/atlas/tools.js";
66
import { registerMongoDBTools } from "./tools/mongodb/index.js";
7-
import { config } from "./config.js";
7+
import config from "./config.js";
88
import logger, { initializeLogger } from "./logger.js";
99
import { mongoLogId } from "mongodb-log-writer";
1010

0 commit comments

Comments
 (0)