diff --git a/README.md b/README.md index b8d4eedd..b887870a 100644 --- a/README.md +++ b/README.md @@ -157,8 +157,6 @@ The MongoDB MCP Server can be configured using multiple methods, with the follow 1. Command-line arguments 2. Environment variables -3. Configuration file -4. Default values ### Configuration Options @@ -167,6 +165,12 @@ The MongoDB MCP Server can be configured using multiple methods, with the follow | `apiClientId` | Atlas API client ID for authentication | | `apiClientSecret` | Atlas API client secret for authentication | | `connectionString` | MongoDB connection string for direct database connections (optional users may choose to inform it on every tool call) | +| `logPath` | Folder to store logs | + +**Default Log Path:** + +- Windows: `%LOCALAPPDATA%\mongodb\mongodb-mcp\.app-logs` +- macOS/Linux: `~/.mongodb/mongodb-mcp/.app-logs` ### Atlas API Access @@ -195,23 +199,6 @@ To use the Atlas API tools, you'll need to create a service account in MongoDB A ### Configuration Methods -#### Configuration File - -Create a JSON configuration file at one of these locations: - -- Linux/macOS: `/etc/mongodb-mcp.conf` -- Windows: `%LOCALAPPDATA%\mongodb\mongodb-mcp\mongodb-mcp.conf` - -Example configuration file: - -```json -{ - "apiClientId": "your-atlas-client-id", - "apiClientSecret": "your-atlas-client-secret", - "connectionString": "mongodb+srv://username:password@cluster.mongodb.net/myDatabase" -} -``` - #### Environment Variables Set environment variables with the prefix `MDB_MCP_` followed by the option name in uppercase with underscores: @@ -223,6 +210,8 @@ export MDB_MCP_API_CLIENT_SECRET="your-atlas-client-secret" # Set a custom MongoDB connection string export MDB_MCP_CONNECTION_STRING="mongodb+srv://username:password@cluster.mongodb.net/myDatabase" + +export MDB_MCP_LOG_PATH="/path/to/logs" ``` #### Command-Line Arguments @@ -230,7 +219,7 @@ export MDB_MCP_CONNECTION_STRING="mongodb+srv://username:password@cluster.mongod Pass configuration options as command-line arguments when starting the server: ```shell -node dist/index.js --apiClientId="your-atlas-client-id" --apiClientSecret="your-atlas-client-secret" --connectionString="mongodb+srv://username:password@cluster.mongodb.net/myDatabase" +node dist/index.js --apiClientId="your-atlas-client-id" --apiClientSecret="your-atlas-client-secret" --connectionString="mongodb+srv://username:password@cluster.mongodb.net/myDatabase" --logPath=/path/to/logs ``` ## 🤝 Contributing diff --git a/package-lock.json b/package-lock.json index 27ab6a0f..0ec360e2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,6 @@ "dependencies": { "@mongodb-js/devtools-connect": "^3.7.2", "@mongosh/service-provider-node-driver": "^3.6.0", - "@napi-rs/keyring": "^1.1.6", "@types/express": "^5.0.1", "bson": "^6.10.3", "mongodb": "^6.15.0", @@ -3643,221 +3642,6 @@ "node": ">=14.15.1" } }, - "node_modules/@napi-rs/keyring": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@napi-rs/keyring/-/keyring-1.1.6.tgz", - "integrity": "sha512-e6xoYELSMyaxcXv4MmEHhf0oOGsMnfWMmeu84CD91ICMgMH1I1vrLSMFpiPEQz03xD+pNQgAkQ7DwwBDozCuvw==", - "license": "MIT", - "engines": { - "node": ">= 10" - }, - "optionalDependencies": { - "@napi-rs/keyring-darwin-arm64": "1.1.6", - "@napi-rs/keyring-darwin-x64": "1.1.6", - "@napi-rs/keyring-freebsd-x64": "1.1.6", - "@napi-rs/keyring-linux-arm-gnueabihf": "1.1.6", - "@napi-rs/keyring-linux-arm64-gnu": "1.1.6", - "@napi-rs/keyring-linux-arm64-musl": "1.1.6", - "@napi-rs/keyring-linux-riscv64-gnu": "1.1.6", - "@napi-rs/keyring-linux-x64-gnu": "1.1.6", - "@napi-rs/keyring-linux-x64-musl": "1.1.6", - "@napi-rs/keyring-win32-arm64-msvc": "1.1.6", - "@napi-rs/keyring-win32-ia32-msvc": "1.1.6", - "@napi-rs/keyring-win32-x64-msvc": "1.1.6" - } - }, - "node_modules/@napi-rs/keyring-darwin-arm64": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@napi-rs/keyring-darwin-arm64/-/keyring-darwin-arm64-1.1.6.tgz", - "integrity": "sha512-8N+qvM+O6OSU59BTgDP/PvqYhoqfOcD2HGy1NgRFo1B0DRmkTp4U/DGZrV4Pk/nOP6Uf0PLqznfx3a/M8O5sjQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/keyring-darwin-x64": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@napi-rs/keyring-darwin-x64/-/keyring-darwin-x64-1.1.6.tgz", - "integrity": "sha512-r3Jgc5/ubfaao6Lmk/USA13IwU/GEVLP8NDfg5gYXjPVllU6bWnAaEDHVg7q4vl51kViwj9ELo6XTmOeJFut6A==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/keyring-freebsd-x64": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@napi-rs/keyring-freebsd-x64/-/keyring-freebsd-x64-1.1.6.tgz", - "integrity": "sha512-ayG396jZAt7j820gsEyW/LJKn+rf9KtgSPq1NKpvu84Y5GXopoFLyjMIP7wYZ1RLBL6SGKy27/f8S4f6YZ4DuA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/keyring-linux-arm-gnueabihf": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-arm-gnueabihf/-/keyring-linux-arm-gnueabihf-1.1.6.tgz", - "integrity": "sha512-8nXavgxcaUTUxyFHR+PEQF7eC8rITlYZNUmlf5amTb36y5bkNKrc3QLvCxjtbFSR/+KYzMi3vydoqNmFpF616w==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/keyring-linux-arm64-gnu": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-arm64-gnu/-/keyring-linux-arm64-gnu-1.1.6.tgz", - "integrity": "sha512-qsI2NTAxGD3mBhZvdyYGL+N0n1D/NAjV0zCpTsFKKSzdpIrQJ0nM5Y0HxlLi6TsHm61dMyXHkdHb0ut8AzTcGA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/keyring-linux-arm64-musl": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-arm64-musl/-/keyring-linux-arm64-musl-1.1.6.tgz", - "integrity": "sha512-SB/2A4LtL+SrS2aZXl3rWBtyCVB2aG2zAU56kOGFDGwRZM2tqaITuQoM1QLOAMwu0eksN/Xedy95Yn2rkRH0nQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/keyring-linux-riscv64-gnu": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-riscv64-gnu/-/keyring-linux-riscv64-gnu-1.1.6.tgz", - "integrity": "sha512-BcjXf33T2CoVgS87SvZ62Y6xxkbenNIeldy0r8O5nz6zFgN+wYB0scz5ulvowEYBQnhi4fmbxfneeqM/0HUOeA==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/keyring-linux-x64-gnu": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-x64-gnu/-/keyring-linux-x64-gnu-1.1.6.tgz", - "integrity": "sha512-eK0OxCBI6Wl8rFHYynrtEID6pxOwhPfnpIIpul7UPeqCCMJSyZpFN4lFP3oZ4vqX/6FnWjwMrR7IGbPgivdMjA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/keyring-linux-x64-musl": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-x64-musl/-/keyring-linux-x64-musl-1.1.6.tgz", - "integrity": "sha512-Qb3NP98KFq4jXmk9PUQlcYrHjbzsBTtG+OOxX4YxUNKTGuUaIOGP79lB0w7jhns2oHdq8DwkW2ugzlmGSUaRSw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/keyring-win32-arm64-msvc": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@napi-rs/keyring-win32-arm64-msvc/-/keyring-win32-arm64-msvc-1.1.6.tgz", - "integrity": "sha512-e794gO2CLD0P7JN2DVPT5CC60k3WmNWTWU5BVoQM8Hj0NYebx7j6LyxMIpdb2cztOHHiv7iltEHekgutf0TMlA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/keyring-win32-ia32-msvc": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@napi-rs/keyring-win32-ia32-msvc/-/keyring-win32-ia32-msvc-1.1.6.tgz", - "integrity": "sha512-SUPafl6vKRMQBKZoSwIeBFZ+c7AGEKUy6mpAD9fVHDKHOBWP3VpHKda4YIlgGtQd3SxH0bjfqJ078Z5SYsDYZQ==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/keyring-win32-x64-msvc": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@napi-rs/keyring-win32-x64-msvc/-/keyring-win32-x64-msvc-1.1.6.tgz", - "integrity": "sha512-FkNhM1x5ijFzGSrRcshRxUxQSrrjxl4wCmvRcXnimWreOHyzNotT+/1EZtSfM/k8yhdK0HEkkVIMQl0UqfioRw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", diff --git a/package.json b/package.json index b91e20f6..80c2b88d 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,6 @@ "dependencies": { "@mongodb-js/devtools-connect": "^3.7.2", "@mongosh/service-provider-node-driver": "^3.6.0", - "@napi-rs/keyring": "^1.1.6", "@types/express": "^5.0.1", "bson": "^6.10.3", "mongodb": "^6.15.0", diff --git a/src/common/atlas/apiClient.ts b/src/common/atlas/apiClient.ts index 7384bec9..3198ea70 100644 --- a/src/common/atlas/apiClient.ts +++ b/src/common/atlas/apiClient.ts @@ -1,9 +1,11 @@ import config from "../../config.js"; -import createClient, { FetchOptions, Middleware } from "openapi-fetch"; +import createClient, { Client, FetchOptions, Middleware } from "openapi-fetch"; import { AccessToken, ClientCredentials } from "simple-oauth2"; import { paths, operations } from "./openapi.js"; +const ATLAS_API_VERSION = "2025-03-12"; + export class ApiClientError extends Error { response?: Response; @@ -25,38 +27,32 @@ export class ApiClientError extends Error { } export interface ApiClientOptions { - credentials: { + credentials?: { clientId: string; clientSecret: string; }; baseUrl?: string; + userAgent?: string; } export class ApiClient { - private client = createClient({ - baseUrl: config.apiBaseUrl, - headers: { - "User-Agent": config.userAgent, - Accept: `application/vnd.atlas.${config.atlasApiVersion}+json`, - }, - }); - private oauth2Client = new ClientCredentials({ - client: { - id: this.options.credentials.clientId, - secret: this.options.credentials.clientSecret, - }, - auth: { - tokenHost: this.options.baseUrl || config.apiBaseUrl, - tokenPath: "/api/oauth/token", - }, - }); + private options: { + baseUrl: string; + userAgent: string; + credentials?: { + clientId: string; + clientSecret: string; + }; + }; + private client: Client; + private oauth2Client?: ClientCredentials; private accessToken?: AccessToken; private getAccessToken = async () => { - if (!this.accessToken || this.accessToken.expired()) { + if (this.oauth2Client && (!this.accessToken || this.accessToken.expired())) { this.accessToken = await this.oauth2Client.getToken({}); } - return this.accessToken.token.access_token; + return this.accessToken?.token.access_token as string | undefined; }; private authMiddleware = (apiClient: ApiClient): Middleware => ({ @@ -82,8 +78,37 @@ export class ApiClient { }, }); - constructor(private options: ApiClientOptions) { - this.client.use(this.authMiddleware(this)); + constructor(options?: ApiClientOptions) { + const defaultOptions = { + baseUrl: "https://cloud.mongodb.com/", + userAgent: `AtlasMCP/${config.version} (${process.platform}; ${process.arch}; ${process.env.HOSTNAME || "unknown"})`, + }; + + this.options = { + ...defaultOptions, + ...options, + }; + + this.client = createClient({ + baseUrl: this.options.baseUrl, + headers: { + "User-Agent": this.options.userAgent, + Accept: `application/vnd.atlas.${ATLAS_API_VERSION}+json`, + }, + }); + if (this.options.credentials?.clientId && this.options.credentials?.clientSecret) { + this.oauth2Client = new ClientCredentials({ + client: { + id: this.options.credentials.clientId, + secret: this.options.credentials.clientSecret, + }, + auth: { + tokenHost: this.options.baseUrl, + tokenPath: "/api/oauth/token", + }, + }); + this.client.use(this.authMiddleware(this)); + } this.client.use(this.errorMiddleware()); } @@ -91,13 +116,13 @@ export class ApiClient { const accessToken = await this.getAccessToken(); const endpoint = "api/private/ipinfo"; - const url = new URL(endpoint, config.apiBaseUrl); + const url = new URL(endpoint, this.options.baseUrl); const response = await fetch(url, { method: "GET", headers: { Accept: "application/json", Authorization: `Bearer ${accessToken}`, - "User-Agent": config.userAgent, + "User-Agent": this.options.userAgent, }, }); diff --git a/src/config.ts b/src/config.ts index acadcf9e..ecdf32ad 100644 --- a/src/config.ts +++ b/src/config.ts @@ -3,17 +3,15 @@ import os from "os"; import argv from "yargs-parser"; import packageJson from "../package.json" with { type: "json" }; -import fs from "fs"; import { ReadConcernLevel, ReadPreferenceMode, W } from "mongodb"; -const { localDataPath, configPath } = getLocalDataPath(); // If we decide to support non-string config options, we'll need to extend the mechanism for parsing // env variables. interface UserConfig { - apiBaseUrl: string; + apiBaseUrl?: string; apiClientId?: string; apiClientSecret?: string; - stateFile: string; + logPath: string; connectionString?: string; connectOptions: { readConcern: ReadConcernLevel; @@ -24,8 +22,7 @@ interface UserConfig { } const defaults: UserConfig = { - apiBaseUrl: "https://cloud.mongodb.com/", - stateFile: path.join(localDataPath, "state.json"), + logPath: getLogPath(), connectOptions: { readConcern: "local", readPreference: "secondaryPreferred", @@ -36,43 +33,26 @@ const defaults: UserConfig = { const mergedUserConfig = { ...defaults, - ...getFileConfig(), ...getEnvConfig(), ...getCliConfig(), }; const config = { ...mergedUserConfig, - atlasApiVersion: `2025-03-12`, version: packageJson.version, - userAgent: `AtlasMCP/${packageJson.version} (${process.platform}; ${process.arch}; ${process.env.HOSTNAME || "unknown"})`, - localDataPath, }; export default config; -function getLocalDataPath(): { localDataPath: string; configPath: string } { - let localDataPath: string | undefined; - let configPath: string | undefined; +function getLogPath(): string { + const localDataPath = + process.platform === "win32" + ? path.join(process.env.LOCALAPPDATA || process.env.APPDATA || os.homedir(), "mongodb") + : path.join(os.homedir(), ".mongodb"); - if (process.platform === "win32") { - const appData = process.env.APPDATA; - const localAppData = process.env.LOCALAPPDATA ?? process.env.APPDATA; - if (localAppData && appData) { - localDataPath = path.join(localAppData, "mongodb", "mongodb-mcp"); - configPath = path.join(localDataPath, "mongodb-mcp.conf"); - } - } - - localDataPath ??= path.join(os.homedir(), ".mongodb", "mongodb-mcp"); - configPath ??= "/etc/mongodb-mcp.conf"; - - fs.mkdirSync(localDataPath, { recursive: true }); + const logPath = path.join(localDataPath, "mongodb-mcp", ".app-logs"); - return { - localDataPath, - configPath, - }; + return logPath; } // Gets the config supplied by the user as environment variables. The variable names @@ -125,17 +105,6 @@ function SNAKE_CASE_toCamelCase(str: string): string { return str.toLowerCase().replace(/([-_][a-z])/g, (group) => group.toUpperCase().replace("_", "")); } -// 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 { - try { - const config = fs.readFileSync(configPath, "utf8"); - return JSON.parse(config); - } catch { - return {}; - } -} - // Reads the cli args and parses them into a UserConfig object. function getCliConfig() { return argv(process.argv.slice(2)) as unknown as Partial; diff --git a/src/logger.ts b/src/logger.ts index d5415a74..532ff506 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -1,8 +1,7 @@ +import fs from "fs"; import { MongoLogId, MongoLogManager, MongoLogWriter } from "mongodb-log-writer"; -import path from "path"; import config from "./config.js"; import redact from "mongodb-redact"; -import fs from "fs/promises"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { LoggingMessageNotification } from "@modelcontextprotocol/sdk/types.js"; @@ -99,12 +98,23 @@ class ProxyingLogger extends LoggerBase { const logger = new ProxyingLogger(); export default logger; +async function mkdirPromise(path: fs.PathLike, options?: fs.Mode | fs.MakeDirectoryOptions) { + return new Promise((resolve, reject) => { + fs.mkdir(path, options, (err, resultPath) => { + if (err) { + reject(err); + } else { + resolve(resultPath); + } + }); + }); +} + export async function initializeLogger(server: McpServer): Promise { - const logDir = path.join(config.localDataPath, ".app-logs"); - await fs.mkdir(logDir, { recursive: true }); + await mkdirPromise(config.logPath, { recursive: true }); const manager = new MongoLogManager({ - directory: path.join(config.localDataPath, ".app-logs"), + directory: config.logPath, retentionDays: 30, onwarn: console.warn, onerror: console.error, diff --git a/src/server.ts b/src/server.ts index bf4ca16f..5530658c 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,5 +1,4 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { ApiClient } from "./common/atlas/apiClient.js"; import defaultState, { State } from "./state.js"; import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; import { registerAtlasTools } from "./tools/atlas/tools.js"; @@ -10,44 +9,21 @@ import { mongoLogId } from "mongodb-log-writer"; export class Server { state: State = defaultState; - apiClient?: ApiClient; - initialized: boolean = false; private server?: McpServer; - private async init() { - if (this.initialized) { - return; - } - - await this.state.loadCredentials(); - - if (config.apiClientId && config.apiClientSecret) { - this.apiClient = new ApiClient({ - credentials: { - clientId: config.apiClientId!, - clientSecret: config.apiClientSecret, - }, - }); - } - - this.initialized = true; - } - async connect(transport: Transport) { - await this.init(); - const server = new McpServer({ + this.server = new McpServer({ name: "MongoDB Atlas", version: config.version, }); - server.server.registerCapabilities({ logging: {} }); + this.server.server.registerCapabilities({ logging: {} }); - registerAtlasTools(server, this.state, this.apiClient); - registerMongoDBTools(server, this.state); + registerAtlasTools(this.server, this.state); + registerMongoDBTools(this.server, this.state); - await server.connect(transport); - await initializeLogger(server); - this.server = server; + await initializeLogger(this.server); + await this.server.connect(transport); logger.info(mongoLogId(1_000_004), "server", `Server started with transport ${transport.constructor.name}`); } diff --git a/src/state.ts b/src/state.ts index bdd167b0..f4e694bd 100644 --- a/src/state.ts +++ b/src/state.ts @@ -1,33 +1,26 @@ import { NodeDriverServiceProvider } from "@mongosh/service-provider-node-driver"; -import { AsyncEntry } from "@napi-rs/keyring"; -import logger from "./logger.js"; -import { mongoLogId } from "mongodb-log-writer"; - -interface Credentials { - connectionString?: string; -} +import { ApiClient } from "./common/atlas/apiClient.js"; +import config from "./config.js"; export class State { - private entry = new AsyncEntry("mongodb-mcp", "credentials"); - credentials: Credentials = {}; serviceProvider?: NodeDriverServiceProvider; + apiClient?: ApiClient; - public async persistCredentials(): Promise { - try { - await this.entry.setPassword(JSON.stringify(this.credentials)); - } catch (err) { - logger.error(mongoLogId(1_000_008), "state", `Failed to save state: ${err}`); - } - } - - public async loadCredentials(): Promise { - try { - const data = await this.entry.getPassword(); - if (data) { - this.credentials = JSON.parse(data); + ensureApiClient(): asserts this is { apiClient: ApiClient } { + if (!this.apiClient) { + if (!config.apiClientId || !config.apiClientSecret) { + throw new Error( + "Not authenticated make sure to configure MCP server with MDB_MCP_API_CLIENT_ID and MDB_MCP_API_CLIENT_SECRET environment variables." + ); } - } catch (err: unknown) { - logger.error(mongoLogId(1_000_007), "state", `Failed to load state: ${err}`); + + this.apiClient = new ApiClient({ + baseUrl: config.apiBaseUrl, + credentials: { + clientId: config.apiClientId, + clientSecret: config.apiClientSecret, + }, + }); } } } diff --git a/src/tools/atlas/atlasTool.ts b/src/tools/atlas/atlasTool.ts index 0b40d088..4aef681c 100644 --- a/src/tools/atlas/atlasTool.ts +++ b/src/tools/atlas/atlasTool.ts @@ -1,20 +1,8 @@ import { ToolBase } from "../tool.js"; -import { ApiClient } from "../../common/atlas/apiClient.js"; import { State } from "../../state.js"; export abstract class AtlasToolBase extends ToolBase { - constructor( - state: State, - protected apiClient?: ApiClient - ) { + constructor(state: State) { super(state); } - - protected ensureAuthenticated(): void { - if (!this.apiClient) { - throw new Error( - "Not authenticated make sure to configure MCP server with MDB_MCP_API_CLIENT_ID and MDB_MCP_API_CLIENT_SECRET environment variables." - ); - } - } } diff --git a/src/tools/atlas/createAccessList.ts b/src/tools/atlas/createAccessList.ts index f91509a4..09a991c8 100644 --- a/src/tools/atlas/createAccessList.ts +++ b/src/tools/atlas/createAccessList.ts @@ -26,7 +26,7 @@ export class CreateAccessListTool extends AtlasToolBase { comment, currentIpAddress, }: ToolArgs): Promise { - this.ensureAuthenticated(); + this.state.ensureApiClient(); if (!ipAddresses?.length && !cidrBlocks?.length && !currentIpAddress) { throw new Error("One of ipAddresses, cidrBlocks, currentIpAddress must be provided."); @@ -39,7 +39,7 @@ export class CreateAccessListTool extends AtlasToolBase { })); if (currentIpAddress) { - const currentIp = await this.apiClient!.getIpInfo(); + const currentIp = await this.state.apiClient.getIpInfo(); const input = { groupId: projectId, ipAddress: currentIp.currentIpv4Address, @@ -56,7 +56,7 @@ export class CreateAccessListTool extends AtlasToolBase { const inputs = [...ipInputs, ...cidrInputs]; - await this.apiClient!.createProjectIpAccessList({ + await this.state.apiClient.createProjectIpAccessList({ params: { path: { groupId: projectId, diff --git a/src/tools/atlas/createDBUser.ts b/src/tools/atlas/createDBUser.ts index fe605a0f..2698f0d8 100644 --- a/src/tools/atlas/createDBUser.ts +++ b/src/tools/atlas/createDBUser.ts @@ -33,7 +33,7 @@ export class CreateDBUserTool extends AtlasToolBase { roles, clusters, }: ToolArgs): Promise { - this.ensureAuthenticated(); + this.state.ensureApiClient(); const input = { groupId: projectId, @@ -53,7 +53,7 @@ export class CreateDBUserTool extends AtlasToolBase { : undefined, } as CloudDatabaseUser; - await this.apiClient!.createDatabaseUser({ + await this.state.apiClient.createDatabaseUser({ params: { path: { groupId: projectId, diff --git a/src/tools/atlas/createFreeCluster.ts b/src/tools/atlas/createFreeCluster.ts index 5339c5d2..8179883f 100644 --- a/src/tools/atlas/createFreeCluster.ts +++ b/src/tools/atlas/createFreeCluster.ts @@ -14,7 +14,7 @@ export class CreateFreeClusterTool extends AtlasToolBase { }; protected async execute({ projectId, name, region }: ToolArgs): Promise { - this.ensureAuthenticated(); + this.state.ensureApiClient(); const input = { groupId: projectId, @@ -38,7 +38,7 @@ export class CreateFreeClusterTool extends AtlasToolBase { terminationProtectionEnabled: false, } as unknown as ClusterDescription20240805; - await this.apiClient!.createCluster({ + await this.state.apiClient.createCluster({ params: { path: { groupId: projectId, diff --git a/src/tools/atlas/inspectAccessList.ts b/src/tools/atlas/inspectAccessList.ts index d1497ee6..c66cf5dc 100644 --- a/src/tools/atlas/inspectAccessList.ts +++ b/src/tools/atlas/inspectAccessList.ts @@ -11,9 +11,9 @@ export class InspectAccessListTool extends AtlasToolBase { }; protected async execute({ projectId }: ToolArgs): Promise { - this.ensureAuthenticated(); + this.state.ensureApiClient(); - const accessList = await this.apiClient!.listProjectIpAccessLists({ + const accessList = await this.state.apiClient.listProjectIpAccessLists({ params: { path: { groupId: projectId, diff --git a/src/tools/atlas/inspectCluster.ts b/src/tools/atlas/inspectCluster.ts index f0bda92d..9ad35a46 100644 --- a/src/tools/atlas/inspectCluster.ts +++ b/src/tools/atlas/inspectCluster.ts @@ -13,9 +13,9 @@ export class InspectClusterTool extends AtlasToolBase { }; protected async execute({ projectId, clusterName }: ToolArgs): Promise { - this.ensureAuthenticated(); + this.state.ensureApiClient(); - const cluster = await this.apiClient!.getCluster({ + const cluster = await this.state.apiClient.getCluster({ params: { path: { groupId: projectId, diff --git a/src/tools/atlas/listClusters.ts b/src/tools/atlas/listClusters.ts index 31dea9e5..eb38f74e 100644 --- a/src/tools/atlas/listClusters.ts +++ b/src/tools/atlas/listClusters.ts @@ -12,14 +12,14 @@ export class ListClustersTool extends AtlasToolBase { }; protected async execute({ projectId }: ToolArgs): Promise { - this.ensureAuthenticated(); + this.state.ensureApiClient(); if (!projectId) { - const data = await this.apiClient!.listClustersForAllProjects(); + const data = await this.state.apiClient.listClustersForAllProjects(); return this.formatAllClustersTable(data); } else { - const project = await this.apiClient!.getProject({ + const project = await this.state.apiClient.getProject({ params: { path: { groupId: projectId, @@ -31,7 +31,7 @@ export class ListClustersTool extends AtlasToolBase { throw new Error(`Project with ID "${projectId}" not found.`); } - const data = await this.apiClient!.listClusters({ + const data = await this.state.apiClient.listClusters({ params: { path: { groupId: project.id || "", diff --git a/src/tools/atlas/listDBUsers.ts b/src/tools/atlas/listDBUsers.ts index 46404123..d9712b1e 100644 --- a/src/tools/atlas/listDBUsers.ts +++ b/src/tools/atlas/listDBUsers.ts @@ -12,9 +12,9 @@ export class ListDBUsersTool extends AtlasToolBase { }; protected async execute({ projectId }: ToolArgs): Promise { - this.ensureAuthenticated(); + this.state.ensureApiClient(); - const data = await this.apiClient!.listDatabaseUsers({ + const data = await this.state.apiClient.listDatabaseUsers({ params: { path: { groupId: projectId, diff --git a/src/tools/atlas/listProjects.ts b/src/tools/atlas/listProjects.ts index d6b16f89..17fa6668 100644 --- a/src/tools/atlas/listProjects.ts +++ b/src/tools/atlas/listProjects.ts @@ -7,9 +7,9 @@ export class ListProjectsTool extends AtlasToolBase { protected argsShape = {}; protected async execute(): Promise { - this.ensureAuthenticated(); + this.state.ensureApiClient(); - const data = await this.apiClient!.listProjects(); + const data = await this.state.apiClient.listProjects(); if (!data?.results?.length) { throw new Error("No projects found in your MongoDB Atlas account."); diff --git a/src/tools/atlas/tools.ts b/src/tools/atlas/tools.ts index 41845730..5e717306 100644 --- a/src/tools/atlas/tools.ts +++ b/src/tools/atlas/tools.ts @@ -1,5 +1,4 @@ import { ToolBase } from "../tool.js"; -import { ApiClient } from "../../common/atlas/apiClient.js"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { State } from "../../state.js"; import { ListClustersTool } from "./listClusters.js"; @@ -11,16 +10,16 @@ import { InspectAccessListTool } from "./inspectAccessList.js"; import { ListDBUsersTool } from "./listDBUsers.js"; import { CreateDBUserTool } from "./createDBUser.js"; -export function registerAtlasTools(server: McpServer, state: State, apiClient?: ApiClient) { +export function registerAtlasTools(server: McpServer, state: State) { const tools: ToolBase[] = [ - new ListClustersTool(state, apiClient), - new ListProjectsTool(state, apiClient), - new InspectClusterTool(state, apiClient), - new CreateFreeClusterTool(state, apiClient), - new CreateAccessListTool(state, apiClient), - new InspectAccessListTool(state, apiClient), - new ListDBUsersTool(state, apiClient), - new CreateDBUserTool(state, apiClient), + new ListClustersTool(state), + new ListProjectsTool(state), + new InspectClusterTool(state), + new CreateFreeClusterTool(state), + new CreateAccessListTool(state), + new InspectAccessListTool(state), + new ListDBUsersTool(state), + new CreateDBUserTool(state), ]; for (const tool of tools) { diff --git a/src/tools/mongodb/connect.ts b/src/tools/mongodb/connect.ts index 3ed18fcd..dfba9926 100644 --- a/src/tools/mongodb/connect.ts +++ b/src/tools/mongodb/connect.ts @@ -20,7 +20,7 @@ export class ConnectTool extends MongoDBToolBase { protected async execute({ connectionStringOrClusterName, }: ToolArgs): Promise { - connectionStringOrClusterName ??= config.connectionString || this.state.credentials.connectionString; + connectionStringOrClusterName ??= config.connectionString; if (!connectionStringOrClusterName) { return { content: [ diff --git a/src/tools/mongodb/mongodbTool.ts b/src/tools/mongodb/mongodbTool.ts index fe9f3813..9c09caf0 100644 --- a/src/tools/mongodb/mongodbTool.ts +++ b/src/tools/mongodb/mongodbTool.ts @@ -68,7 +68,5 @@ export abstract class MongoDBToolBase extends ToolBase { }); state.serviceProvider = provider; - state.credentials.connectionString = connectionString; - await state.persistCredentials(); } } diff --git a/tests/integration/helpers.ts b/tests/integration/helpers.ts index d20da21a..a08b3eea 100644 --- a/tests/integration/helpers.ts +++ b/tests/integration/helpers.ts @@ -4,23 +4,12 @@ import { Server } from "../../src/server.js"; import runner, { MongoCluster } from "mongodb-runner"; import path from "path"; import fs from "fs/promises"; -import defaultState from "../../src/state.js"; -export async function setupIntegrationTest({ mockStateStore = true }: { mockStateStore?: boolean } = {}): Promise<{ +export async function setupIntegrationTest(): Promise<{ client: Client; server: Server; teardown: () => Promise; }> { - let loadCredentialsMock: jest.SpyInstance | undefined; - let saveCredentialsMock: jest.SpyInstance | undefined; - if (mockStateStore) { - // Mock the load/persist credentials method to avoid state loading/restore messing up with the tests - loadCredentialsMock = jest.spyOn(defaultState, "loadCredentials").mockImplementation(() => Promise.resolve()); - saveCredentialsMock = jest - .spyOn(defaultState, "persistCredentials") - .mockImplementation(() => Promise.resolve()); - } - const clientTransport = new InMemoryTransport(); const serverTransport = new InMemoryTransport(); @@ -50,9 +39,6 @@ export async function setupIntegrationTest({ mockStateStore = true }: { mockStat teardown: async () => { await client.close(); await server.close(); - - loadCredentialsMock?.mockRestore(); - saveCredentialsMock?.mockRestore(); }, }; }