From 949f71804284d6d93fb5c1dd006d1ed340b51b14 Mon Sep 17 00:00:00 2001 From: Filipe C Menezes Date: Fri, 11 Apr 2025 15:26:29 +0100 Subject: [PATCH 1/6] chore: auto generate apiClient --- .vscode/launch.json | 10 +++- scripts/apply.ts | 91 ++++++++++++++++++++++++++++++ scripts/filter.ts | 0 scripts/generate.sh | 3 +- src/common/atlas/apiClient.ts | 71 +++++++++-------------- src/common/atlas/apiClientError.ts | 19 +++++++ 6 files changed, 147 insertions(+), 47 deletions(-) create mode 100755 scripts/apply.ts mode change 100644 => 100755 scripts/filter.ts create mode 100644 src/common/atlas/apiClientError.ts diff --git a/.vscode/launch.json b/.vscode/launch.json index a55e49ac..73d20321 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -9,9 +9,13 @@ "request": "launch", "name": "Launch Program", "skipFiles": ["/**"], - "program": "${workspaceFolder}/dist/index.js", - "preLaunchTask": "tsc: build - tsconfig.json", - "outFiles": ["${workspaceFolder}/dist/**/*.js"] + "program": "${workspaceFolder}/scripts/apply.ts", + "args": [ + "--spec", + "${workspaceFolder}/scripts/bundledSpec.json", + "--template", + "${workspaceFolder}/scripts/apiClient.ts.template" + ], } ] } diff --git a/scripts/apply.ts b/scripts/apply.ts new file mode 100755 index 00000000..855cdb50 --- /dev/null +++ b/scripts/apply.ts @@ -0,0 +1,91 @@ +import fs, { writeFile } from "fs"; +import { OpenAPIV3_1 } from "openapi-types"; +import argv from "yargs-parser"; +import { promisify } from "util"; + +const readFileAsync = promisify(fs.readFile); +const writeFileAsync = promisify(fs.writeFile); + +function findParamFromRef(ref: string, openapi: OpenAPIV3_1.Document): OpenAPIV3_1.ParameterObject { + const paramParts = ref.split("/"); + paramParts.shift(); // Remove the first part which is always '#' + let param: any = openapi; // eslint-disable-line @typescript-eslint/no-explicit-any + while (true) { + const part = paramParts.shift(); + if (!part) { + break; + } + param = param[part]; + } + return param; +} + +async function main() { + const {spec, file} = argv(process.argv.slice(2)); + + if (!spec || !file) { + console.error("Please provide both --spec and --file arguments."); + process.exit(1); + } + + const specFile = await readFileAsync(spec, "utf8") as string; + + const operations: { + path: string; + method: string; + operationId: string; + requiredParams: boolean; + tag: string; + }[] = []; + + const openapi = JSON.parse(specFile) as OpenAPIV3_1.Document; + for (const path in openapi.paths) { + for (const method in openapi.paths[path]) { + const operation: OpenAPIV3_1.OperationObject = openapi.paths[path][method]; + + if (!operation.operationId || !operation.tags?.length) { + continue; + } + + let requiredParams = !!operation.requestBody; + + for (const param of operation.parameters || []) { + const ref = (param as OpenAPIV3_1.ReferenceObject).$ref as string | undefined; + let paramObject: OpenAPIV3_1.ParameterObject = param as OpenAPIV3_1.ParameterObject; + if (ref) { + paramObject = findParamFromRef(ref, openapi); + } + if (paramObject.in === "path") { + requiredParams = true; + } + } + + operations.push({ + path, + method: method.toUpperCase(), + operationId: operation.operationId || '', + requiredParams, + tag: operation.tags[0], + }); + } + } + + const operationOutput = operations.map((operation) => { + const { operationId, method, path, requiredParams } = operation; + return `async ${operationId}(options${requiredParams ? '' : '?'}: FetchOptions) { + const { data } = await this.client.${method}("${path}", options); + return data; +} +`; + }).join("\n"); + + const templateFile = await readFileAsync(file, "utf8") as string; + const output = templateFile.replace(/\/\/ DO NOT EDIT\. This is auto-generated code\.\n.*\/\/ DO NOT EDIT\. This is auto-generated code\./g, operationOutput); + + await writeFileAsync(file, output, "utf8"); +} + +main().catch((error) => { + console.error("Error:", error); + process.exit(1); +}); diff --git a/scripts/filter.ts b/scripts/filter.ts old mode 100644 new mode 100755 diff --git a/scripts/generate.sh b/scripts/generate.sh index 5fbc898a..6febc8ff 100755 --- a/scripts/generate.sh +++ b/scripts/generate.sh @@ -6,5 +6,6 @@ curl -Lo ./scripts/spec.json https://github.com/mongodb/openapi/raw/refs/heads/m tsx ./scripts/filter.ts > ./scripts/filteredSpec.json < ./scripts/spec.json redocly bundle --ext json --remove-unused-components ./scripts/filteredSpec.json --output ./scripts/bundledSpec.json openapi-typescript ./scripts/bundledSpec.json --root-types-no-schema-prefix --root-types --output ./src/common/atlas/openapi.d.ts -prettier --write ./src/common/atlas/openapi.d.ts +tsx ./scripts/apply.ts --spec ./scripts/bundledSpec.json --file ./src/common/atlas/apiClient.ts +prettier --write ./src/common/atlas/openapi.d.ts ./src/common/atlas/apiClient.ts rm -rf ./scripts/bundledSpec.json ./scripts/filteredSpec.json ./scripts/spec.json diff --git a/src/common/atlas/apiClient.ts b/src/common/atlas/apiClient.ts index 3198ea70..84e827ac 100644 --- a/src/common/atlas/apiClient.ts +++ b/src/common/atlas/apiClient.ts @@ -1,31 +1,11 @@ import config from "../../config.js"; import createClient, { Client, FetchOptions, Middleware } from "openapi-fetch"; import { AccessToken, ClientCredentials } from "simple-oauth2"; - +import { ApiClientError } from "./apiClientError.js"; import { paths, operations } from "./openapi.js"; const ATLAS_API_VERSION = "2025-03-12"; -export class ApiClientError extends Error { - response?: Response; - - constructor(message: string, response: Response | undefined = undefined) { - super(message); - this.name = "ApiClientError"; - this.response = response; - } - - static async fromResponse(response: Response, message?: string): Promise { - message ||= `error calling Atlas API`; - try { - const text = await response.text(); - return new ApiClientError(`${message}: [${response.status} ${response.statusText}] ${text}`, response); - } catch { - return new ApiClientError(`${message}: ${response.status} ${response.statusText}`, response); - } - } -} - export interface ApiClientOptions { credentials?: { clientId: string; @@ -79,14 +59,12 @@ export class ApiClient { }); constructor(options?: ApiClientOptions) { - const defaultOptions = { - baseUrl: "https://cloud.mongodb.com/", - userAgent: `AtlasMCP/${config.version} (${process.platform}; ${process.arch}; ${process.env.HOSTNAME || "unknown"})`, - }; - this.options = { - ...defaultOptions, ...options, + baseUrl: options?.baseUrl || "https://cloud.mongodb.com/", + userAgent: + options?.userAgent || + `AtlasMCP/${config.version} (${process.platform}; ${process.arch}; ${process.env.HOSTNAME || "unknown"})`, }; this.client = createClient({ @@ -136,38 +114,39 @@ export class ApiClient { }; } - async listProjects(options?: FetchOptions) { - const { data } = await this.client.GET(`/api/atlas/v2/groups`, options); + // DO NOT EDIT. This is auto-generated code. + async listClustersForAllProjects(options?: FetchOptions) { + const { data } = await this.client.GET("/api/atlas/v2/clusters", options); return data; } - async listProjectIpAccessLists(options: FetchOptions) { - const { data } = await this.client.GET(`/api/atlas/v2/groups/{groupId}/accessList`, options); + async listProjects(options?: FetchOptions) { + const { data } = await this.client.GET("/api/atlas/v2/groups", options); return data; } - async createProjectIpAccessList(options: FetchOptions) { - const { data } = await this.client.POST(`/api/atlas/v2/groups/{groupId}/accessList`, options); + async createProject(options: FetchOptions) { + const { data } = await this.client.POST("/api/atlas/v2/groups", options); return data; } async getProject(options: FetchOptions) { - const { data } = await this.client.GET(`/api/atlas/v2/groups/{groupId}`, options); + const { data } = await this.client.GET("/api/atlas/v2/groups/{groupId}", options); return data; } - async listClusters(options: FetchOptions) { - const { data } = await this.client.GET(`/api/atlas/v2/groups/{groupId}/clusters`, options); + async listProjectIpAccessLists(options: FetchOptions) { + const { data } = await this.client.GET("/api/atlas/v2/groups/{groupId}/accessList", options); return data; } - async listClustersForAllProjects(options?: FetchOptions) { - const { data } = await this.client.GET(`/api/atlas/v2/clusters`, options); + async createProjectIpAccessList(options: FetchOptions) { + const { data } = await this.client.POST("/api/atlas/v2/groups/{groupId}/accessList", options); return data; } - async getCluster(options: FetchOptions) { - const { data } = await this.client.GET(`/api/atlas/v2/groups/{groupId}/clusters/{clusterName}`, options); + async listClusters(options: FetchOptions) { + const { data } = await this.client.GET("/api/atlas/v2/groups/{groupId}/clusters", options); return data; } @@ -176,13 +155,19 @@ export class ApiClient { return data; } - async createDatabaseUser(options: FetchOptions) { - const { data } = await this.client.POST("/api/atlas/v2/groups/{groupId}/databaseUsers", options); + async getCluster(options: FetchOptions) { + const { data } = await this.client.GET("/api/atlas/v2/groups/{groupId}/clusters/{clusterName}", options); return data; } async listDatabaseUsers(options: FetchOptions) { - const { data } = await this.client.GET(`/api/atlas/v2/groups/{groupId}/databaseUsers`, options); + const { data } = await this.client.GET("/api/atlas/v2/groups/{groupId}/databaseUsers", options); + return data; + } + + async createDatabaseUser(options: FetchOptions) { + const { data } = await this.client.POST("/api/atlas/v2/groups/{groupId}/databaseUsers", options); return data; } + // DO NOT EDIT. This is auto-generated code. } diff --git a/src/common/atlas/apiClientError.ts b/src/common/atlas/apiClientError.ts new file mode 100644 index 00000000..03bd5696 --- /dev/null +++ b/src/common/atlas/apiClientError.ts @@ -0,0 +1,19 @@ +export class ApiClientError extends Error { + response?: Response; + + constructor(message: string, response: Response | undefined = undefined) { + super(message); + this.name = "ApiClientError"; + this.response = response; + } + + static async fromResponse(response: Response, message?: string): Promise { + message ||= `error calling Atlas API`; + try { + const text = await response.text(); + return new ApiClientError(`${message}: [${response.status} ${response.statusText}] ${text}`, response); + } catch { + return new ApiClientError(`${message}: ${response.status} ${response.statusText}`, response); + } + } +} From 6b183f27d36ae350d66267744f7fca57ead54a69 Mon Sep 17 00:00:00 2001 From: Filipe C Menezes Date: Fri, 11 Apr 2025 15:34:22 +0100 Subject: [PATCH 2/6] fix vscode settings --- .vscode/launch.json | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 73d20321..a55e49ac 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -9,13 +9,9 @@ "request": "launch", "name": "Launch Program", "skipFiles": ["/**"], - "program": "${workspaceFolder}/scripts/apply.ts", - "args": [ - "--spec", - "${workspaceFolder}/scripts/bundledSpec.json", - "--template", - "${workspaceFolder}/scripts/apiClient.ts.template" - ], + "program": "${workspaceFolder}/dist/index.js", + "preLaunchTask": "tsc: build - tsconfig.json", + "outFiles": ["${workspaceFolder}/dist/**/*.js"] } ] } From fd473f54c59899b3d5633fcd24f3a5b549240d83 Mon Sep 17 00:00:00 2001 From: Filipe C Menezes Date: Fri, 11 Apr 2025 15:36:59 +0100 Subject: [PATCH 3/6] chore: drive-by --- src/logger.ts | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/src/logger.ts b/src/logger.ts index 532ff506..21b94be8 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -4,6 +4,7 @@ import config from "./config.js"; import redact from "mongodb-redact"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { LoggingMessageNotification } from "@modelcontextprotocol/sdk/types.js"; +import { promisify } from "util"; export type LogLevel = LoggingMessageNotification["params"]["level"]; @@ -98,20 +99,10 @@ class ProxyingLogger extends LoggerBase { const logger = new ProxyingLogger(); export default logger; -async function mkdirPromise(path: fs.PathLike, options?: fs.Mode | fs.MakeDirectoryOptions) { - return new Promise((resolve, reject) => { - fs.mkdir(path, options, (err, resultPath) => { - if (err) { - reject(err); - } else { - resolve(resultPath); - } - }); - }); -} +const mkdirAsync = promisify(fs.mkdir); export async function initializeLogger(server: McpServer): Promise { - await mkdirPromise(config.logPath, { recursive: true }); + await mkdirAsync(config.logPath, { recursive: true }); const manager = new MongoLogManager({ directory: config.logPath, From 284f660fe449d3571e1547970c3d6620d30fa07b Mon Sep 17 00:00:00 2001 From: Filipe C Menezes Date: Fri, 11 Apr 2025 15:38:24 +0100 Subject: [PATCH 4/6] fix: styles --- scripts/apply.ts | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/scripts/apply.ts b/scripts/apply.ts index 855cdb50..5f163b5f 100755 --- a/scripts/apply.ts +++ b/scripts/apply.ts @@ -1,4 +1,4 @@ -import fs, { writeFile } from "fs"; +import fs from "fs"; import { OpenAPIV3_1 } from "openapi-types"; import argv from "yargs-parser"; import { promisify } from "util"; @@ -21,14 +21,14 @@ function findParamFromRef(ref: string, openapi: OpenAPIV3_1.Document): OpenAPIV3 } async function main() { - const {spec, file} = argv(process.argv.slice(2)); + const { spec, file } = argv(process.argv.slice(2)); if (!spec || !file) { console.error("Please provide both --spec and --file arguments."); process.exit(1); } - const specFile = await readFileAsync(spec, "utf8") as string; + const specFile = (await readFileAsync(spec, "utf8")) as string; const operations: { path: string; @@ -42,7 +42,7 @@ async function main() { for (const path in openapi.paths) { for (const method in openapi.paths[path]) { const operation: OpenAPIV3_1.OperationObject = openapi.paths[path][method]; - + if (!operation.operationId || !operation.tags?.length) { continue; } @@ -63,24 +63,29 @@ async function main() { operations.push({ path, method: method.toUpperCase(), - operationId: operation.operationId || '', + operationId: operation.operationId || "", requiredParams, tag: operation.tags[0], }); } } - - const operationOutput = operations.map((operation) => { - const { operationId, method, path, requiredParams } = operation; - return `async ${operationId}(options${requiredParams ? '' : '?'}: FetchOptions) { + + const operationOutput = operations + .map((operation) => { + const { operationId, method, path, requiredParams } = operation; + return `async ${operationId}(options${requiredParams ? "" : "?"}: FetchOptions) { const { data } = await this.client.${method}("${path}", options); return data; } `; - }).join("\n"); + }) + .join("\n"); - const templateFile = await readFileAsync(file, "utf8") as string; - const output = templateFile.replace(/\/\/ DO NOT EDIT\. This is auto-generated code\.\n.*\/\/ DO NOT EDIT\. This is auto-generated code\./g, operationOutput); + const templateFile = (await readFileAsync(file, "utf8")) as string; + const output = templateFile.replace( + /\/\/ DO NOT EDIT\. This is auto-generated code\.\n.*\/\/ DO NOT EDIT\. This is auto-generated code\./g, + operationOutput + ); await writeFileAsync(file, output, "utf8"); } From 96704b89608fe3838e93cc1822a76ab4cbc13496 Mon Sep 17 00:00:00 2001 From: Filipe C Menezes Date: Fri, 11 Apr 2025 15:48:47 +0100 Subject: [PATCH 5/6] fix: promises --- scripts/apply.ts | 12 ++++-------- src/logger.ts | 7 ++----- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/scripts/apply.ts b/scripts/apply.ts index 5f163b5f..e8ef94f4 100755 --- a/scripts/apply.ts +++ b/scripts/apply.ts @@ -1,10 +1,6 @@ -import fs from "fs"; +import fs from "fs/promises"; import { OpenAPIV3_1 } from "openapi-types"; import argv from "yargs-parser"; -import { promisify } from "util"; - -const readFileAsync = promisify(fs.readFile); -const writeFileAsync = promisify(fs.writeFile); function findParamFromRef(ref: string, openapi: OpenAPIV3_1.Document): OpenAPIV3_1.ParameterObject { const paramParts = ref.split("/"); @@ -28,7 +24,7 @@ async function main() { process.exit(1); } - const specFile = (await readFileAsync(spec, "utf8")) as string; + const specFile = (await fs.readFile(spec, "utf8")) as string; const operations: { path: string; @@ -81,13 +77,13 @@ async function main() { }) .join("\n"); - const templateFile = (await readFileAsync(file, "utf8")) as string; + const templateFile = (await fs.readFile(file, "utf8")) as string; const output = templateFile.replace( /\/\/ DO NOT EDIT\. This is auto-generated code\.\n.*\/\/ DO NOT EDIT\. This is auto-generated code\./g, operationOutput ); - await writeFileAsync(file, output, "utf8"); + await fs.writeFile(file, output, "utf8"); } main().catch((error) => { diff --git a/src/logger.ts b/src/logger.ts index 21b94be8..6b19cbec 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -1,10 +1,9 @@ -import fs from "fs"; +import fs from "fs/promises"; import { MongoLogId, MongoLogManager, MongoLogWriter } from "mongodb-log-writer"; import config from "./config.js"; import redact from "mongodb-redact"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { LoggingMessageNotification } from "@modelcontextprotocol/sdk/types.js"; -import { promisify } from "util"; export type LogLevel = LoggingMessageNotification["params"]["level"]; @@ -99,10 +98,8 @@ class ProxyingLogger extends LoggerBase { const logger = new ProxyingLogger(); export default logger; -const mkdirAsync = promisify(fs.mkdir); - export async function initializeLogger(server: McpServer): Promise { - await mkdirAsync(config.logPath, { recursive: true }); + await fs.mkdir(config.logPath, { recursive: true }); const manager = new MongoLogManager({ directory: config.logPath, From 365f4570df9698cac11b6e881445926fa1963c92 Mon Sep 17 00:00:00 2001 From: Filipe C Menezes Date: Fri, 11 Apr 2025 15:49:47 +0100 Subject: [PATCH 6/6] fix: default paramenter --- src/common/atlas/apiClientError.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/common/atlas/apiClientError.ts b/src/common/atlas/apiClientError.ts index 03bd5696..8659a7c7 100644 --- a/src/common/atlas/apiClientError.ts +++ b/src/common/atlas/apiClientError.ts @@ -7,8 +7,7 @@ export class ApiClientError extends Error { this.response = response; } - static async fromResponse(response: Response, message?: string): Promise { - message ||= `error calling Atlas API`; + static async fromResponse(response: Response, message: string = `error calling Atlas API`): Promise { try { const text = await response.text(); return new ApiClientError(`${message}: [${response.status} ${response.statusText}] ${text}`, response);