Skip to content

chore: auto generate apiClient #64

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Apr 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions scripts/apply.ts
Original file line number Diff line number Diff line change
@@ -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<operations["${operationId}"]>) {
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);
});
Empty file modified scripts/filter.ts
100644 → 100755
Empty file.
3 changes: 2 additions & 1 deletion scripts/generate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
73 changes: 29 additions & 44 deletions src/common/atlas/apiClient.ts
Original file line number Diff line number Diff line change
@@ -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<ApiClientError> {
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;
Expand Down Expand Up @@ -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<paths>({
Expand Down Expand Up @@ -138,38 +116,39 @@ export class ApiClient {
}>;
}

async listProjects(options?: FetchOptions<operations["listProjects"]>) {
const { data } = await this.client.GET(`/api/atlas/v2/groups`, options);
// DO NOT EDIT. This is auto-generated code.
async listClustersForAllProjects(options?: FetchOptions<operations["listClustersForAllProjects"]>) {
const { data } = await this.client.GET("/api/atlas/v2/clusters", options);
return data;
}

async listProjectIpAccessLists(options: FetchOptions<operations["listProjectIpAccessLists"]>) {
const { data } = await this.client.GET(`/api/atlas/v2/groups/{groupId}/accessList`, options);
async listProjects(options?: FetchOptions<operations["listProjects"]>) {
const { data } = await this.client.GET("/api/atlas/v2/groups", options);
return data;
}

async createProjectIpAccessList(options: FetchOptions<operations["createProjectIpAccessList"]>) {
const { data } = await this.client.POST(`/api/atlas/v2/groups/{groupId}/accessList`, options);
async createProject(options: FetchOptions<operations["createProject"]>) {
const { data } = await this.client.POST("/api/atlas/v2/groups", options);
return data;
}

async getProject(options: FetchOptions<operations["getProject"]>) {
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<operations["listClusters"]>) {
const { data } = await this.client.GET(`/api/atlas/v2/groups/{groupId}/clusters`, options);
async listProjectIpAccessLists(options: FetchOptions<operations["listProjectIpAccessLists"]>) {
const { data } = await this.client.GET("/api/atlas/v2/groups/{groupId}/accessList", options);
return data;
}

async listClustersForAllProjects(options?: FetchOptions<operations["listClustersForAllProjects"]>) {
const { data } = await this.client.GET(`/api/atlas/v2/clusters`, options);
async createProjectIpAccessList(options: FetchOptions<operations["createProjectIpAccessList"]>) {
const { data } = await this.client.POST("/api/atlas/v2/groups/{groupId}/accessList", options);
return data;
}

async getCluster(options: FetchOptions<operations["getCluster"]>) {
const { data } = await this.client.GET(`/api/atlas/v2/groups/{groupId}/clusters/{clusterName}`, options);
async listClusters(options: FetchOptions<operations["listClusters"]>) {
const { data } = await this.client.GET("/api/atlas/v2/groups/{groupId}/clusters", options);
return data;
}

Expand All @@ -178,13 +157,19 @@ export class ApiClient {
return data;
}

async createDatabaseUser(options: FetchOptions<operations["createDatabaseUser"]>) {
const { data } = await this.client.POST("/api/atlas/v2/groups/{groupId}/databaseUsers", options);
async getCluster(options: FetchOptions<operations["getCluster"]>) {
const { data } = await this.client.GET("/api/atlas/v2/groups/{groupId}/clusters/{clusterName}", options);
return data;
}

async listDatabaseUsers(options: FetchOptions<operations["listDatabaseUsers"]>) {
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<operations["createDatabaseUser"]>) {
const { data } = await this.client.POST("/api/atlas/v2/groups/{groupId}/databaseUsers", options);
return data;
}
// DO NOT EDIT. This is auto-generated code.
}
21 changes: 21 additions & 0 deletions src/common/atlas/apiClientError.ts
Original file line number Diff line number Diff line change
@@ -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<ApiClientError> {
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);
}
}
}
16 changes: 2 additions & 14 deletions src/logger.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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<string | undefined>((resolve, reject) => {
fs.mkdir(path, options, (err, resultPath) => {
if (err) {
reject(err);
} else {
resolve(resultPath);
}
});
});
}

export async function initializeLogger(server: McpServer): Promise<void> {
await mkdirPromise(config.logPath, { recursive: true });
await fs.mkdir(config.logPath, { recursive: true });

const manager = new MongoLogManager({
directory: config.logPath,
Expand Down
Loading