diff --git a/scripts/apply.ts b/scripts/apply.ts new file mode 100755 index 00000000..e8ef94f4 --- /dev/null +++ b/scripts/apply.ts @@ -0,0 +1,92 @@ +import fs from "fs/promises"; +import { OpenAPIV3_1 } from "openapi-types"; +import argv from "yargs-parser"; + +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 fs.readFile(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 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 fs.writeFile(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 6bb8e4fd..b784e43e 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,15 +59,13 @@ 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"})`, - }; - + constructor(options?: ApiClientOptions) { 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({ @@ -138,38 +116,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; } @@ -178,13 +157,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..6073c161 --- /dev/null +++ b/src/common/atlas/apiClientError.ts @@ -0,0 +1,21 @@ +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 = `error calling Atlas API` + ): Promise { + 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); + } + } +} diff --git a/src/logger.ts b/src/logger.ts index 064b7fe8..6682566a 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -1,4 +1,4 @@ -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"; @@ -98,20 +98,8 @@ class ProxyingLogger extends LoggerBase { const logger = new ProxyingLogger(); export default logger; -async function mkdirPromise(path: fs.PathLike, options?: fs.Mode | fs.MakeDirectoryOptions) { - return new Promise((resolve, reject) => { - fs.mkdir(path, options, (err, resultPath) => { - if (err) { - reject(err); - } else { - resolve(resultPath); - } - }); - }); -} - export async function initializeLogger(server: McpServer): Promise { - await mkdirPromise(config.logPath, { recursive: true }); + await fs.mkdir(config.logPath, { recursive: true }); const manager = new MongoLogManager({ directory: config.logPath,