From 2febc5de923f6be7a360d7921c9dcae30b941462 Mon Sep 17 00:00:00 2001 From: gagik Date: Thu, 10 Apr 2025 19:41:31 +0200 Subject: [PATCH 01/13] refactor: remove type assertions and simplify state setup --- eslint.config.js | 6 ++++ src/common/atlas/apiClient.ts | 13 +++++++ src/index.ts | 38 +++++++++++++++----- src/server.ts | 61 --------------------------------- src/state.ts | 3 -- src/tools/atlas/createDBUser.ts | 2 +- src/tools/atlas/listClusters.ts | 8 ++--- src/tools/atlas/listDBUsers.ts | 2 +- src/tools/atlas/listProjects.ts | 6 ++-- tests/unit/index.test.ts | 6 ++-- tsconfig.json | 1 + 11 files changed, 60 insertions(+), 86 deletions(-) delete mode 100644 src/server.ts diff --git a/eslint.config.js b/eslint.config.js index e93a22bf..3784279d 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -9,5 +9,11 @@ export default defineConfig([ { files: ["src/**/*.ts"], languageOptions: { globals: globals.node } }, tseslint.configs.recommended, eslintConfigPrettier, + { + files: ["src/**/*.ts"], + rules: { + "@typescript-eslint/no-non-null-assertion": "error", + }, + }, globalIgnores(["node_modules", "dist"]), ]); diff --git a/src/common/atlas/apiClient.ts b/src/common/atlas/apiClient.ts index 0c6615d7..66cf7f4f 100644 --- a/src/common/atlas/apiClient.ts +++ b/src/common/atlas/apiClient.ts @@ -2,6 +2,7 @@ import config from "../../config.js"; import createClient, { FetchOptions, Middleware } from "openapi-fetch"; import { paths, operations } from "./openapi.js"; +import { State } from "../../state.js"; export interface OAuthToken { access_token: string; @@ -85,6 +86,18 @@ export class ApiClient { this.client.use(this.errorMiddleware()); } + static fromState(state: State): ApiClient { + return new ApiClient({ + token: state.credentials.auth.token, + saveToken: async (token) => { + state.credentials.auth.code = undefined; + state.credentials.auth.token = token; + state.credentials.auth.status = "issued"; + await state.persistCredentials(); + }, + }); + } + async storeToken(token: OAuthToken): Promise { this.token = token; diff --git a/src/index.ts b/src/index.ts index 39fb0bc8..a355b97d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,17 +1,37 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { Server } from "./server.js"; import logger from "./logger.js"; import { mongoLogId } from "mongodb-log-writer"; +import { ApiClient } from "./common/atlas/apiClient.js"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import config from "./config.js"; +import { State } from "./state.js"; +import { registerAtlasTools } from "./tools/atlas/tools.js"; +import { registerMongoDBTools } from "./tools/mongodb/index.js"; export async function runServer() { - const server = new Server(); + try { + const state = new State(); + await state.loadCredentials(); - const transport = new StdioServerTransport(); - await server.connect(transport); -} + const apiClient = ApiClient.fromState(state); + + const mcp = new McpServer({ + name: "MongoDB Atlas", + version: config.version, + }); + + mcp.server.registerCapabilities({ logging: {} }); -runServer().catch((error) => { - logger.emergency(mongoLogId(1_000_004), "server", `Fatal error running server: ${error}`); + const transport = new StdioServerTransport(); + await mcp.server.connect(transport); + + registerAtlasTools(mcp, state, apiClient); + registerMongoDBTools(mcp, state); + } catch (error) { + logger.emergency(mongoLogId(1_000_004), "server", `Fatal error running server: ${error}`); + + process.exit(1); + } +} - process.exit(1); -}); +runServer(); diff --git a/src/server.ts b/src/server.ts deleted file mode 100644 index 0415f038..00000000 --- a/src/server.ts +++ /dev/null @@ -1,61 +0,0 @@ -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"; -import { registerMongoDBTools } from "./tools/mongodb/index.js"; -import config from "./config.js"; -import logger, { initializeLogger } from "./logger.js"; -import { mongoLogId } from "mongodb-log-writer"; - -export class Server { - state: State = defaultState; - apiClient: ApiClient | undefined = undefined; - initialized: boolean = false; - - private async init() { - if (this.initialized) { - return; - } - - await this.state.loadCredentials(); - - this.apiClient = new ApiClient({ - token: this.state.credentials.auth.token, - saveToken: async (token) => { - if (!this.state) { - throw new Error("State is not initialized"); - } - this.state.credentials.auth.code = undefined; - this.state.credentials.auth.token = token; - this.state.credentials.auth.status = "issued"; - await this.state.persistCredentials(); - }, - }); - - this.initialized = true; - } - - private createMcpServer(): McpServer { - const server = new McpServer({ - name: "MongoDB Atlas", - version: config.version, - }); - - server.server.registerCapabilities({ logging: {} }); - - registerAtlasTools(server, this.state, this.apiClient!); - registerMongoDBTools(server, this.state); - - return server; - } - - async connect(transport: Transport) { - await this.init(); - const server = this.createMcpServer(); - await server.connect(transport); - await initializeLogger(server); - - 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 9cc79626..6d293a27 100644 --- a/src/state.ts +++ b/src/state.ts @@ -40,6 +40,3 @@ export class State { } } } - -const defaultState = new State(); -export default defaultState; diff --git a/src/tools/atlas/createDBUser.ts b/src/tools/atlas/createDBUser.ts index d2a3b5d6..47459b0e 100644 --- a/src/tools/atlas/createDBUser.ts +++ b/src/tools/atlas/createDBUser.ts @@ -53,7 +53,7 @@ export class CreateDBUserTool extends AtlasToolBase { : undefined, } as CloudDatabaseUser; - await this.apiClient!.createDatabaseUser({ + await this.apiClient.createDatabaseUser({ params: { path: { groupId: projectId, diff --git a/src/tools/atlas/listClusters.ts b/src/tools/atlas/listClusters.ts index eda4d420..caeecb6f 100644 --- a/src/tools/atlas/listClusters.ts +++ b/src/tools/atlas/listClusters.ts @@ -47,8 +47,8 @@ export class ListClustersTool extends AtlasToolBase { if (!clusters?.results?.length) { throw new Error("No clusters found."); } - const rows = clusters - .results!.map((result) => { + const rows = clusters.results + .map((result) => { return (result.clusters || []).map((cluster) => { return { ...result, ...cluster, clusters: undefined }; }); @@ -75,8 +75,8 @@ ${rows}`, if (!clusters?.results?.length) { throw new Error("No clusters found."); } - const rows = clusters - .results!.map((cluster) => { + const rows = clusters.results + .map((cluster) => { const connectionString = cluster.connectionStrings?.standard || "N/A"; const mongoDBVersion = cluster.mongoDBVersion || "N/A"; return `${cluster.name} | ${cluster.stateName} | ${mongoDBVersion} | ${connectionString}`; diff --git a/src/tools/atlas/listDBUsers.ts b/src/tools/atlas/listDBUsers.ts index d49d981b..d4f1e19b 100644 --- a/src/tools/atlas/listDBUsers.ts +++ b/src/tools/atlas/listDBUsers.ts @@ -14,7 +14,7 @@ export class ListDBUsersTool extends AtlasToolBase { protected async execute({ projectId }: ToolArgs): Promise { await this.ensureAuthenticated(); - const data = await this.apiClient!.listDatabaseUsers({ + const data = await this.apiClient.listDatabaseUsers({ params: { path: { groupId: projectId, diff --git a/src/tools/atlas/listProjects.ts b/src/tools/atlas/listProjects.ts index 6b4b7d4a..438a3cd8 100644 --- a/src/tools/atlas/listProjects.ts +++ b/src/tools/atlas/listProjects.ts @@ -9,15 +9,15 @@ export class ListProjectsTool extends AtlasToolBase { protected async execute(): Promise { await this.ensureAuthenticated(); - const data = await this.apiClient!.listProjects(); + const data = await this.apiClient.listProjects(); if (!data?.results?.length) { throw new Error("No projects found in your MongoDB Atlas account."); } // Format projects as a table - const rows = data! - .results!.map((project) => { + const rows = data.results + .map((project) => { const createdAt = project.created ? new Date(project.created).toLocaleString() : "N/A"; return `${project.name} | ${project.id} | ${createdAt}`; }) diff --git a/tests/unit/index.test.ts b/tests/unit/index.test.ts index 8773fd75..f87229b9 100644 --- a/tests/unit/index.test.ts +++ b/tests/unit/index.test.ts @@ -1,6 +1,5 @@ import { describe, it } from "@jest/globals"; -import { runServer } from "../../src/index"; -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; // mock the StdioServerTransport jest.mock("@modelcontextprotocol/sdk/server/stdio"); @@ -21,7 +20,6 @@ jest.mock("../../src/server.ts", () => { describe("Server initialization", () => { it("should create a server instance", async () => { - await runServer(); - expect(StdioServerTransport).toHaveBeenCalled(); + await expect(StdioServerTransport).toHaveBeenCalled(); }); }); diff --git a/tsconfig.json b/tsconfig.json index a195f859..1fe57f10 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,6 +6,7 @@ "rootDir": "./src", "outDir": "./dist", "strict": true, + "strictNullChecks": true, "esModuleInterop": true, "types": ["node"], "sourceMap": true, From 6c8752bc806bc2152b1f98da2bef4c2c542aa9dd Mon Sep 17 00:00:00 2001 From: gagik Date: Thu, 10 Apr 2025 21:10:22 +0200 Subject: [PATCH 02/13] fix: tests and token refactor --- src/common/atlas/apiClient.ts | 48 +++++++++++++++++++---------------- src/index.ts | 36 ++++++++++++-------------- tests/unit/index.test.ts | 29 +++++++-------------- 3 files changed, 51 insertions(+), 62 deletions(-) diff --git a/src/common/atlas/apiClient.ts b/src/common/atlas/apiClient.ts index 66cf7f4f..e930d7bb 100644 --- a/src/common/atlas/apiClient.ts +++ b/src/common/atlas/apiClient.ts @@ -51,39 +51,40 @@ export interface ApiClientOptions { export class ApiClient { private token?: OAuthToken; - private saveToken?: saveTokenFunction; - private client = createClient({ + private readonly saveToken?: saveTokenFunction; + private readonly client = createClient({ baseUrl: config.apiBaseUrl, headers: { "User-Agent": config.userAgent, Accept: `application/vnd.atlas.${config.atlasApiVersion}+json`, }, }); - private authMiddleware = (apiClient: ApiClient): Middleware => ({ - async onRequest({ request, schemaPath }) { + + private readonly authMiddleware: Middleware = { + onRequest: async ({ request, schemaPath }) => { if (schemaPath.startsWith("/api/private/unauth") || schemaPath.startsWith("/api/oauth")) { return undefined; } - if (await apiClient.validateToken()) { - request.headers.set("Authorization", `Bearer ${apiClient.token!.access_token}`); + if (this.token && (await this.validateToken())) { + request.headers.set("Authorization", `Bearer ${this.token.access_token}`); return request; } }, - }); - private errorMiddleware = (): Middleware => ({ + }; + private readonly errorMiddleware: Middleware = { async onResponse({ response }) { if (!response.ok) { throw await ApiClientError.fromResponse(response); } }, - }); + }; constructor(options: ApiClientOptions) { const { token, saveToken } = options; this.token = token; this.saveToken = saveToken; - this.client.use(this.authMiddleware(this)); - this.client.use(this.errorMiddleware()); + this.client.use(this.authMiddleware); + this.client.use(this.errorMiddleware); } static fromState(state: State): ApiClient { @@ -173,7 +174,7 @@ export class ApiClient { } } - async refreshToken(token?: OAuthToken): Promise { + async refreshToken(token: OAuthToken): Promise { const endpoint = "api/private/unauth/account/device/token"; const url = new URL(endpoint, config.apiBaseUrl); const response = await fetch(url, { @@ -184,7 +185,7 @@ export class ApiClient { }, body: new URLSearchParams({ client_id: config.clientId, - refresh_token: (token || this.token)?.refresh_token || "", + refresh_token: token.refresh_token, grant_type: "refresh_token", scope: "openid profile offline_access", }).toString(), @@ -207,7 +208,7 @@ export class ApiClient { return await this.storeToken(tokenToStore); } - async revokeToken(token?: OAuthToken): Promise { + async revokeToken(token: OAuthToken): Promise { const endpoint = "api/private/unauth/account/device/token"; const url = new URL(endpoint, config.apiBaseUrl); const response = await fetch(url, { @@ -219,7 +220,7 @@ export class ApiClient { }, body: new URLSearchParams({ client_id: config.clientId, - token: (token || this.token)?.access_token || "", + token: token.access_token || "", token_type_hint: "refresh_token", }).toString(), }); @@ -235,9 +236,8 @@ export class ApiClient { return; } - private checkTokenExpiry(token?: OAuthToken): boolean { + private checkTokenExpiry(token: OAuthToken): boolean { try { - token = token || this.token; if (!token || !token.access_token) { return false; } @@ -252,13 +252,17 @@ export class ApiClient { } } - async validateToken(token?: OAuthToken): Promise { - if (this.checkTokenExpiry(token)) { + async validateToken(): Promise { + if (!this.token) { + return false; + } + + if (this.checkTokenExpiry(this.token)) { return true; } try { - await this.refreshToken(token); + await this.refreshToken(this.token); return true; } catch { return false; @@ -266,7 +270,7 @@ export class ApiClient { } async getIpInfo() { - if (!(await this.validateToken())) { + if (!this.token || !(await this.validateToken())) { throw new Error("Not Authenticated"); } @@ -276,7 +280,7 @@ export class ApiClient { method: "GET", headers: { Accept: "application/json", - Authorization: `Bearer ${this.token!.access_token}`, + Authorization: `Bearer ${this.token.access_token}`, "User-Agent": config.userAgent, }, }); diff --git a/src/index.ts b/src/index.ts index a355b97d..dfff1475 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,30 +8,26 @@ import { State } from "./state.js"; import { registerAtlasTools } from "./tools/atlas/tools.js"; import { registerMongoDBTools } from "./tools/mongodb/index.js"; -export async function runServer() { - try { - const state = new State(); - await state.loadCredentials(); +try { + const state = new State(); + await state.loadCredentials(); - const apiClient = ApiClient.fromState(state); + const apiClient = ApiClient.fromState(state); - const mcp = new McpServer({ - name: "MongoDB Atlas", - version: config.version, - }); + const mcp = new McpServer({ + name: "MongoDB Atlas", + version: config.version, + }); - mcp.server.registerCapabilities({ logging: {} }); + mcp.server.registerCapabilities({ logging: {} }); - const transport = new StdioServerTransport(); - await mcp.server.connect(transport); + registerAtlasTools(mcp, state, apiClient); + registerMongoDBTools(mcp, state); - registerAtlasTools(mcp, state, apiClient); - registerMongoDBTools(mcp, state); - } catch (error) { - logger.emergency(mongoLogId(1_000_004), "server", `Fatal error running server: ${error}`); + const transport = new StdioServerTransport(); + await mcp.server.connect(transport); +} catch (error) { + logger.emergency(mongoLogId(1_000_004), "server", `Fatal error running server: ${error}`); - process.exit(1); - } + process.exit(1); } - -runServer(); diff --git a/tests/unit/index.test.ts b/tests/unit/index.test.ts index f87229b9..a1b413f2 100644 --- a/tests/unit/index.test.ts +++ b/tests/unit/index.test.ts @@ -1,25 +1,14 @@ import { describe, it } from "@jest/globals"; -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; - -// mock the StdioServerTransport -jest.mock("@modelcontextprotocol/sdk/server/stdio"); -// mock Server class and its methods -jest.mock("../../src/server.ts", () => { - return { - Server: jest.fn().mockImplementation(() => { - return { - connect: jest.fn().mockImplementation((transport) => { - return new Promise((resolve) => { - resolve(transport); - }); - }), - }; - }), - }; -}); +import { State } from "../../src/state"; describe("Server initialization", () => { - it("should create a server instance", async () => { - await expect(StdioServerTransport).toHaveBeenCalled(); + it("should define a default state", async () => { + const state = new State(); + + expect(state.credentials).toEqual({ + auth: { + status: "not_auth", + }, + }); }); }); From cfc8d897a49f51561fdda14c4b73fbd97114d57c Mon Sep 17 00:00:00 2001 From: gagik Date: Fri, 11 Apr 2025 09:07:26 +0200 Subject: [PATCH 03/13] refactor: re-introduce the server class --- src/index.ts | 18 +++++++++------- src/server.ts | 58 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 8 deletions(-) create mode 100644 src/server.ts diff --git a/src/index.ts b/src/index.ts index dfff1475..1b1761be 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,8 +5,7 @@ import { ApiClient } from "./common/atlas/apiClient.js"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import config from "./config.js"; import { State } from "./state.js"; -import { registerAtlasTools } from "./tools/atlas/tools.js"; -import { registerMongoDBTools } from "./tools/mongodb/index.js"; +import { Server } from "./server.js"; try { const state = new State(); @@ -14,18 +13,21 @@ try { const apiClient = ApiClient.fromState(state); - const mcp = new McpServer({ + const mcpServer = new McpServer({ name: "MongoDB Atlas", version: config.version, }); - mcp.server.registerCapabilities({ logging: {} }); + const transport = new StdioServerTransport(); - registerAtlasTools(mcp, state, apiClient); - registerMongoDBTools(mcp, state); + const server = new Server({ + mcpServer, + state, + apiClient, + transport, + }); - const transport = new StdioServerTransport(); - await mcp.server.connect(transport); + await server.registerAndConnect(); } catch (error) { logger.emergency(mongoLogId(1_000_004), "server", `Fatal error running server: ${error}`); diff --git a/src/server.ts b/src/server.ts new file mode 100644 index 00000000..1d34eb97 --- /dev/null +++ b/src/server.ts @@ -0,0 +1,58 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { ApiClient } from "./common/atlas/apiClient.js"; +import { State } 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 logger, { initializeLogger } from "./logger.js"; +import { mongoLogId } from "mongodb-log-writer"; + +export class Server { + readonly state: State; + private readonly apiClient: ApiClient; + private readonly mcpServer: McpServer; + private readonly transport: Transport; + + constructor({ + state, + apiClient, + mcpServer, + transport, + }: { + state: State; + apiClient: ApiClient; + mcpServer: McpServer; + transport: Transport; + }) { + this.state = state; + this.apiClient = apiClient; + this.mcpServer = mcpServer; + this.transport = transport; + } + + async registerAndConnect() { + this.mcpServer.server.registerCapabilities({ logging: {} }); + + registerAtlasTools(this.mcpServer, this.state, this.apiClient); + registerMongoDBTools(this.mcpServer, this.state); + + await this.mcpServer.connect(this.transport); + + await initializeLogger(this.mcpServer); + + logger.info( + mongoLogId(1_000_004), + "server", + `Server started with transport ${this.transport.constructor.name}` + ); + } + + async close(): Promise { + try { + await this.state.serviceProvider?.close(true); + } catch { + // Ignore errors during service provider close + } + await this.mcpServer.close(); + } +} From 0f032889b6e641f4521f3e48397b8387a82ecdd5 Mon Sep 17 00:00:00 2001 From: gagik Date: Fri, 11 Apr 2025 14:11:39 +0200 Subject: [PATCH 04/13] chore: add type-powered eslint rules --- .prettierignore | 1 + .prettierrc.json | 7 + eslint.config.js | 29 +- package-lock.json | 14 +- package.json | 5 +- src/common/atlas/apiClient.ts | 15 +- src/common/atlas/openapi.d.ts | 392 ++++-------------- src/config.ts | 2 +- src/index.ts | 2 + src/logger.ts | 4 +- src/server.ts | 2 +- src/state.ts | 6 +- src/tools/atlas/listClusters.ts | 8 +- src/tools/atlas/listProjects.ts | 4 +- src/tools/mongodb/create/insertOne.ts | 2 +- .../mongodb/metadata/collectionSchema.ts | 3 +- .../mongodb/metadata/collectionStorageSize.ts | 4 +- src/tools/mongodb/metadata/listDatabases.ts | 2 +- src/tools/mongodb/update/updateMany.ts | 2 +- src/tools/mongodb/update/updateOne.ts | 2 +- src/tools/tool.ts | 21 +- 21 files changed, 168 insertions(+), 359 deletions(-) diff --git a/.prettierignore b/.prettierignore index ffb0dcd0..52be6ef1 100644 --- a/.prettierignore +++ b/.prettierignore @@ -2,3 +2,4 @@ dist coverage package-lock.json tests/tmp +src/common/atlas/openapi.d.ts diff --git a/.prettierrc.json b/.prettierrc.json index 076027c3..a8d4dcc3 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -5,6 +5,13 @@ "singleQuote": false, "printWidth": 120, "overrides": [ + { + "files": "*.ts", + "options": { + "insertPragma": false, + "proseWrap": "preserve" + } + }, { "files": "*.json", "options": { diff --git a/eslint.config.js b/eslint.config.js index e93a22bf..18f564c7 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -4,10 +4,31 @@ import globals from "globals"; import tseslint from "typescript-eslint"; import eslintConfigPrettier from "eslint-config-prettier/flat"; +const files = ["src/**/*.ts", "scripts/**/*.ts", "tests/**/*.test.ts", "eslint.config.js", "jest.config.js"]; + export default defineConfig([ - { files: ["src/**/*.ts"], plugins: { js }, extends: ["js/recommended"] }, - { files: ["src/**/*.ts"], languageOptions: { globals: globals.node } }, - tseslint.configs.recommended, + { files, plugins: { js }, extends: ["js/recommended"] }, + { files, languageOptions: { globals: globals.node } }, + tseslint.configs.recommendedTypeChecked, + { + languageOptions: { + parserOptions: { + projectService: true, + tsconfigRootDir: import.meta.dirname, + }, + }, + }, + { + files, + rules: { + "@typescript-eslint/switch-exhaustiveness-check": "error", + }, + }, + // Ignore features specific to TypeScript resolved rules + tseslint.config({ + // TODO: Configure tests and scripts to work with this. + ignores: ["eslint.config.js", "jest.config.js", "tests/**/*.ts", "scripts/**/*.ts"], + }), + globalIgnores(["node_modules", "dist", "src/common/atlas/openapi.d.ts"]), eslintConfigPrettier, - globalIgnores(["node_modules", "dist"]), ]); diff --git a/package-lock.json b/package-lock.json index 27ab6a0f..40c33480 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,6 @@ "@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", "mongodb-log-writer": "^2.4.1", @@ -32,6 +31,7 @@ "@modelcontextprotocol/inspector": "^0.8.2", "@modelcontextprotocol/sdk": "^1.8.0", "@redocly/cli": "^1.34.2", + "@types/express": "^5.0.1", "@types/jest": "^29.5.14", "@types/node": "^22.14.0", "@types/simple-oauth2": "^5.0.7", @@ -6060,6 +6060,7 @@ "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, "license": "MIT", "dependencies": { "@types/connect": "*", @@ -6070,6 +6071,7 @@ "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -6086,6 +6088,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.1.tgz", "integrity": "sha512-UZUw8vjpWFXuDnjFTh7/5c2TWDlQqeXHi6hcN7F2XSVT5P+WmUnnbFS3KA6Jnc6IsEqI2qCVu2bK0R0J4A8ZQQ==", + "dev": true, "license": "MIT", "dependencies": { "@types/body-parser": "*", @@ -6097,6 +6100,7 @@ "version": "5.0.6", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz", "integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==", + "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", @@ -6119,6 +6123,7 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true, "license": "MIT" }, "node_modules/@types/istanbul-lib-coverage": { @@ -6170,12 +6175,14 @@ "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, "license": "MIT" }, "node_modules/@types/node": { "version": "22.14.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.0.tgz", "integrity": "sha512-Kmpl+z84ILoG+3T/zQFyAJsU6EPTmOCj8/2+83fSN6djd6I4o7uOuGIH6vq3PrjY5BGitSbFuMN18j3iknubbA==", + "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -6192,18 +6199,21 @@ "version": "6.9.18", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==", + "dev": true, "license": "MIT" }, "node_modules/@types/range-parser": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, "license": "MIT" }, "node_modules/@types/send": { "version": "0.17.4", "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, "license": "MIT", "dependencies": { "@types/mime": "^1", @@ -6214,6 +6224,7 @@ "version": "1.15.7", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dev": true, "license": "MIT", "dependencies": { "@types/http-errors": "*", @@ -14798,6 +14809,7 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, "license": "MIT" }, "node_modules/unpipe": { diff --git a/package.json b/package.json index b91e20f6..31c9a38e 100644 --- a/package.json +++ b/package.json @@ -19,9 +19,8 @@ "prepare": "npm run build", "build:clean": "rm -rf dist", "build:compile": "tsc", - "build:addshebang": "echo '#!/usr/bin/env node' > dist/index2.js && cat dist/index.js >> dist/index2.js && mv dist/index2.js dist/index.js", "build:chmod": "chmod +x dist/index.js", - "build": "npm run build:clean && npm run build:compile && npm run build:addshebang && npm run build:chmod", + "build": "npm run build:clean && npm run build:compile && npm run build:chmod", "inspect": "npm run build && mcp-inspector -- dist/index.js", "prettier": "prettier", "check": "npm run build && npm run check:lint && npm run check:format", @@ -38,6 +37,7 @@ "@modelcontextprotocol/inspector": "^0.8.2", "@modelcontextprotocol/sdk": "^1.8.0", "@redocly/cli": "^1.34.2", + "@types/express": "^5.0.1", "@types/jest": "^29.5.14", "@types/node": "^22.14.0", "@types/simple-oauth2": "^5.0.7", @@ -61,7 +61,6 @@ "@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", "mongodb-log-writer": "^2.4.1", diff --git a/src/common/atlas/apiClient.ts b/src/common/atlas/apiClient.ts index 7384bec9..dd4e699b 100644 --- a/src/common/atlas/apiClient.ts +++ b/src/common/atlas/apiClient.ts @@ -52,12 +52,12 @@ export class ApiClient { }); private accessToken?: AccessToken; - private getAccessToken = async () => { + private async getAccessToken(): Promise { if (!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; + } private authMiddleware = (apiClient: ApiClient): Middleware => ({ async onRequest({ request, schemaPath }) { @@ -87,7 +87,9 @@ export class ApiClient { this.client.use(this.errorMiddleware()); } - async getIpInfo() { + public async getIpInfo(): Promise<{ + currentIpv4Address: string; + }> { const accessToken = await this.getAccessToken(); const endpoint = "api/private/ipinfo"; @@ -105,10 +107,9 @@ export class ApiClient { throw await ApiClientError.fromResponse(response); } - const responseBody = await response.json(); - return responseBody as { + return (await response.json()) as Promise<{ currentIpv4Address: string; - }; + }>; } async listProjects(options?: FetchOptions) { diff --git a/src/common/atlas/openapi.d.ts b/src/common/atlas/openapi.d.ts index 8ff7878c..c55f53ae 100644 --- a/src/common/atlas/openapi.d.ts +++ b/src/common/atlas/openapi.d.ts @@ -241,13 +241,7 @@ export interface components { * @enum {string} */ providerName: "AWS"; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - providerName: "AWS"; - }; + } ; AWSCloudProviderSettings: Omit & { autoScaling?: components["schemas"]["CloudProviderAWSAutoScaling"]; /** @@ -346,13 +340,7 @@ export interface components { * @enum {string} */ providerName: "AWS"; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - providerName: "AWS"; - }; + } ; /** * AWS * @description Collection of settings that configures how a cluster might scale its cluster tier and whether the cluster can scale down. Cluster tier auto-scaling is unavailable for clusters using Low CPU or NVME storage classes. @@ -643,13 +631,7 @@ export interface components { * @enum {string} */ providerName: "AWS"; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - providerName: "AWS"; - }; + } ; /** * AWS Regional Replication Specifications * @description Details that explain how MongoDB Cloud replicates data in one region on the specified MongoDB database. @@ -665,13 +647,7 @@ export interface components { * @enum {string} */ providerName: "AWS"; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - providerName: "AWS"; - }; + } ; /** * Automatic Scaling Settings * @description Options that determine how this cluster handles resource scaling. @@ -919,13 +895,7 @@ export interface components { * @enum {string} */ providerName: "AZURE"; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - providerName: "AZURE"; - }; + } ; AzureCloudProviderSettings: Omit & { autoScaling?: components["schemas"]["CloudProviderAzureAutoScaling"]; /** @@ -1022,13 +992,7 @@ export interface components { * @enum {string} */ providerName: "AZURE"; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - providerName: "AZURE"; - }; + } ; /** * Azure * @description Collection of settings that configures how a cluster might scale its cluster tier and whether the cluster can scale down. Cluster tier auto-scaling is unavailable for clusters using Low CPU or NVME storage classes. @@ -1246,13 +1210,7 @@ export interface components { * @enum {string} */ providerName: "AZURE"; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - providerName: "AZURE"; - }; + } ; /** * Azure Regional Replication Specifications * @description Details that explain how MongoDB Cloud replicates data in one region on the specified MongoDB database. @@ -1268,13 +1226,7 @@ export interface components { * @enum {string} */ providerName: "AZURE"; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - providerName: "AZURE"; - }; + } ; /** @description Bad request detail. */ BadRequestDetail: { /** @description Describes all violations in a client request. */ @@ -1310,49 +1262,49 @@ export interface components { | "M400_NVME" ) | ( - | "M10" - | "M20" - | "M30" - | "M40" - | "M50" - | "M60" - | "M80" + + + + + + + | "M90" - | "M200" - | "R40" - | "R50" - | "R60" - | "R80" - | "R200" - | "R300" - | "R400" - | "M60_NVME" - | "M80_NVME" - | "M200_NVME" + + + + + + + + + + + | "M300_NVME" - | "M400_NVME" + | "M600_NVME" ) | ( - | "M10" - | "M20" - | "M30" - | "M40" - | "M50" - | "M60" - | "M80" - | "M140" - | "M200" + + + + + + + + + | "M250" - | "M300" + | "M400" - | "R40" - | "R50" - | "R60" - | "R80" - | "R200" - | "R300" - | "R400" + + + + + + + | "R600" ); BasicDBObject: { @@ -1832,13 +1784,7 @@ export interface components { * @enum {string} */ providerName: "GCP"; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - providerName: "GCP"; - }; + } ; /** @description Range of instance sizes to which your cluster can scale. */ CloudProviderAWSAutoScaling: { compute?: components["schemas"]["AWSComputeAutoScaling"]; @@ -2258,14 +2204,14 @@ export interface components { | ( | "US_CENTRAL" | "US_EAST" - | "US_EAST_2" + | "US_NORTH_CENTRAL" | "US_WEST" | "US_SOUTH_CENTRAL" | "EUROPE_NORTH" | "EUROPE_WEST" | "US_WEST_CENTRAL" - | "US_WEST_2" + | "US_WEST_3" | "CANADA_EAST" | "CANADA_CENTRAL" @@ -2313,9 +2259,9 @@ export interface components { | "US_EAST_4_AW" | "US_EAST_5" | "US_EAST_5_AW" - | "US_WEST_2" + | "US_WEST_2_AW" - | "US_WEST_3" + | "US_WEST_3_AW" | "US_WEST_4" | "US_WEST_4_AW" @@ -2426,14 +2372,14 @@ export interface components { | ( | "US_CENTRAL" | "US_EAST" - | "US_EAST_2" + | "US_NORTH_CENTRAL" | "US_WEST" | "US_SOUTH_CENTRAL" | "EUROPE_NORTH" | "EUROPE_WEST" | "US_WEST_CENTRAL" - | "US_WEST_2" + | "US_WEST_3" | "CANADA_EAST" | "CANADA_CENTRAL" @@ -2481,9 +2427,9 @@ export interface components { | "US_EAST_4_AW" | "US_EAST_5" | "US_EAST_5_AW" - | "US_WEST_2" + | "US_WEST_2_AW" - | "US_WEST_3" + | "US_WEST_3_AW" | "US_WEST_4" | "US_WEST_4_AW" @@ -2756,13 +2702,7 @@ export interface components { * @enum {string} */ providerName: "FLEX"; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - providerName: "FLEX"; - }; + } ; /** @description Range of instance sizes to which your cluster can scale. */ ClusterFreeAutoScaling: { compute?: components["schemas"]["FreeComputeAutoScalingRules"]; @@ -2795,13 +2735,7 @@ export interface components { * @enum {string} */ providerName: "TENANT"; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - providerName: "TENANT"; - }; + } ; /** * Cloud Service Provider Settings for a Cluster * @description Group of cloud provider settings that configure the provisioned MongoDB hosts. @@ -3007,13 +2941,7 @@ export interface components { * @enum {string} */ type: "DAILY"; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - type: "DAILY"; - }; + } ; DataLakeAtlasStoreInstance: Omit & { /** @description Human-readable label of the MongoDB Cloud cluster on which the store is based. */ clusterName?: string; @@ -3027,13 +2955,7 @@ export interface components { * @enum {string} */ provider: "atlas"; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - provider: "atlas"; - }; + } ; /** @description MongoDB Cloud cluster read concern, which determines the consistency and isolation properties of the data read from an Atlas cluster. */ DataLakeAtlasStoreReadConcern: { /** @@ -3140,13 +3062,7 @@ export interface components { * @enum {string} */ provider: "azure"; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - provider: "azure"; - }; + } ; DataLakeDLSAWSStore: Omit & { /** * AWS Regions @@ -3194,13 +3110,7 @@ export interface components { * @enum {string} */ provider: "dls:aws"; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - provider: "dls:aws"; - }; + } ; DataLakeDLSAzureStore: Omit & { /** * Azure Regions @@ -3263,13 +3173,7 @@ export interface components { * @enum {string} */ provider: "dls:azure"; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - provider: "dls:azure"; - }; + } ; DataLakeDLSGCPStore: Omit & { /** * GCP Regions @@ -3319,13 +3223,7 @@ export interface components { * @enum {string} */ provider: "dls:gcp"; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - provider: "dls:gcp"; - }; + } ; DataLakeGoogleCloudStorageStore: Omit & { /** @description Human-readable label that identifies the Google Cloud Storage bucket. */ bucket?: string; @@ -3388,13 +3286,7 @@ export interface components { * @enum {string} */ provider: "gcs"; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - provider: "gcs"; - }; + } ; DataLakeHTTPStore: Omit & { /** * @description Flag that validates the scheme in the specified URLs. If `true`, allows insecure `HTTP` scheme, doesn't verify the server's certificate chain and hostname, and accepts any certificate with any hostname presented by the server. If `false`, allows secure `HTTPS` scheme only. @@ -3411,13 +3303,7 @@ export interface components { * @enum {string} */ provider: "http"; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - provider: "http"; - }; + } ; /** * Partition Field * @description Partition Field in the Data Lake Storage provider for a Data Lake Pipeline. @@ -3497,13 +3383,7 @@ export interface components { * @enum {string} */ provider: "s3"; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - provider: "s3"; - }; + } ; /** @description Group of settings that define where the data is stored. */ DataLakeStoreSettings: { /** @description Human-readable label that identifies the data store. The **databases.[n].collections.[n].dataSources.[n].storeName** field references this values as part of the mapping configuration. To use MongoDB Cloud as a data store, the data lake requires a serverless instance or an `M10` or higher cluster. */ @@ -3626,13 +3506,7 @@ export interface components { * @enum {string} */ type: "DEFAULT"; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - type: "DEFAULT"; - }; + } ; DiskBackupSnapshotAWSExportBucketRequest: Omit< WithRequired, "cloudProvider" @@ -3886,13 +3760,7 @@ export interface components { * @enum {string} */ providerName: "GCP"; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - providerName: "GCP"; - }; + } ; /** * GCP * @description Collection of settings that configures how a cluster might scale its cluster tier and whether the cluster can scale down. Cluster tier auto-scaling is unavailable for clusters using Low CPU or NVME storage classes. @@ -4075,13 +3943,7 @@ export interface components { * @enum {string} */ providerName: "GCP"; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - providerName: "GCP"; - }; + } ; /** * GCP Regional Replication Specifications * @description Details that explain how MongoDB Cloud replicates data in one region on the specified MongoDB database. @@ -4097,13 +3959,7 @@ export interface components { * @enum {string} */ providerName: "GCP"; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - providerName: "GCP"; - }; + } ; Group: { /** * Format: int64 @@ -4191,13 +4047,7 @@ export interface components { * @enum {string} */ orgMembershipStatus: "ACTIVE"; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - orgMembershipStatus: "ACTIVE"; - }; + } ; GroupPendingUserResponse: Omit< WithRequired< components["schemas"]["GroupUserResponse"], @@ -4226,13 +4076,7 @@ export interface components { * @enum {string} */ orgMembershipStatus: "PENDING"; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - orgMembershipStatus: "PENDING"; - }; + } ; GroupRoleAssignment: { /** * @description Unique 24-hexadecimal digit string that identifies the project to which these roles belong. @@ -4871,13 +4715,7 @@ export interface components { * @enum {string} */ type: "MONTHLY"; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - type: "MONTHLY"; - }; + } ; NetworkPermissionEntry: { /** @description Unique string of the Amazon Web Services (AWS) security group that you want to add to the project's IP access list. Your IP access list entry can be one **awsSecurityGroup**, one **cidrBlock**, or one **ipAddress**. You must configure Virtual Private Connection (VPC) peering for your project before you can add an AWS security group to an IP access list. You cannot set AWS security groups as temporary access list entries. Don't set this parameter if you set **cidrBlock** or **ipAddress**. */ awsSecurityGroup?: string; @@ -4976,13 +4814,7 @@ export interface components { * @enum {string} */ orgMembershipStatus: "ACTIVE"; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - orgMembershipStatus: "ACTIVE"; - }; + } ; OrgGroup: { /** @description Settings that describe the clusters in each project that the API key is authorized to view. */ readonly clusters?: components["schemas"]["CloudCluster"][]; @@ -5024,13 +4856,7 @@ export interface components { * @enum {string} */ orgMembershipStatus: "PENDING"; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - orgMembershipStatus: "PENDING"; - }; + } ; OrgUserResponse: { /** * @description Unique 24-hexadecimal digit string that identifies the MongoDB Cloud user. @@ -5605,13 +5431,7 @@ export interface components { * @enum {string} */ type: "AWSLambda"; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - type: "AWSLambda"; - }; + } ; StreamsClusterConnection: Omit & { /** @description Name of the cluster configured for this connection. */ clusterName?: string; @@ -5622,13 +5442,7 @@ export interface components { * @enum {string} */ type: "Cluster"; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - type: "Cluster"; - }; + } ; /** @description Settings that define a connection to an external data store. */ StreamsConnection: { /** @description List of one or more Uniform Resource Locators (URLs) that point to API sub-resources, related API resources, or both. RFC 5988 outlines these relationships. */ @@ -5661,13 +5475,7 @@ export interface components { * @enum {string} */ type: "Https"; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - type: "Https"; - }; + } ; /** @description User credentials required to connect to a Kafka Cluster. Includes the authentication type, as well as the parameters for that authentication mode. */ StreamsKafkaAuthentication: { /** @description List of one or more Uniform Resource Locators (URLs) that point to API sub-resources, related API resources, or both. RFC 5988 outlines these relationships. */ @@ -5710,13 +5518,7 @@ export interface components { * @enum {string} */ type: "Kafka"; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - type: "Kafka"; - }; + } ; /** @description Networking Access Type can either be 'PUBLIC' (default) or VPC. VPC type is in public preview, please file a support ticket to enable VPC Network Access. */ StreamsKafkaNetworking: { access?: components["schemas"]["StreamsKafkaNetworkingAccess"]; @@ -5759,26 +5561,14 @@ export interface components { * @enum {string} */ type: "S3"; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - type: "S3"; - }; + } ; StreamsSampleConnection: Omit & { /** * @description discriminator enum property added by openapi-typescript * @enum {string} */ type: "Sample"; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - type: "Sample"; - }; + } ; /** * Synonym Mapping Status Detail * @description Contains the status of the index's synonym mappings on each search host. This field (and its subfields) only appear if the index has synonyms defined. @@ -5863,13 +5653,7 @@ export interface components { * @enum {string} */ providerName: "TENANT"; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - providerName: "TENANT"; - }; + } ; /** * Tenant Regional Replication Specifications * @description Details that explain how MongoDB Cloud replicates data in one region on the specified MongoDB database. @@ -5888,13 +5672,7 @@ export interface components { * @enum {string} */ providerName: "TENANT"; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - providerName: "TENANT"; - }; + } ; /** Text Search Host Status Detail */ TextSearchHostStatusDetail: { /** @description Hostname that corresponds to the status detail. */ @@ -6403,13 +6181,7 @@ export interface components { * @enum {string} */ type: "WEEKLY"; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - type: "WEEKLY"; - }; + } ; /** * htmlStrip * @description Filter that strips out HTML constructs. diff --git a/src/config.ts b/src/config.ts index acadcf9e..d05e6419 100644 --- a/src/config.ts +++ b/src/config.ts @@ -130,7 +130,7 @@ function SNAKE_CASE_toCamelCase(str: string): string { function getFileConfig(): Partial { try { const config = fs.readFileSync(configPath, "utf8"); - return JSON.parse(config); + return JSON.parse(config) as Partial; } catch { return {}; } diff --git a/src/index.ts b/src/index.ts index 39fb0bc8..0f282e4d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,5 @@ +#!/usr/bin/env node + import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { Server } from "./server.js"; import logger from "./logger.js"; diff --git a/src/logger.ts b/src/logger.ts index d5415a74..9deaea08 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -45,7 +45,7 @@ abstract class LoggerBase { class ConsoleLogger extends LoggerBase { log(level: LogLevel, id: MongoLogId, context: string, message: string): void { message = redact(message); - console.error(`[${level.toUpperCase()}] ${id} - ${context}: ${message}`); + console.error(`[${level.toUpperCase()}] ${id.__value} - ${context}: ${message}`); } } @@ -61,7 +61,7 @@ class Logger extends LoggerBase { message = redact(message); const mongoDBLevel = this.mapToMongoDBLogLevel(level); this.logWriter[mongoDBLevel]("MONGODB-MCP", id, context, message); - this.server.server.sendLoggingMessage({ + void this.server.server.sendLoggingMessage({ level, data: `[${context}]: ${message}`, }); diff --git a/src/server.ts b/src/server.ts index bf4ca16f..e45d40d0 100644 --- a/src/server.ts +++ b/src/server.ts @@ -24,7 +24,7 @@ export class Server { if (config.apiClientId && config.apiClientSecret) { this.apiClient = new ApiClient({ credentials: { - clientId: config.apiClientId!, + clientId: config.apiClientId, clientSecret: config.apiClientSecret, }, }); diff --git a/src/state.ts b/src/state.ts index bdd167b0..f87b276e 100644 --- a/src/state.ts +++ b/src/state.ts @@ -16,7 +16,7 @@ export class State { try { await this.entry.setPassword(JSON.stringify(this.credentials)); } catch (err) { - logger.error(mongoLogId(1_000_008), "state", `Failed to save state: ${err}`); + logger.error(mongoLogId(1_000_008), "state", `Failed to save state: ${err as string}`); } } @@ -24,10 +24,10 @@ export class State { try { const data = await this.entry.getPassword(); if (data) { - this.credentials = JSON.parse(data); + this.credentials = JSON.parse(data) as Credentials; } } catch (err: unknown) { - logger.error(mongoLogId(1_000_007), "state", `Failed to load state: ${err}`); + logger.error(mongoLogId(1_000_007), "state", `Failed to load state: ${err as string}`); } } } diff --git a/src/tools/atlas/listClusters.ts b/src/tools/atlas/listClusters.ts index 31dea9e5..72329081 100644 --- a/src/tools/atlas/listClusters.ts +++ b/src/tools/atlas/listClusters.ts @@ -47,8 +47,8 @@ export class ListClustersTool extends AtlasToolBase { if (!clusters?.results?.length) { throw new Error("No clusters found."); } - const rows = clusters - .results!.map((result) => { + const rows = clusters.results + .map((result) => { return (result.clusters || []).map((cluster) => { return { ...result, ...cluster, clusters: undefined }; }); @@ -75,8 +75,8 @@ ${rows}`, if (!clusters?.results?.length) { throw new Error("No clusters found."); } - const rows = clusters - .results!.map((cluster) => { + const rows = clusters.results + .map((cluster) => { const connectionString = cluster.connectionStrings?.standard || "N/A"; const mongoDBVersion = cluster.mongoDBVersion || "N/A"; return `${cluster.name} | ${cluster.stateName} | ${mongoDBVersion} | ${connectionString}`; diff --git a/src/tools/atlas/listProjects.ts b/src/tools/atlas/listProjects.ts index d6b16f89..301391f5 100644 --- a/src/tools/atlas/listProjects.ts +++ b/src/tools/atlas/listProjects.ts @@ -16,8 +16,8 @@ export class ListProjectsTool extends AtlasToolBase { } // Format projects as a table - const rows = data! - .results!.map((project) => { + const rows = data.results + .map((project) => { const createdAt = project.created ? new Date(project.created).toLocaleString() : "N/A"; return `${project.name} | ${project.id} | ${createdAt}`; }) diff --git a/src/tools/mongodb/create/insertOne.ts b/src/tools/mongodb/create/insertOne.ts index b490efd4..795c603e 100644 --- a/src/tools/mongodb/create/insertOne.ts +++ b/src/tools/mongodb/create/insertOne.ts @@ -29,7 +29,7 @@ export class InsertOneTool extends MongoDBToolBase { return { content: [ { - text: `Inserted document with ID \`${result.insertedId}\` into collection \`${collection}\``, + text: `Inserted document with ID \`${result.insertedId.toString()}\` into collection \`${collection}\``, type: "text", }, ], diff --git a/src/tools/mongodb/metadata/collectionSchema.ts b/src/tools/mongodb/metadata/collectionSchema.ts index f780252d..dfa45a6c 100644 --- a/src/tools/mongodb/metadata/collectionSchema.ts +++ b/src/tools/mongodb/metadata/collectionSchema.ts @@ -33,7 +33,8 @@ export class CollectionSchemaTool extends MongoDBToolBase { let result = "| Field | Type | Confidence |\n"; result += "|-------|------|-------------|\n"; for (const field of fields) { - result += `| ${field.name} | \`${field.type}\` | ${(field.probability * 100).toFixed(0)}% |\n`; + const fieldType = Array.isArray(field.type) ? field.type.join(", ") : field.type; + result += `| ${field.name} | \`${fieldType}\` | ${(field.probability * 100).toFixed(0)}% |\n`; } return result; } diff --git a/src/tools/mongodb/metadata/collectionStorageSize.ts b/src/tools/mongodb/metadata/collectionStorageSize.ts index ab41261d..09b7e6d3 100644 --- a/src/tools/mongodb/metadata/collectionStorageSize.ts +++ b/src/tools/mongodb/metadata/collectionStorageSize.ts @@ -11,12 +11,12 @@ export class CollectionStorageSizeTool extends MongoDBToolBase { protected async execute({ database, collection }: ToolArgs): Promise { const provider = await this.ensureConnected(); - const [{ value }] = await provider + const [{ value }] = (await provider .aggregate(database, collection, [ { $collStats: { storageStats: {} } }, { $group: { _id: null, value: { $sum: "$storageStats.storageSize" } } }, ]) - .toArray(); + .toArray()) as [{ value: number }]; return { content: [ diff --git a/src/tools/mongodb/metadata/listDatabases.ts b/src/tools/mongodb/metadata/listDatabases.ts index 7a89d09c..bdbdd19a 100644 --- a/src/tools/mongodb/metadata/listDatabases.ts +++ b/src/tools/mongodb/metadata/listDatabases.ts @@ -16,7 +16,7 @@ export class ListDatabasesTool extends MongoDBToolBase { return { content: dbs.map((db) => { return { - text: `Name: ${db.name}, Size: ${db.sizeOnDisk} bytes`, + text: `Name: ${db.name}, Size: ${db.sizeOnDisk.toString()} bytes`, type: "text", }; }), diff --git a/src/tools/mongodb/update/updateMany.ts b/src/tools/mongodb/update/updateMany.ts index e692c36b..f02e2a62 100644 --- a/src/tools/mongodb/update/updateMany.ts +++ b/src/tools/mongodb/update/updateMany.ts @@ -49,7 +49,7 @@ export class UpdateManyTool extends MongoDBToolBase { message += ` Modified ${result.modifiedCount} document(s).`; } if (result.upsertedCount > 0) { - message += ` Upserted ${result.upsertedCount} document(s) (with id: ${result.upsertedId}).`; + message += ` Upserted ${result.upsertedCount} document(s) (with id: ${result.upsertedId?.toString()}).`; } } diff --git a/src/tools/mongodb/update/updateOne.ts b/src/tools/mongodb/update/updateOne.ts index b1a604c6..9d117b7b 100644 --- a/src/tools/mongodb/update/updateOne.ts +++ b/src/tools/mongodb/update/updateOne.ts @@ -49,7 +49,7 @@ export class UpdateOneTool extends MongoDBToolBase { message += ` Modified ${result.modifiedCount} document(s).`; } if (result.upsertedCount > 0) { - message += ` Upserted ${result.upsertedCount} document(s) (with id: ${result.upsertedId}).`; + message += ` Upserted ${result.upsertedCount} document(s) (with id: ${result.upsertedId?.toString()}).`; } } diff --git a/src/tools/tool.ts b/src/tools/tool.ts index c1889be5..d09be52d 100644 --- a/src/tools/tool.ts +++ b/src/tools/tool.ts @@ -1,4 +1,4 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { McpServer, ToolCallback } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z, ZodNever, ZodRawShape } from "zod"; import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import { State } from "../state.js"; @@ -14,12 +14,12 @@ export abstract class ToolBase { protected abstract argsShape: ZodRawShape; - protected abstract execute(args: ToolArgs): Promise; + protected abstract execute(...args: Parameters>): Promise; protected constructor(protected state: State) {} public register(server: McpServer): void { - const callback = async (args: ToolArgs): Promise => { + const callback: ToolCallback = async (...args) => { try { // TODO: add telemetry here logger.debug( @@ -28,22 +28,15 @@ export abstract class ToolBase { `Executing ${this.name} with args: ${JSON.stringify(args)}` ); - return await this.execute(args); - } catch (error) { - logger.error(mongoLogId(1_000_000), "tool", `Error executing ${this.name}: ${error}`); + return await this.execute(...args); + } catch (error: unknown) { + logger.error(mongoLogId(1_000_000), "tool", `Error executing ${this.name}: ${error as string}`); return await this.handleError(error); } }; - if (this.argsShape) { - // Not sure why typescript doesn't like the type signature of callback. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - server.tool(this.name, this.description, this.argsShape, callback as any); - } else { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - server.tool(this.name, this.description, callback as any); - } + server.tool(this.name, this.description, this.argsShape, callback); } // This method is intended to be overridden by subclasses to handle errors From 7e0a287678a95c3a8aeb9f282e858cfc71a8a462 Mon Sep 17 00:00:00 2001 From: gagik Date: Fri, 11 Apr 2025 14:28:25 +0200 Subject: [PATCH 05/13] fix: adapt the tst --- tests/unit/index.test.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/unit/index.test.ts b/tests/unit/index.test.ts index a748e870..53a8516b 100644 --- a/tests/unit/index.test.ts +++ b/tests/unit/index.test.ts @@ -1,6 +1,4 @@ import { describe, it } from "@jest/globals"; -import { runServer } from "../../src/index"; -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { State } from "../../src/state"; // mock the StdioServerTransport From 8062288868cf8406913b6a481ebb4ff87e2d1412 Mon Sep 17 00:00:00 2001 From: gagik Date: Fri, 11 Apr 2025 15:02:07 +0200 Subject: [PATCH 06/13] wip --- package-lock.json | 2 -- src/common/atlas/apiClient.ts | 8 ++++---- src/config.ts | 5 ++++- src/index.ts | 8 +------- src/server.ts | 12 +++++++++--- src/state.ts | 1 - src/tools/mongodb/connect.ts | 2 +- src/tools/mongodb/mongodbTool.ts | 20 ++++++++------------ src/tools/tool.ts | 3 +-- 9 files changed, 28 insertions(+), 33 deletions(-) diff --git a/package-lock.json b/package-lock.json index 73333384..db3e8841 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,8 +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", "mongodb-log-writer": "^2.4.1", diff --git a/src/common/atlas/apiClient.ts b/src/common/atlas/apiClient.ts index 1120f5fc..f079e375 100644 --- a/src/common/atlas/apiClient.ts +++ b/src/common/atlas/apiClient.ts @@ -3,7 +3,6 @@ import createClient, { Client, FetchOptions, Middleware } from "openapi-fetch"; import { AccessToken, ClientCredentials } from "simple-oauth2"; import { paths, operations } from "./openapi.js"; -import { State } from "../../state.js"; const ATLAS_API_VERSION = "2025-03-12"; @@ -70,7 +69,8 @@ export class ApiClient { // ignore not availble tokens, API will return 401 } }, - }; + }); + private readonly errorMiddleware: Middleware = { async onResponse({ response }) { if (!response.ok) { @@ -79,7 +79,7 @@ export class ApiClient { }, }; - constructor(options?: ApiClientOptions) { + constructor(options: ApiClientOptions) { const defaultOptions = { baseUrl: "https://cloud.mongodb.com/", userAgent: `AtlasMCP/${config.version} (${process.platform}; ${process.arch}; ${process.env.HOSTNAME || "unknown"})`, @@ -110,7 +110,7 @@ export class ApiClient { }); this.client.use(this.authMiddleware(this)); } - this.client.use(this.errorMiddleware()); + this.client.use(this.errorMiddleware); } public async getIpInfo(): Promise<{ diff --git a/src/config.ts b/src/config.ts index ecdf32ad..cce25722 100644 --- a/src/config.ts +++ b/src/config.ts @@ -60,7 +60,10 @@ function getLogPath(): string { // to SNAKE_UPPER_CASE. function getEnvConfig(): Partial { function setValue(obj: Record, path: string[], value: string): void { - const currentField = path.shift()!; + const currentField = path.shift(); + if (!currentField) { + return; + } if (path.length === 0) { const numberValue = Number(value); if (!isNaN(numberValue)) { diff --git a/src/index.ts b/src/index.ts index 99b8e7df..41a9bae3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,21 +11,15 @@ import { Server } from "./server.js"; try { const state = new State(); - await state.loadCredentials(); - - const apiClient = ApiClient.fromState(state); - const mcpServer = new McpServer({ name: "MongoDB Atlas", version: config.version, }); - const transport = new StdioServerTransport(); - const server = new Server({ + const server = new Seriver({ mcpServer, state, - apiClient, transport, }); diff --git a/src/server.ts b/src/server.ts index 434fb241..ecdd320c 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,14 +1,20 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import defaultState, { State } from "./state.js"; +import { State } 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 logger, { initializeLogger } from "./logger.js"; import { mongoLogId } from "mongodb-log-writer"; +import { ApiClient } from "./common/atlas/apiClient.js"; export class Server { - state: State = defaultState; - private server?: McpServer; + public readonly state: State; + private readonly mcpServer: McpServer; + + constructor({ mcpServer, state, transport }: { mcpServer: McpServer; state: State; transport: Transport }) { + this.mcpServer = server; + this.state = new State(); + } async connect(transport: Transport) { this.server = new McpServer({ diff --git a/src/state.ts b/src/state.ts index 320c35d3..67363149 100644 --- a/src/state.ts +++ b/src/state.ts @@ -3,7 +3,6 @@ import { ApiClient } from "./common/atlas/apiClient.js"; import config from "./config.js"; export class State { - serviceProvider?: NodeDriverServiceProvider; apiClient?: ApiClient; ensureApiClient(): asserts this is { apiClient: ApiClient } { diff --git a/src/tools/mongodb/connect.ts b/src/tools/mongodb/connect.ts index dfba9926..66df62e3 100644 --- a/src/tools/mongodb/connect.ts +++ b/src/tools/mongodb/connect.ts @@ -57,7 +57,7 @@ export class ConnectTool extends MongoDBToolBase { throw new MongoDBError(ErrorCodes.InvalidParams, "Invalid connection options"); } - await this.connectToMongoDB(connectionString, this.state); + await this.connectToMongoDB(connectionString); return { content: [{ type: "text", text: `Successfully connected to ${connectionString}.` }], diff --git a/src/tools/mongodb/mongodbTool.ts b/src/tools/mongodb/mongodbTool.ts index 9c09caf0..6fa4dd12 100644 --- a/src/tools/mongodb/mongodbTool.ts +++ b/src/tools/mongodb/mongodbTool.ts @@ -1,6 +1,5 @@ import { z } from "zod"; import { ToolBase } from "../tool.js"; -import { State } from "../../state.js"; import { NodeDriverServiceProvider } from "@mongosh/service-provider-node-driver"; import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import { ErrorCodes, MongoDBError } from "../../errors.js"; @@ -14,23 +13,22 @@ export const DbOperationArgs = { export type DbOperationType = "metadata" | "read" | "create" | "update" | "delete"; export abstract class MongoDBToolBase extends ToolBase { - constructor(state: State) { - super(state); + constructor(private serviceProvider: NodeDriverServiceProvider | undefined) { + super(); } protected abstract operationType: DbOperationType; protected async ensureConnected(): Promise { - const provider = this.state.serviceProvider; - if (!provider && config.connectionString) { - await this.connectToMongoDB(config.connectionString, this.state); + if (!this.serviceProvider && config.connectionString) { + this.serviceProvider = await this.connectToMongoDB(config.connectionString); } - if (!provider) { + if (!this.serviceProvider) { throw new MongoDBError(ErrorCodes.NotConnectedToMongoDB, "Not connected to MongoDB"); } - return provider; + return this.serviceProvider; } protected handleError(error: unknown): Promise | CallToolResult { @@ -53,8 +51,8 @@ export abstract class MongoDBToolBase extends ToolBase { return super.handleError(error); } - protected async connectToMongoDB(connectionString: string, state: State): Promise { - const provider = await NodeDriverServiceProvider.connect(connectionString, { + protected async connectToMongoDB(connectionString: string): Promise { + return NodeDriverServiceProvider.connect(connectionString, { productDocsLink: "https://docs.mongodb.com/todo-mcp", productName: "MongoDB MCP", readConcern: { @@ -66,7 +64,5 @@ export abstract class MongoDBToolBase extends ToolBase { }, timeoutMS: config.connectOptions.timeoutMS, }); - - state.serviceProvider = provider; } } diff --git a/src/tools/tool.ts b/src/tools/tool.ts index d09be52d..f3f3100d 100644 --- a/src/tools/tool.ts +++ b/src/tools/tool.ts @@ -1,7 +1,6 @@ import { McpServer, ToolCallback } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z, ZodNever, ZodRawShape } from "zod"; import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; -import { State } from "../state.js"; import logger from "../logger.js"; import { mongoLogId } from "mongodb-log-writer"; @@ -16,7 +15,7 @@ export abstract class ToolBase { protected abstract execute(...args: Parameters>): Promise; - protected constructor(protected state: State) {} + protected constructor() {} public register(server: McpServer): void { const callback: ToolCallback = async (...args) => { From 8e4f19c065c53406c713fe11ba9064d6174b6a0e Mon Sep 17 00:00:00 2001 From: gagik Date: Fri, 11 Apr 2025 15:43:17 +0200 Subject: [PATCH 07/13] refactor: adapt to latest structure --- src/index.ts | 4 +-- src/server.ts | 48 ++++++++++++++++------------- src/{state.ts => session.ts} | 3 +- src/tools/atlas/atlasTool.ts | 25 +++++++++++++-- src/tools/atlas/createDBUser.ts | 2 +- src/tools/atlas/tools.ts | 29 ++++++----------- src/tools/mongodb/index.ts | 53 ++++++++++++++------------------ src/tools/mongodb/mongodbTool.ts | 20 +++++++----- src/tools/tool.ts | 3 +- tests/unit/index.test.ts | 4 +-- 10 files changed, 104 insertions(+), 87 deletions(-) rename src/{state.ts => session.ts} (92%) diff --git a/src/index.ts b/src/index.ts index 41a9bae3..36a1a98e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,11 +6,11 @@ import { mongoLogId } from "mongodb-log-writer"; import { ApiClient } from "./common/atlas/apiClient.js"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import config from "./config.js"; -import { State } from "./state.js"; +import { Session } from "./session.js"; import { Server } from "./server.js"; try { - const state = new State(); + const state = new Session(); const mcpServer = new McpServer({ name: "MongoDB Atlas", version: config.version, diff --git a/src/server.ts b/src/server.ts index ecdd320c..d9e70224 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,44 +1,50 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { State } from "./state.js"; +import { Session } from "./session.js"; import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; -import { registerAtlasTools } from "./tools/atlas/tools.js"; -import { registerMongoDBTools } from "./tools/mongodb/index.js"; +import { AtlasTools } from "./tools/atlas/tools.js"; +import { MongoDbTools } from "./tools/mongodb/index.js"; import logger, { initializeLogger } from "./logger.js"; import { mongoLogId } from "mongodb-log-writer"; -import { ApiClient } from "./common/atlas/apiClient.js"; export class Server { - public readonly state: State; + public readonly session: Session; private readonly mcpServer: McpServer; + private readonly transport: Transport; - constructor({ mcpServer, state, transport }: { mcpServer: McpServer; state: State; transport: Transport }) { - this.mcpServer = server; - this.state = new State(); + constructor({ mcpServer, transport, session }: { mcpServer: McpServer; session: Session; transport: Transport }) { + this.mcpServer = mcpServer; + this.transport = transport; + this.session = session; } - async connect(transport: Transport) { - this.server = new McpServer({ - name: "MongoDB Atlas", - version: config.version, - }); + async connect() { + this.mcpServer.server.registerCapabilities({ logging: {} }); - this.server.server.registerCapabilities({ logging: {} }); + this.registerTools(); - registerAtlasTools(this.server, this.state); - registerMongoDBTools(this.server, this.state); + await initializeLogger(this.mcpServer); - await initializeLogger(this.server); - await this.server.connect(transport); + await this.mcpServer.connect(this.transport); - logger.info(mongoLogId(1_000_004), "server", `Server started with transport ${transport.constructor.name}`); + logger.info( + mongoLogId(1_000_004), + "server", + `Server started with transport ${this.transport.constructor.name}` + ); } async close(): Promise { try { - await this.state.serviceProvider?.close(true); + await this.session.serviceProvider?.close(true); } catch { // Ignore errors during service provider close } - await this.server?.close(); + await this.mcpServer?.close(); + } + + private registerTools() { + for (const tool of [...AtlasTools, ...MongoDbTools]) { + new tool(this.session).register(this.mcpServer); + } } } diff --git a/src/state.ts b/src/session.ts similarity index 92% rename from src/state.ts rename to src/session.ts index 67363149..b442b7f1 100644 --- a/src/state.ts +++ b/src/session.ts @@ -2,7 +2,8 @@ import { NodeDriverServiceProvider } from "@mongosh/service-provider-node-driver import { ApiClient } from "./common/atlas/apiClient.js"; import config from "./config.js"; -export class State { +export class Session { + serviceProvider?: NodeDriverServiceProvider; apiClient?: ApiClient; ensureApiClient(): asserts this is { apiClient: ApiClient } { diff --git a/src/tools/atlas/atlasTool.ts b/src/tools/atlas/atlasTool.ts index 4aef681c..a7809c39 100644 --- a/src/tools/atlas/atlasTool.ts +++ b/src/tools/atlas/atlasTool.ts @@ -1,8 +1,29 @@ import { ToolBase } from "../tool.js"; -import { State } from "../../state.js"; +import { ApiClient } from "../../common/atlas/apiClient.js"; +import { Session } from "../../session.js"; export abstract class AtlasToolBase extends ToolBase { - constructor(state: State) { + private apiClient?: ApiClient; + + 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." + ); + } + + this.apiClient = new ApiClient({ + baseUrl: config.apiBaseUrl, + credentials: { + clientId: config.apiClientId, + clientSecret: config.apiClientSecret, + }, + }); + } + } + + constructor(protected readonly session: Session) { super(state); } } diff --git a/src/tools/atlas/createDBUser.ts b/src/tools/atlas/createDBUser.ts index 2698f0d8..fe2948c3 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.state.ensureApiClient(); + this.ensureApiClient(); const input = { groupId: projectId, diff --git a/src/tools/atlas/tools.ts b/src/tools/atlas/tools.ts index 5e717306..4e7bd200 100644 --- a/src/tools/atlas/tools.ts +++ b/src/tools/atlas/tools.ts @@ -1,6 +1,3 @@ -import { ToolBase } from "../tool.js"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { State } from "../../state.js"; import { ListClustersTool } from "./listClusters.js"; import { ListProjectsTool } from "./listProjects.js"; import { InspectClusterTool } from "./inspectCluster.js"; @@ -10,19 +7,13 @@ import { InspectAccessListTool } from "./inspectAccessList.js"; import { ListDBUsersTool } from "./listDBUsers.js"; import { CreateDBUserTool } from "./createDBUser.js"; -export function registerAtlasTools(server: McpServer, state: State) { - const tools: ToolBase[] = [ - 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) { - tool.register(server); - } -} +export const AtlasTools = [ + ListClustersTool, + ListProjectsTool, + InspectClusterTool, + CreateFreeClusterTool, + CreateAccessListTool, + InspectAccessListTool, + ListDBUsersTool, + CreateDBUserTool, +]; diff --git a/src/tools/mongodb/index.ts b/src/tools/mongodb/index.ts index be30c494..abe13fa2 100644 --- a/src/tools/mongodb/index.ts +++ b/src/tools/mongodb/index.ts @@ -1,5 +1,5 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { State } from "../../state.js"; +import { State } from "../../session.js"; import { ConnectTool } from "./connect.js"; import { ListCollectionsTool } from "./metadata/listCollections.js"; import { CollectionIndexesTool } from "./collectionIndexes.js"; @@ -21,32 +21,25 @@ import { RenameCollectionTool } from "./update/renameCollection.js"; import { DropDatabaseTool } from "./delete/dropDatabase.js"; import { DropCollectionTool } from "./delete/dropCollection.js"; -export function registerMongoDBTools(server: McpServer, state: State) { - const tools = [ - ConnectTool, - ListCollectionsTool, - ListDatabasesTool, - CollectionIndexesTool, - CreateIndexTool, - CollectionSchemaTool, - InsertOneTool, - FindTool, - InsertManyTool, - DeleteManyTool, - DeleteOneTool, - CollectionStorageSizeTool, - CountTool, - DbStatsTool, - AggregateTool, - UpdateOneTool, - UpdateManyTool, - RenameCollectionTool, - DropDatabaseTool, - DropCollectionTool, - ]; - - for (const tool of tools) { - const instance = new tool(state); - instance.register(server); - } -} +export const MongoDbTools = [ + ConnectTool, + ListCollectionsTool, + ListDatabasesTool, + CollectionIndexesTool, + CreateIndexTool, + CollectionSchemaTool, + InsertOneTool, + FindTool, + InsertManyTool, + DeleteManyTool, + DeleteOneTool, + CollectionStorageSizeTool, + CountTool, + DbStatsTool, + AggregateTool, + UpdateOneTool, + UpdateManyTool, + RenameCollectionTool, + DropDatabaseTool, + DropCollectionTool, +]; diff --git a/src/tools/mongodb/mongodbTool.ts b/src/tools/mongodb/mongodbTool.ts index 6fa4dd12..9c09caf0 100644 --- a/src/tools/mongodb/mongodbTool.ts +++ b/src/tools/mongodb/mongodbTool.ts @@ -1,5 +1,6 @@ import { z } from "zod"; import { ToolBase } from "../tool.js"; +import { State } from "../../state.js"; import { NodeDriverServiceProvider } from "@mongosh/service-provider-node-driver"; import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import { ErrorCodes, MongoDBError } from "../../errors.js"; @@ -13,22 +14,23 @@ export const DbOperationArgs = { export type DbOperationType = "metadata" | "read" | "create" | "update" | "delete"; export abstract class MongoDBToolBase extends ToolBase { - constructor(private serviceProvider: NodeDriverServiceProvider | undefined) { - super(); + constructor(state: State) { + super(state); } protected abstract operationType: DbOperationType; protected async ensureConnected(): Promise { - if (!this.serviceProvider && config.connectionString) { - this.serviceProvider = await this.connectToMongoDB(config.connectionString); + const provider = this.state.serviceProvider; + if (!provider && config.connectionString) { + await this.connectToMongoDB(config.connectionString, this.state); } - if (!this.serviceProvider) { + if (!provider) { throw new MongoDBError(ErrorCodes.NotConnectedToMongoDB, "Not connected to MongoDB"); } - return this.serviceProvider; + return provider; } protected handleError(error: unknown): Promise | CallToolResult { @@ -51,8 +53,8 @@ export abstract class MongoDBToolBase extends ToolBase { return super.handleError(error); } - protected async connectToMongoDB(connectionString: string): Promise { - return NodeDriverServiceProvider.connect(connectionString, { + protected async connectToMongoDB(connectionString: string, state: State): Promise { + const provider = await NodeDriverServiceProvider.connect(connectionString, { productDocsLink: "https://docs.mongodb.com/todo-mcp", productName: "MongoDB MCP", readConcern: { @@ -64,5 +66,7 @@ export abstract class MongoDBToolBase extends ToolBase { }, timeoutMS: config.connectOptions.timeoutMS, }); + + state.serviceProvider = provider; } } diff --git a/src/tools/tool.ts b/src/tools/tool.ts index f3f3100d..bc07b87d 100644 --- a/src/tools/tool.ts +++ b/src/tools/tool.ts @@ -1,6 +1,7 @@ import { McpServer, ToolCallback } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z, ZodNever, ZodRawShape } from "zod"; import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import { State } from "../session.js"; import logger from "../logger.js"; import { mongoLogId } from "mongodb-log-writer"; @@ -15,7 +16,7 @@ export abstract class ToolBase { protected abstract execute(...args: Parameters>): Promise; - protected constructor() {} + protected constructor(protected state: State) {} public register(server: McpServer): void { const callback: ToolCallback = async (...args) => { diff --git a/tests/unit/index.test.ts b/tests/unit/index.test.ts index 53a8516b..a0bed5b3 100644 --- a/tests/unit/index.test.ts +++ b/tests/unit/index.test.ts @@ -1,5 +1,5 @@ import { describe, it } from "@jest/globals"; -import { State } from "../../src/state"; +import { Session } from "../../src/session"; // mock the StdioServerTransport jest.mock("@modelcontextprotocol/sdk/server/stdio"); @@ -20,7 +20,7 @@ jest.mock("../../src/server.ts", () => { describe("Server initialization", () => { it("should define a default state", async () => { - const state = new State(); + const state = new Session(); expect(state.credentials).toEqual({ auth: { From 2a873872e30e155581d1cd2a60ff0ffc7a7b8a2b Mon Sep 17 00:00:00 2001 From: gagik Date: Fri, 11 Apr 2025 16:10:02 +0200 Subject: [PATCH 08/13] refactor: adapt to the latest structure --- src/common/atlas/apiClient.ts | 10 ++++----- src/index.ts | 13 ++++++------ src/server.ts | 14 ++++--------- src/session.ts | 2 +- src/tools/atlas/atlasTool.ts | 20 +----------------- src/tools/atlas/createAccessList.ts | 6 +++--- src/tools/atlas/createDBUser.ts | 4 ++-- src/tools/atlas/createFreeCluster.ts | 4 ++-- src/tools/atlas/inspectAccessList.ts | 4 ++-- src/tools/atlas/inspectCluster.ts | 4 ++-- src/tools/atlas/listClusters.ts | 8 +++---- src/tools/atlas/listDBUsers.ts | 4 ++-- src/tools/atlas/listProjects.ts | 4 ++-- src/tools/mongodb/index.ts | 2 -- src/tools/mongodb/mongodbTool.ts | 14 ++++++------- src/tools/tool.ts | 4 ++-- tests/integration/helpers.ts | 10 ++++++++- tests/unit/index.test.ts | 31 ---------------------------- 18 files changed, 54 insertions(+), 104 deletions(-) delete mode 100644 tests/unit/index.test.ts diff --git a/src/common/atlas/apiClient.ts b/src/common/atlas/apiClient.ts index f079e375..6bb8e4fd 100644 --- a/src/common/atlas/apiClient.ts +++ b/src/common/atlas/apiClient.ts @@ -55,21 +55,21 @@ export class ApiClient { return this.accessToken?.token.access_token as string | undefined; }; - private authMiddleware = (apiClient: ApiClient): Middleware => ({ - async onRequest({ request, schemaPath }) { + private authMiddleware: Middleware = { + onRequest: async ({ request, schemaPath }) => { if (schemaPath.startsWith("/api/private/unauth") || schemaPath.startsWith("/api/oauth")) { return undefined; } try { - const accessToken = await apiClient.getAccessToken(); + const accessToken = await this.getAccessToken(); request.headers.set("Authorization", `Bearer ${accessToken}`); return request; } catch { // ignore not availble tokens, API will return 401 } }, - }); + }; private readonly errorMiddleware: Middleware = { async onResponse({ response }) { @@ -108,7 +108,7 @@ export class ApiClient { tokenPath: "/api/oauth/token", }, }); - this.client.use(this.authMiddleware(this)); + this.client.use(this.authMiddleware); } this.client.use(this.errorMiddleware); } diff --git a/src/index.ts b/src/index.ts index 36a1a98e..5b65548b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,27 +3,26 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import logger from "./logger.js"; import { mongoLogId } from "mongodb-log-writer"; -import { ApiClient } from "./common/atlas/apiClient.js"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import config from "./config.js"; import { Session } from "./session.js"; import { Server } from "./server.js"; try { - const state = new Session(); + const session = new Session(); const mcpServer = new McpServer({ name: "MongoDB Atlas", version: config.version, }); - const transport = new StdioServerTransport(); - const server = new Seriver({ + const server = new Server({ mcpServer, - state, - transport, + session, }); - await server.registerAndConnect(); + const transport = new StdioServerTransport(); + + await server.connect(transport); } catch (error) { logger.emergency(mongoLogId(1_000_004), "server", `Fatal error running server: ${error}`); diff --git a/src/server.ts b/src/server.ts index d9e70224..624f5b1f 100644 --- a/src/server.ts +++ b/src/server.ts @@ -9,28 +9,22 @@ import { mongoLogId } from "mongodb-log-writer"; export class Server { public readonly session: Session; private readonly mcpServer: McpServer; - private readonly transport: Transport; - constructor({ mcpServer, transport, session }: { mcpServer: McpServer; session: Session; transport: Transport }) { + constructor({ mcpServer, session }: { mcpServer: McpServer; session: Session }) { this.mcpServer = mcpServer; - this.transport = transport; this.session = session; } - async connect() { + async connect(transport: Transport) { this.mcpServer.server.registerCapabilities({ logging: {} }); this.registerTools(); await initializeLogger(this.mcpServer); - await this.mcpServer.connect(this.transport); + await this.mcpServer.connect(transport); - logger.info( - mongoLogId(1_000_004), - "server", - `Server started with transport ${this.transport.constructor.name}` - ); + logger.info(mongoLogId(1_000_004), "server", `Server started with transport ${transport.constructor.name}`); } async close(): Promise { diff --git a/src/session.ts b/src/session.ts index b442b7f1..121c8740 100644 --- a/src/session.ts +++ b/src/session.ts @@ -6,7 +6,7 @@ export class Session { serviceProvider?: NodeDriverServiceProvider; apiClient?: ApiClient; - ensureApiClient(): asserts this is { apiClient: ApiClient } { + ensureAuthenticated(): asserts this is { apiClient: ApiClient } { if (!this.apiClient) { if (!config.apiClientId || !config.apiClientSecret) { throw new Error( diff --git a/src/tools/atlas/atlasTool.ts b/src/tools/atlas/atlasTool.ts index a7809c39..5d177acb 100644 --- a/src/tools/atlas/atlasTool.ts +++ b/src/tools/atlas/atlasTool.ts @@ -5,25 +5,7 @@ import { Session } from "../../session.js"; export abstract class AtlasToolBase extends ToolBase { private apiClient?: ApiClient; - 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." - ); - } - - this.apiClient = new ApiClient({ - baseUrl: config.apiBaseUrl, - credentials: { - clientId: config.apiClientId, - clientSecret: config.apiClientSecret, - }, - }); - } - } - constructor(protected readonly session: Session) { - super(state); + super(session); } } diff --git a/src/tools/atlas/createAccessList.ts b/src/tools/atlas/createAccessList.ts index 09a991c8..3ba12046 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.state.ensureApiClient(); + this.session.ensureAuthenticated(); 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.state.apiClient.getIpInfo(); + const currentIp = await this.session.apiClient.getIpInfo(); const input = { groupId: projectId, ipAddress: currentIp.currentIpv4Address, @@ -56,7 +56,7 @@ export class CreateAccessListTool extends AtlasToolBase { const inputs = [...ipInputs, ...cidrInputs]; - await this.state.apiClient.createProjectIpAccessList({ + await this.session.apiClient.createProjectIpAccessList({ params: { path: { groupId: projectId, diff --git a/src/tools/atlas/createDBUser.ts b/src/tools/atlas/createDBUser.ts index fe2948c3..a388ef9a 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.ensureApiClient(); + this.session.ensureAuthenticated(); const input = { groupId: projectId, @@ -53,7 +53,7 @@ export class CreateDBUserTool extends AtlasToolBase { : undefined, } as CloudDatabaseUser; - await this.state.apiClient.createDatabaseUser({ + await this.session.apiClient.createDatabaseUser({ params: { path: { groupId: projectId, diff --git a/src/tools/atlas/createFreeCluster.ts b/src/tools/atlas/createFreeCluster.ts index 8179883f..675d48cd 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.state.ensureApiClient(); + this.session.ensureAuthenticated(); const input = { groupId: projectId, @@ -38,7 +38,7 @@ export class CreateFreeClusterTool extends AtlasToolBase { terminationProtectionEnabled: false, } as unknown as ClusterDescription20240805; - await this.state.apiClient.createCluster({ + await this.session.apiClient.createCluster({ params: { path: { groupId: projectId, diff --git a/src/tools/atlas/inspectAccessList.ts b/src/tools/atlas/inspectAccessList.ts index c66cf5dc..8c25367b 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.state.ensureApiClient(); + this.session.ensureAuthenticated(); - const accessList = await this.state.apiClient.listProjectIpAccessLists({ + const accessList = await this.session.apiClient.listProjectIpAccessLists({ params: { path: { groupId: projectId, diff --git a/src/tools/atlas/inspectCluster.ts b/src/tools/atlas/inspectCluster.ts index 9ad35a46..c8aa3185 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.state.ensureApiClient(); + this.session.ensureAuthenticated(); - const cluster = await this.state.apiClient.getCluster({ + const cluster = await this.session.apiClient.getCluster({ params: { path: { groupId: projectId, diff --git a/src/tools/atlas/listClusters.ts b/src/tools/atlas/listClusters.ts index 16c85233..8a6a1b08 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.state.ensureApiClient(); + this.session.ensureAuthenticated(); if (!projectId) { - const data = await this.state.apiClient.listClustersForAllProjects(); + const data = await this.session.apiClient.listClustersForAllProjects(); return this.formatAllClustersTable(data); } else { - const project = await this.state.apiClient.getProject({ + const project = await this.session.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.state.apiClient.listClusters({ + const data = await this.session.apiClient.listClusters({ params: { path: { groupId: project.id || "", diff --git a/src/tools/atlas/listDBUsers.ts b/src/tools/atlas/listDBUsers.ts index d9712b1e..5e7a73a9 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.state.ensureApiClient(); + this.session.ensureAuthenticated(); - const data = await this.state.apiClient.listDatabaseUsers({ + const data = await this.session.apiClient.listDatabaseUsers({ params: { path: { groupId: projectId, diff --git a/src/tools/atlas/listProjects.ts b/src/tools/atlas/listProjects.ts index cdf6d6cc..bb9e0865 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.state.ensureApiClient(); + this.session.ensureAuthenticated(); - const data = await this.state.apiClient.listProjects(); + const data = await this.session.apiClient.listProjects(); if (!data?.results?.length) { throw new Error("No projects found in your MongoDB Atlas account."); diff --git a/src/tools/mongodb/index.ts b/src/tools/mongodb/index.ts index abe13fa2..0f89335d 100644 --- a/src/tools/mongodb/index.ts +++ b/src/tools/mongodb/index.ts @@ -1,5 +1,3 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { State } from "../../session.js"; import { ConnectTool } from "./connect.js"; import { ListCollectionsTool } from "./metadata/listCollections.js"; import { CollectionIndexesTool } from "./collectionIndexes.js"; diff --git a/src/tools/mongodb/mongodbTool.ts b/src/tools/mongodb/mongodbTool.ts index 9c09caf0..fb1a0a32 100644 --- a/src/tools/mongodb/mongodbTool.ts +++ b/src/tools/mongodb/mongodbTool.ts @@ -1,6 +1,6 @@ import { z } from "zod"; import { ToolBase } from "../tool.js"; -import { State } from "../../state.js"; +import { Session } from "../../session.js"; import { NodeDriverServiceProvider } from "@mongosh/service-provider-node-driver"; import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import { ErrorCodes, MongoDBError } from "../../errors.js"; @@ -14,16 +14,16 @@ export const DbOperationArgs = { export type DbOperationType = "metadata" | "read" | "create" | "update" | "delete"; export abstract class MongoDBToolBase extends ToolBase { - constructor(state: State) { - super(state); + constructor(session: Session) { + super(session); } protected abstract operationType: DbOperationType; protected async ensureConnected(): Promise { - const provider = this.state.serviceProvider; + const provider = this.session.serviceProvider; if (!provider && config.connectionString) { - await this.connectToMongoDB(config.connectionString, this.state); + await this.connectToMongoDB(config.connectionString); } if (!provider) { @@ -53,7 +53,7 @@ export abstract class MongoDBToolBase extends ToolBase { return super.handleError(error); } - protected async connectToMongoDB(connectionString: string, state: State): Promise { + protected async connectToMongoDB(connectionString: string): Promise { const provider = await NodeDriverServiceProvider.connect(connectionString, { productDocsLink: "https://docs.mongodb.com/todo-mcp", productName: "MongoDB MCP", @@ -67,6 +67,6 @@ export abstract class MongoDBToolBase extends ToolBase { timeoutMS: config.connectOptions.timeoutMS, }); - state.serviceProvider = provider; + this.session.serviceProvider = provider; } } diff --git a/src/tools/tool.ts b/src/tools/tool.ts index bc07b87d..1f5dd8c4 100644 --- a/src/tools/tool.ts +++ b/src/tools/tool.ts @@ -1,7 +1,7 @@ import { McpServer, ToolCallback } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z, ZodNever, ZodRawShape } from "zod"; import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; -import { State } from "../session.js"; +import { Session } from "../session.js"; import logger from "../logger.js"; import { mongoLogId } from "mongodb-log-writer"; @@ -16,7 +16,7 @@ export abstract class ToolBase { protected abstract execute(...args: Parameters>): Promise; - protected constructor(protected state: State) {} + protected constructor(protected session: Session) {} public register(server: McpServer): void { const callback: ToolCallback = async (...args) => { diff --git a/tests/integration/helpers.ts b/tests/integration/helpers.ts index a08b3eea..207492da 100644 --- a/tests/integration/helpers.ts +++ b/tests/integration/helpers.ts @@ -4,6 +4,8 @@ import { Server } from "../../src/server.js"; import runner, { MongoCluster } from "mongodb-runner"; import path from "path"; import fs from "fs/promises"; +import { Session } from "../../src/session.js"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; export async function setupIntegrationTest(): Promise<{ client: Client; @@ -29,7 +31,13 @@ export async function setupIntegrationTest(): Promise<{ } ); - const server = new Server(); + const server = new Server({ + mcpServer: new McpServer({ + name: "test-server", + version: "1.2.3", + }), + session: new Session(), + }); await server.connect(serverTransport); await client.connect(clientTransport); diff --git a/tests/unit/index.test.ts b/tests/unit/index.test.ts deleted file mode 100644 index a0bed5b3..00000000 --- a/tests/unit/index.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { describe, it } from "@jest/globals"; -import { Session } from "../../src/session"; - -// mock the StdioServerTransport -jest.mock("@modelcontextprotocol/sdk/server/stdio"); -// mock Server class and its methods -jest.mock("../../src/server.ts", () => { - return { - Server: jest.fn().mockImplementation(() => { - return { - connect: jest.fn().mockImplementation((transport) => { - return new Promise((resolve) => { - resolve(transport); - }); - }), - }; - }), - }; -}); - -describe("Server initialization", () => { - it("should define a default state", async () => { - const state = new Session(); - - expect(state.credentials).toEqual({ - auth: { - status: "not_auth", - }, - }); - }); -}); From e434bae08ee12a45c669f99f1e8dcf3e84668180 Mon Sep 17 00:00:00 2001 From: gagik Date: Fri, 11 Apr 2025 16:20:27 +0200 Subject: [PATCH 09/13] fix: style --- src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index 5b65548b..944ee92a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,8 +23,8 @@ try { const transport = new StdioServerTransport(); await server.connect(transport); -} catch (error) { - logger.emergency(mongoLogId(1_000_004), "server", `Fatal error running server: ${error}`); +} catch (error: unknown) { + logger.emergency(mongoLogId(1_000_004), "server", `Fatal error running server: ${error as string}`); process.exit(1); } From 4d9e651c7cd64d794827f14435ff92901eb952fc Mon Sep 17 00:00:00 2001 From: gagik Date: Fri, 11 Apr 2025 16:22:47 +0200 Subject: [PATCH 10/13] refactor: make the session close --- src/server.ts | 8 ++------ src/session.ts | 11 +++++++++++ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/server.ts b/src/server.ts index 624f5b1f..d0d7dccf 100644 --- a/src/server.ts +++ b/src/server.ts @@ -28,12 +28,8 @@ export class Server { } async close(): Promise { - try { - await this.session.serviceProvider?.close(true); - } catch { - // Ignore errors during service provider close - } - await this.mcpServer?.close(); + await this.session.close(); + await this.mcpServer.close(); } private registerTools() { diff --git a/src/session.ts b/src/session.ts index 121c8740..0d5ac951 100644 --- a/src/session.ts +++ b/src/session.ts @@ -23,4 +23,15 @@ export class Session { }); } } + + async close(): Promise { + if (this.serviceProvider) { + try { + await this.serviceProvider.close(true); + } catch (error) { + console.error("Error closing service provider:", error); + } + this.serviceProvider = undefined; + } + } } From 94ffeee74938220c3357b0d0ab05d300e366a8d2 Mon Sep 17 00:00:00 2001 From: Gagik Amaryan Date: Fri, 11 Apr 2025 16:45:48 +0200 Subject: [PATCH 11/13] Update src/tools/atlas/atlasTool.ts Co-authored-by: Filipe Constantinov Menezes --- src/tools/atlas/atlasTool.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/tools/atlas/atlasTool.ts b/src/tools/atlas/atlasTool.ts index 5d177acb..6270d803 100644 --- a/src/tools/atlas/atlasTool.ts +++ b/src/tools/atlas/atlasTool.ts @@ -3,7 +3,6 @@ import { ApiClient } from "../../common/atlas/apiClient.js"; import { Session } from "../../session.js"; export abstract class AtlasToolBase extends ToolBase { - private apiClient?: ApiClient; constructor(protected readonly session: Session) { super(session); From 0bfd2aa53346d0970a984e9d65b3c9455f065fc7 Mon Sep 17 00:00:00 2001 From: gagik Date: Fri, 11 Apr 2025 16:50:30 +0200 Subject: [PATCH 12/13] refactor: align tools naming --- src/server.ts | 2 +- src/tools/atlas/atlasTool.ts | 2 -- src/tools/mongodb/{index.ts => tools.ts} | 0 3 files changed, 1 insertion(+), 3 deletions(-) rename src/tools/mongodb/{index.ts => tools.ts} (100%) diff --git a/src/server.ts b/src/server.ts index d0d7dccf..72d9c4f9 100644 --- a/src/server.ts +++ b/src/server.ts @@ -2,7 +2,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { Session } from "./session.js"; import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; import { AtlasTools } from "./tools/atlas/tools.js"; -import { MongoDbTools } from "./tools/mongodb/index.js"; +import { MongoDbTools } from "./tools/mongodb/tools.js"; import logger, { initializeLogger } from "./logger.js"; import { mongoLogId } from "mongodb-log-writer"; diff --git a/src/tools/atlas/atlasTool.ts b/src/tools/atlas/atlasTool.ts index 6270d803..7a1c00fe 100644 --- a/src/tools/atlas/atlasTool.ts +++ b/src/tools/atlas/atlasTool.ts @@ -1,9 +1,7 @@ import { ToolBase } from "../tool.js"; -import { ApiClient } from "../../common/atlas/apiClient.js"; import { Session } from "../../session.js"; export abstract class AtlasToolBase extends ToolBase { - constructor(protected readonly session: Session) { super(session); } diff --git a/src/tools/mongodb/index.ts b/src/tools/mongodb/tools.ts similarity index 100% rename from src/tools/mongodb/index.ts rename to src/tools/mongodb/tools.ts From 4c96ad94bd37f37068b18dc7d237152d6238dc3a Mon Sep 17 00:00:00 2001 From: gagik Date: Fri, 11 Apr 2025 16:51:26 +0200 Subject: [PATCH 13/13] add changes