From 09a9757146807240a27b1943684735937e3bc619 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Thu, 10 Apr 2025 13:45:35 +0200 Subject: [PATCH 1/7] feat: add support for supplying config options --- package-lock.json | 10 ++++- package.json | 2 + src/common/atlas/apiClient.ts | 18 ++++---- src/config.ts | 77 ++++++++++++++++++++++++++++++++--- src/server.ts | 2 +- 5 files changed, 93 insertions(+), 16 deletions(-) diff --git a/package-lock.json b/package-lock.json index 83af188c..307ef728 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "mongodb-log-writer": "^2.4.1", "mongodb-redact": "^1.1.6", "mongodb-schema": "^12.6.2", + "yargs-parser": "^21.1.1", "zod": "^3.24.2" }, "bin": { @@ -29,6 +30,7 @@ "@redocly/cli": "^1.34.2", "@types/node": "^22.14.0", "@types/simple-oauth2": "^5.0.7", + "@types/yargs-parser": "^21.0.3", "eslint": "^9.24.0", "eslint-config-prettier": "^10.1.1", "globals": "^16.0.0", @@ -4769,6 +4771,13 @@ "@types/webidl-conversions": "*" } }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.29.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.29.1.tgz", @@ -11218,7 +11227,6 @@ "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "devOptional": true, "license": "ISC", "engines": { "node": ">=12" diff --git a/package.json b/package.json index 94a85898..62f82c4a 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "@redocly/cli": "^1.34.2", "@types/node": "^22.14.0", "@types/simple-oauth2": "^5.0.7", + "@types/yargs-parser": "^21.0.3", "eslint": "^9.24.0", "eslint-config-prettier": "^10.1.1", "globals": "^16.0.0", @@ -58,6 +59,7 @@ "mongodb-log-writer": "^2.4.1", "mongodb-redact": "^1.1.6", "mongodb-schema": "^12.6.2", + "yargs-parser": "^21.1.1", "zod": "^3.24.2" }, "engines": { diff --git a/src/common/atlas/apiClient.ts b/src/common/atlas/apiClient.ts index 6d745792..87ce4b05 100644 --- a/src/common/atlas/apiClient.ts +++ b/src/common/atlas/apiClient.ts @@ -91,7 +91,7 @@ export class ApiClient { throw new Error("Not authenticated. Please run the auth tool first."); } - const url = new URL(`api/atlas/v2${endpoint}`, `${config.apiBaseURL}`); + const url = new URL(`api/atlas/v2${endpoint}`, `${config.apiBaseUrl}`); if (!this.checkTokenExpiry()) { await this.refreshToken(); @@ -119,7 +119,7 @@ export class ApiClient { async authenticate(): Promise { const endpoint = "api/private/unauth/account/device/authorize"; - const authUrl = new URL(endpoint, config.apiBaseURL); + const authUrl = new URL(endpoint, config.apiBaseUrl); const response = await fetch(authUrl, { method: "POST", @@ -128,7 +128,7 @@ export class ApiClient { Accept: "application/json", }, body: new URLSearchParams({ - client_id: config.clientID, + client_id: config.clientId, scope: "openid profile offline_access", grant_type: "urn:ietf:params:oauth:grant-type:device_code", }).toString(), @@ -143,14 +143,14 @@ export class ApiClient { async retrieveToken(device_code: string): Promise { const endpoint = "api/private/unauth/account/device/token"; - const url = new URL(endpoint, config.apiBaseURL); + const url = new URL(endpoint, config.apiBaseUrl); const response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", }, body: new URLSearchParams({ - client_id: config.clientID, + client_id: config.clientId, device_code: device_code, grant_type: "urn:ietf:params:oauth:grant-type:device_code", }).toString(), @@ -179,7 +179,7 @@ export class ApiClient { async refreshToken(token?: OAuthToken): Promise { const endpoint = "api/private/unauth/account/device/token"; - const url = new URL(endpoint, config.apiBaseURL); + const url = new URL(endpoint, config.apiBaseUrl); const response = await fetch(url, { method: "POST", headers: { @@ -187,7 +187,7 @@ export class ApiClient { Accept: "application/json", }, body: new URLSearchParams({ - client_id: config.clientID, + client_id: config.clientId, refresh_token: (token || this.token)?.refresh_token || "", grant_type: "refresh_token", scope: "openid profile offline_access", @@ -213,7 +213,7 @@ export class ApiClient { async revokeToken(token?: OAuthToken): Promise { const endpoint = "api/private/unauth/account/device/token"; - const url = new URL(endpoint, config.apiBaseURL); + const url = new URL(endpoint, config.apiBaseUrl); const response = await fetch(url, { method: "POST", headers: { @@ -222,7 +222,7 @@ export class ApiClient { "User-Agent": config.userAgent, }, body: new URLSearchParams({ - client_id: config.clientID, + client_id: config.clientId, token: (token || this.token)?.access_token || "", token_type_hint: "refresh_token", }).toString(), diff --git a/src/config.ts b/src/config.ts index 5972fadf..16afd581 100644 --- a/src/config.ts +++ b/src/config.ts @@ -3,25 +3,45 @@ import fs from "fs"; import { fileURLToPath } from "url"; import os from "os"; +import argv from "yargs-parser"; + +// If we decide to support non-string config options, we'll need to extend the mechanism for parsing +// env variables. +interface UserConfig extends Record { + apiBaseUrl: string; + clientId: string; + stateFile: string; + projectId: string; +} + +const cliConfig = argv(process.argv.slice(2)) as unknown as Partial; + +const defaults: UserConfig = { + apiBaseUrl: "https://cloud.mongodb.com/", + clientId: "0oabtxactgS3gHIR0297", + stateFile: path.resolve("./state.json"), + projectId: "", +}; + +const mergedUserConfig = mergeConfigs(defaults, getFileConfig(), getEnvConfig(), cliConfig); + const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const packageMetadata = fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8"); const packageJson = JSON.parse(packageMetadata); -export const config = { +const config = { + ...mergedUserConfig, atlasApiVersion: `2025-03-12`, version: packageJson.version, - apiBaseURL: process.env.API_BASE_URL || "https://cloud.mongodb.com/", - clientID: process.env.CLIENT_ID || "0oabtxactgS3gHIR0297", - stateFile: process.env.STATE_FILE || path.resolve("./state.json"), userAgent: `AtlasMCP/${packageJson.version} (${process.platform}; ${process.arch}; ${process.env.HOSTNAME || "unknown"})`, localDataPath: getLocalDataPath(), }; export default config; -function getLocalDataPath() { +function getLocalDataPath(): string { if (process.platform === "win32") { const appData = process.env.APPDATA; const localAppData = process.env.LOCALAPPDATA ?? process.env.APPDATA; @@ -32,3 +52,50 @@ function getLocalDataPath() { return path.join(os.homedir(), ".mongodb", "mongodb-mcp"); } + +// Gets the config supplied by the user as environment variables. The variable names +// are prefixed with `MDB_MCP_` and the keys match the UserConfig keys, but are converted +// to SNAKE_UPPER_CASE. +function getEnvConfig(): Partial { + const camelCaseToSNAKE_UPPER_CASE = (str: string): string => { + return str.replace(/([a-z])([A-Z])/g, "$1_$2").toUpperCase(); + }; + + const result: Partial = {}; + Object.keys(defaults).forEach((key) => { + const envVarName = `MDB_MCP_${camelCaseToSNAKE_UPPER_CASE(key)}`; + if (process.env[envVarName]) { + result[key as keyof UserConfig] = process.env[envVarName]; + } + }); + + return result; +} + +// Gets the config supplied by the user as a JSON file. The file is expected to be located in the local data path +// and named `config.json`. +function getFileConfig(): Partial { + const configPath = path.join(getLocalDataPath(), "config.json"); + + try { + const config = fs.readFileSync(configPath, "utf8"); + return JSON.parse(config); + } catch { + return {}; + } +} + +// Merges several user-supplied configs into one. The precedence is from right to left where the last +// config in the `partialConfigs` array overrides the previous ones. The `defaults` config is used as a base. +function mergeConfigs(defaults: UserConfig, ...partialConfigs: Array>): UserConfig { + const mergedConfig: UserConfig = { ...defaults }; + for (const key of Object.keys(defaults)) { + for (const partialConfig of partialConfigs) { + if (partialConfig[key]) { + mergedConfig[key] = partialConfig[key]; + } + } + } + + return mergedConfig; +} diff --git a/src/server.ts b/src/server.ts index b72b69ba..0fc68679 100644 --- a/src/server.ts +++ b/src/server.ts @@ -4,7 +4,7 @@ import { State, saveState, loadState } from "./state.js"; import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; import { registerAtlasTools } from "./tools/atlas/tools.js"; import { registerMongoDBTools } from "./tools/mongodb/index.js"; -import { config } from "./config.js"; +import config from "./config.js"; import logger, { initializeLogger } from "./logger.js"; import { mongoLogId } from "mongodb-log-writer"; From 45e10a8d30db7f1a98c0d2329fed2c3273ad6767 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Thu, 10 Apr 2025 14:00:21 +0200 Subject: [PATCH 2/7] move state.json to the app dir --- src/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config.ts b/src/config.ts index 16afd581..9f207357 100644 --- a/src/config.ts +++ b/src/config.ts @@ -19,7 +19,7 @@ const cliConfig = argv(process.argv.slice(2)) as unknown as Partial; const defaults: UserConfig = { apiBaseUrl: "https://cloud.mongodb.com/", clientId: "0oabtxactgS3gHIR0297", - stateFile: path.resolve("./state.json"), + stateFile: path.join(getLocalDataPath(), "state.json"), projectId: "", }; From ee7ce69a54e6c9e9287bd89182317d3ed9c72aeb Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Thu, 10 Apr 2025 14:11:46 +0200 Subject: [PATCH 3/7] fix state-saving --- src/config.ts | 18 ++++++++++---- src/server.ts | 4 +-- src/state.ts | 47 ++++++++++++++---------------------- src/tools/atlas/auth.ts | 10 ++++---- src/tools/mongodb/connect.ts | 5 +++- 5 files changed, 42 insertions(+), 42 deletions(-) diff --git a/src/config.ts b/src/config.ts index 9f207357..cf19cfb3 100644 --- a/src/config.ts +++ b/src/config.ts @@ -5,6 +5,8 @@ import os from "os"; import argv from "yargs-parser"; +const localDataPath = getLocalDataPath(); + // If we decide to support non-string config options, we'll need to extend the mechanism for parsing // env variables. interface UserConfig extends Record { @@ -19,7 +21,7 @@ const cliConfig = argv(process.argv.slice(2)) as unknown as Partial; const defaults: UserConfig = { apiBaseUrl: "https://cloud.mongodb.com/", clientId: "0oabtxactgS3gHIR0297", - stateFile: path.join(getLocalDataPath(), "state.json"), + stateFile: path.join(localDataPath, "state.json"), projectId: "", }; @@ -36,21 +38,27 @@ const config = { atlasApiVersion: `2025-03-12`, version: packageJson.version, userAgent: `AtlasMCP/${packageJson.version} (${process.platform}; ${process.arch}; ${process.env.HOSTNAME || "unknown"})`, - localDataPath: getLocalDataPath(), + localDataPath, }; export default config; function getLocalDataPath(): string { + let result: string | undefined; + if (process.platform === "win32") { const appData = process.env.APPDATA; const localAppData = process.env.LOCALAPPDATA ?? process.env.APPDATA; if (localAppData && appData) { - return path.join(localAppData, "mongodb", "mongodb-mcp"); + result = path.join(localAppData, "mongodb", "mongodb-mcp"); } } - return path.join(os.homedir(), ".mongodb", "mongodb-mcp"); + result ??= path.join(os.homedir(), ".mongodb", "mongodb-mcp"); + + fs.mkdirSync(result, { recursive: true }); + + return result; } // Gets the config supplied by the user as environment variables. The variable names @@ -75,7 +83,7 @@ function getEnvConfig(): Partial { // Gets the config supplied by the user as a JSON file. The file is expected to be located in the local data path // and named `config.json`. function getFileConfig(): Partial { - const configPath = path.join(getLocalDataPath(), "config.json"); + const configPath = path.join(localDataPath, "config.json"); try { const config = fs.readFileSync(configPath, "utf8"); diff --git a/src/server.ts b/src/server.ts index 0fc68679..6ccb92f5 100644 --- a/src/server.ts +++ b/src/server.ts @@ -21,14 +21,14 @@ export class Server { this.apiClient = new ApiClient({ token: this.state?.auth.token, - saveToken: (token) => { + saveToken: async (token) => { if (!this.state) { throw new Error("State is not initialized"); } this.state.auth.code = undefined; this.state.auth.token = token; this.state.auth.status = "issued"; - saveState(this.state); + await saveState(this.state); }, }); diff --git a/src/state.ts b/src/state.ts index b020479e..349dd04c 100644 --- a/src/state.ts +++ b/src/state.ts @@ -1,4 +1,4 @@ -import fs from "fs"; +import fs from "fs/promises"; import config from "./config.js"; import { OauthDeviceCode, OAuthToken } from "./common/atlas/apiClient.js"; @@ -8,37 +8,26 @@ export interface State { code?: OauthDeviceCode; token?: OAuthToken; }; + connectionString?: string; } export async function saveState(state: State): Promise { - return new Promise((resolve, reject) => { - fs.writeFile(config.stateFile, JSON.stringify(state), function (err) { - if (err) { - return reject(err); - } - - return resolve(); - }); - }); + await fs.writeFile(config.stateFile, JSON.stringify(state), { encoding: "utf-8" }); } -export async function loadState() { - return new Promise((resolve, reject) => { - fs.readFile(config.stateFile, "utf-8", (err, data) => { - if (err) { - if (err.code === "ENOENT") { - // File does not exist, return default state - const defaultState: State = { - auth: { - status: "not_auth", - }, - }; - return resolve(defaultState); - } else { - return reject(err); - } - } - return resolve(JSON.parse(data) as State); - }); - }); +export async function loadState(): Promise { + try { + const data = await fs.readFile(config.stateFile, "utf-8"); + return JSON.parse(data) as State; + } catch (err: unknown) { + if (err && typeof err === "object" && "code" in err && err.code === "ENOENT") { + return { + auth: { + status: "not_auth", + }, + }; + } + + throw err; + } } diff --git a/src/tools/atlas/auth.ts b/src/tools/atlas/auth.ts index e6964a11..84fe8527 100644 --- a/src/tools/atlas/auth.ts +++ b/src/tools/atlas/auth.ts @@ -11,7 +11,7 @@ export class AuthTool extends AtlasToolBase { protected argsShape = {}; private async isAuthenticated(): Promise { - return isAuthenticated(this.state!, this.apiClient); + return isAuthenticated(this.state, this.apiClient); } async execute(): Promise { @@ -25,11 +25,11 @@ export class AuthTool extends AtlasToolBase { try { const code = await this.apiClient.authenticate(); - this.state!.auth.status = "requested"; - this.state!.auth.code = code; - this.state!.auth.token = undefined; + this.state.auth.status = "requested"; + this.state.auth.code = code; + this.state.auth.token = undefined; - await saveState(this.state!); + await saveState(this.state); return { content: [ diff --git a/src/tools/mongodb/connect.ts b/src/tools/mongodb/connect.ts index 65e6b5c1..94296f94 100644 --- a/src/tools/mongodb/connect.ts +++ b/src/tools/mongodb/connect.ts @@ -4,6 +4,7 @@ import { NodeDriverServiceProvider } from "@mongosh/service-provider-node-driver import { DbOperationType, MongoDBToolBase } from "./mongodbTool.js"; import { ToolArgs } from "../tool"; import { ErrorCodes, MongoDBError } from "../../errors.js"; +import { saveState } from "../../state.js"; export class ConnectTool extends MongoDBToolBase { protected name = "connect"; @@ -20,8 +21,8 @@ export class ConnectTool extends MongoDBToolBase { protected async execute({ connectionStringOrClusterName, }: ToolArgs): Promise { + connectionStringOrClusterName ??= this.state.connectionString; if (!connectionStringOrClusterName) { - // TODO: try reconnecting to the default connection return { content: [ { type: "text", text: "No connection details provided." }, @@ -71,5 +72,7 @@ export class ConnectTool extends MongoDBToolBase { }); this.mongodbState.serviceProvider = provider; + this.state.connectionString = connectionString; + await saveState(this.state); } } From cf7d011d3b2a437ed8e94476c40c3f4673121e2f Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Thu, 10 Apr 2025 14:14:55 +0200 Subject: [PATCH 4/7] simplify config merging --- src/config.ts | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/src/config.ts b/src/config.ts index cf19cfb3..9cd839fc 100644 --- a/src/config.ts +++ b/src/config.ts @@ -16,8 +16,6 @@ interface UserConfig extends Record { projectId: string; } -const cliConfig = argv(process.argv.slice(2)) as unknown as Partial; - const defaults: UserConfig = { apiBaseUrl: "https://cloud.mongodb.com/", clientId: "0oabtxactgS3gHIR0297", @@ -25,7 +23,7 @@ const defaults: UserConfig = { projectId: "", }; -const mergedUserConfig = mergeConfigs(defaults, getFileConfig(), getEnvConfig(), cliConfig); +const mergedUserConfig = Object.assign({}, defaults, getFileConfig(), getEnvConfig(), getCliConfig()); const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -93,17 +91,7 @@ function getFileConfig(): Partial { } } -// Merges several user-supplied configs into one. The precedence is from right to left where the last -// config in the `partialConfigs` array overrides the previous ones. The `defaults` config is used as a base. -function mergeConfigs(defaults: UserConfig, ...partialConfigs: Array>): UserConfig { - const mergedConfig: UserConfig = { ...defaults }; - for (const key of Object.keys(defaults)) { - for (const partialConfig of partialConfigs) { - if (partialConfig[key]) { - mergedConfig[key] = partialConfig[key]; - } - } - } - - return mergedConfig; +// Reads the cli args and parses them into a UserConfig object. +function getCliConfig() { + return argv(process.argv.slice(2)) as unknown as Partial; } From ad91bfc2ac92e1955fbd413e1b86d3779d71f832 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Thu, 10 Apr 2025 14:27:15 +0200 Subject: [PATCH 5/7] Use object spread --- src/config.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/config.ts b/src/config.ts index 9cd839fc..ba6e1f3e 100644 --- a/src/config.ts +++ b/src/config.ts @@ -13,17 +13,20 @@ interface UserConfig extends Record { apiBaseUrl: string; clientId: string; stateFile: string; - projectId: string; } const defaults: UserConfig = { apiBaseUrl: "https://cloud.mongodb.com/", clientId: "0oabtxactgS3gHIR0297", stateFile: path.join(localDataPath, "state.json"), - projectId: "", }; -const mergedUserConfig = Object.assign({}, defaults, getFileConfig(), getEnvConfig(), getCliConfig()); +const mergedUserConfig = { + ...defaults, + ...getFileConfig(), + ...getEnvConfig(), + ...getCliConfig(), +}; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -68,12 +71,12 @@ function getEnvConfig(): Partial { }; const result: Partial = {}; - Object.keys(defaults).forEach((key) => { + for (const key of Object.keys(defaults)) { const envVarName = `MDB_MCP_${camelCaseToSNAKE_UPPER_CASE(key)}`; if (process.env[envVarName]) { - result[key as keyof UserConfig] = process.env[envVarName]; + result[key] = process.env[envVarName]; } - }); + } return result; } From 84a0fe04e281fa1f87505fb20c7d2886d0bd36f7 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Thu, 10 Apr 2025 14:51:15 +0200 Subject: [PATCH 6/7] Change config storage location --- src/config.ts | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/config.ts b/src/config.ts index 25681da9..03aa16f2 100644 --- a/src/config.ts +++ b/src/config.ts @@ -4,7 +4,7 @@ import argv from "yargs-parser"; import packageJson from "../package.json" with { type: "json" }; import fs from "fs"; -const localDataPath = getLocalDataPath(); +const { localDataPath, configPath } = getLocalDataPath(); // If we decide to support non-string config options, we'll need to extend the mechanism for parsing // env variables. @@ -37,22 +37,28 @@ const config = { export default config; -function getLocalDataPath(): string { - let result: string | undefined; +function getLocalDataPath(): { localDataPath: string; configPath: string } { + let localDataPath: string | undefined; + let configPath: string | undefined; if (process.platform === "win32") { const appData = process.env.APPDATA; const localAppData = process.env.LOCALAPPDATA ?? process.env.APPDATA; if (localAppData && appData) { - result = path.join(localAppData, "mongodb", "mongodb-mcp"); + localDataPath = path.join(localAppData, "mongodb", "mongodb-mcp"); + configPath = path.join(localAppData, "mongodb", "mongodb-mcp.conf"); } } - result ??= path.join(os.homedir(), ".mongodb", "mongodb-mcp"); + localDataPath ??= path.join(os.homedir(), ".mongodb", "mongodb-mcp"); + configPath ??= "/etc/mongodb-mcp.conf"; - fs.mkdirSync(result, { recursive: true }); + fs.mkdirSync(localDataPath, { recursive: true }); - return result; + return { + localDataPath, + configPath, + }; } // Gets the config supplied by the user as environment variables. The variable names @@ -77,8 +83,6 @@ function getEnvConfig(): Partial { // Gets the config supplied by the user as a JSON file. The file is expected to be located in the local data path // and named `config.json`. function getFileConfig(): Partial { - const configPath = path.join(localDataPath, "config.json"); - try { const config = fs.readFileSync(configPath, "utf8"); return JSON.parse(config); From cac95a7b64b3625dad7ffadd6587b79ec509f1e2 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Thu, 10 Apr 2025 14:59:32 +0200 Subject: [PATCH 7/7] Use correct folder on windows --- src/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config.ts b/src/config.ts index 03aa16f2..0357101c 100644 --- a/src/config.ts +++ b/src/config.ts @@ -46,7 +46,7 @@ function getLocalDataPath(): { localDataPath: string; configPath: string } { const localAppData = process.env.LOCALAPPDATA ?? process.env.APPDATA; if (localAppData && appData) { localDataPath = path.join(localAppData, "mongodb", "mongodb-mcp"); - configPath = path.join(localAppData, "mongodb", "mongodb-mcp.conf"); + configPath = path.join(localDataPath, "mongodb-mcp.conf"); } }