Skip to content

Commit 9286078

Browse files
authored
chore: auto generate apiClient (#64)
1 parent 8ca583a commit 9286078

File tree

6 files changed

+146
-59
lines changed

6 files changed

+146
-59
lines changed

scripts/apply.ts

+92
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import fs from "fs/promises";
2+
import { OpenAPIV3_1 } from "openapi-types";
3+
import argv from "yargs-parser";
4+
5+
function findParamFromRef(ref: string, openapi: OpenAPIV3_1.Document): OpenAPIV3_1.ParameterObject {
6+
const paramParts = ref.split("/");
7+
paramParts.shift(); // Remove the first part which is always '#'
8+
let param: any = openapi; // eslint-disable-line @typescript-eslint/no-explicit-any
9+
while (true) {
10+
const part = paramParts.shift();
11+
if (!part) {
12+
break;
13+
}
14+
param = param[part];
15+
}
16+
return param;
17+
}
18+
19+
async function main() {
20+
const { spec, file } = argv(process.argv.slice(2));
21+
22+
if (!spec || !file) {
23+
console.error("Please provide both --spec and --file arguments.");
24+
process.exit(1);
25+
}
26+
27+
const specFile = (await fs.readFile(spec, "utf8")) as string;
28+
29+
const operations: {
30+
path: string;
31+
method: string;
32+
operationId: string;
33+
requiredParams: boolean;
34+
tag: string;
35+
}[] = [];
36+
37+
const openapi = JSON.parse(specFile) as OpenAPIV3_1.Document;
38+
for (const path in openapi.paths) {
39+
for (const method in openapi.paths[path]) {
40+
const operation: OpenAPIV3_1.OperationObject = openapi.paths[path][method];
41+
42+
if (!operation.operationId || !operation.tags?.length) {
43+
continue;
44+
}
45+
46+
let requiredParams = !!operation.requestBody;
47+
48+
for (const param of operation.parameters || []) {
49+
const ref = (param as OpenAPIV3_1.ReferenceObject).$ref as string | undefined;
50+
let paramObject: OpenAPIV3_1.ParameterObject = param as OpenAPIV3_1.ParameterObject;
51+
if (ref) {
52+
paramObject = findParamFromRef(ref, openapi);
53+
}
54+
if (paramObject.in === "path") {
55+
requiredParams = true;
56+
}
57+
}
58+
59+
operations.push({
60+
path,
61+
method: method.toUpperCase(),
62+
operationId: operation.operationId || "",
63+
requiredParams,
64+
tag: operation.tags[0],
65+
});
66+
}
67+
}
68+
69+
const operationOutput = operations
70+
.map((operation) => {
71+
const { operationId, method, path, requiredParams } = operation;
72+
return `async ${operationId}(options${requiredParams ? "" : "?"}: FetchOptions<operations["${operationId}"]>) {
73+
const { data } = await this.client.${method}("${path}", options);
74+
return data;
75+
}
76+
`;
77+
})
78+
.join("\n");
79+
80+
const templateFile = (await fs.readFile(file, "utf8")) as string;
81+
const output = templateFile.replace(
82+
/\/\/ DO NOT EDIT\. This is auto-generated code\.\n.*\/\/ DO NOT EDIT\. This is auto-generated code\./g,
83+
operationOutput
84+
);
85+
86+
await fs.writeFile(file, output, "utf8");
87+
}
88+
89+
main().catch((error) => {
90+
console.error("Error:", error);
91+
process.exit(1);
92+
});

scripts/filter.ts

100644100755
File mode changed.

scripts/generate.sh

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@ curl -Lo ./scripts/spec.json https://github.com/mongodb/openapi/raw/refs/heads/m
66
tsx ./scripts/filter.ts > ./scripts/filteredSpec.json < ./scripts/spec.json
77
redocly bundle --ext json --remove-unused-components ./scripts/filteredSpec.json --output ./scripts/bundledSpec.json
88
openapi-typescript ./scripts/bundledSpec.json --root-types-no-schema-prefix --root-types --output ./src/common/atlas/openapi.d.ts
9-
prettier --write ./src/common/atlas/openapi.d.ts
9+
tsx ./scripts/apply.ts --spec ./scripts/bundledSpec.json --file ./src/common/atlas/apiClient.ts
10+
prettier --write ./src/common/atlas/openapi.d.ts ./src/common/atlas/apiClient.ts
1011
rm -rf ./scripts/bundledSpec.json ./scripts/filteredSpec.json ./scripts/spec.json

src/common/atlas/apiClient.ts

+29-44
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,11 @@
11
import config from "../../config.js";
22
import createClient, { Client, FetchOptions, Middleware } from "openapi-fetch";
33
import { AccessToken, ClientCredentials } from "simple-oauth2";
4-
4+
import { ApiClientError } from "./apiClientError.js";
55
import { paths, operations } from "./openapi.js";
66

77
const ATLAS_API_VERSION = "2025-03-12";
88

9-
export class ApiClientError extends Error {
10-
response?: Response;
11-
12-
constructor(message: string, response: Response | undefined = undefined) {
13-
super(message);
14-
this.name = "ApiClientError";
15-
this.response = response;
16-
}
17-
18-
static async fromResponse(response: Response, message?: string): Promise<ApiClientError> {
19-
message ||= `error calling Atlas API`;
20-
try {
21-
const text = await response.text();
22-
return new ApiClientError(`${message}: [${response.status} ${response.statusText}] ${text}`, response);
23-
} catch {
24-
return new ApiClientError(`${message}: ${response.status} ${response.statusText}`, response);
25-
}
26-
}
27-
}
28-
299
export interface ApiClientOptions {
3010
credentials?: {
3111
clientId: string;
@@ -79,15 +59,13 @@ export class ApiClient {
7959
},
8060
};
8161

82-
constructor(options: ApiClientOptions) {
83-
const defaultOptions = {
84-
baseUrl: "https://cloud.mongodb.com/",
85-
userAgent: `AtlasMCP/${config.version} (${process.platform}; ${process.arch}; ${process.env.HOSTNAME || "unknown"})`,
86-
};
87-
62+
constructor(options?: ApiClientOptions) {
8863
this.options = {
89-
...defaultOptions,
9064
...options,
65+
baseUrl: options?.baseUrl || "https://cloud.mongodb.com/",
66+
userAgent:
67+
options?.userAgent ||
68+
`AtlasMCP/${config.version} (${process.platform}; ${process.arch}; ${process.env.HOSTNAME || "unknown"})`,
9169
};
9270

9371
this.client = createClient<paths>({
@@ -138,38 +116,39 @@ export class ApiClient {
138116
}>;
139117
}
140118

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

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

151-
async createProjectIpAccessList(options: FetchOptions<operations["createProjectIpAccessList"]>) {
152-
const { data } = await this.client.POST(`/api/atlas/v2/groups/{groupId}/accessList`, options);
130+
async createProject(options: FetchOptions<operations["createProject"]>) {
131+
const { data } = await this.client.POST("/api/atlas/v2/groups", options);
153132
return data;
154133
}
155134

156135
async getProject(options: FetchOptions<operations["getProject"]>) {
157-
const { data } = await this.client.GET(`/api/atlas/v2/groups/{groupId}`, options);
136+
const { data } = await this.client.GET("/api/atlas/v2/groups/{groupId}", options);
158137
return data;
159138
}
160139

161-
async listClusters(options: FetchOptions<operations["listClusters"]>) {
162-
const { data } = await this.client.GET(`/api/atlas/v2/groups/{groupId}/clusters`, options);
140+
async listProjectIpAccessLists(options: FetchOptions<operations["listProjectIpAccessLists"]>) {
141+
const { data } = await this.client.GET("/api/atlas/v2/groups/{groupId}/accessList", options);
163142
return data;
164143
}
165144

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

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

@@ -178,13 +157,19 @@ export class ApiClient {
178157
return data;
179158
}
180159

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

186165
async listDatabaseUsers(options: FetchOptions<operations["listDatabaseUsers"]>) {
187-
const { data } = await this.client.GET(`/api/atlas/v2/groups/{groupId}/databaseUsers`, options);
166+
const { data } = await this.client.GET("/api/atlas/v2/groups/{groupId}/databaseUsers", options);
167+
return data;
168+
}
169+
170+
async createDatabaseUser(options: FetchOptions<operations["createDatabaseUser"]>) {
171+
const { data } = await this.client.POST("/api/atlas/v2/groups/{groupId}/databaseUsers", options);
188172
return data;
189173
}
174+
// DO NOT EDIT. This is auto-generated code.
190175
}

src/common/atlas/apiClientError.ts

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
export class ApiClientError extends Error {
2+
response?: Response;
3+
4+
constructor(message: string, response: Response | undefined = undefined) {
5+
super(message);
6+
this.name = "ApiClientError";
7+
this.response = response;
8+
}
9+
10+
static async fromResponse(
11+
response: Response,
12+
message: string = `error calling Atlas API`
13+
): Promise<ApiClientError> {
14+
try {
15+
const text = await response.text();
16+
return new ApiClientError(`${message}: [${response.status} ${response.statusText}] ${text}`, response);
17+
} catch {
18+
return new ApiClientError(`${message}: ${response.status} ${response.statusText}`, response);
19+
}
20+
}
21+
}

src/logger.ts

+2-14
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import fs from "fs";
1+
import fs from "fs/promises";
22
import { MongoLogId, MongoLogManager, MongoLogWriter } from "mongodb-log-writer";
33
import config from "./config.js";
44
import redact from "mongodb-redact";
@@ -98,20 +98,8 @@ class ProxyingLogger extends LoggerBase {
9898
const logger = new ProxyingLogger();
9999
export default logger;
100100

101-
async function mkdirPromise(path: fs.PathLike, options?: fs.Mode | fs.MakeDirectoryOptions) {
102-
return new Promise<string | undefined>((resolve, reject) => {
103-
fs.mkdir(path, options, (err, resultPath) => {
104-
if (err) {
105-
reject(err);
106-
} else {
107-
resolve(resultPath);
108-
}
109-
});
110-
});
111-
}
112-
113101
export async function initializeLogger(server: McpServer): Promise<void> {
114-
await mkdirPromise(config.logPath, { recursive: true });
102+
await fs.mkdir(config.logPath, { recursive: true });
115103

116104
const manager = new MongoLogManager({
117105
directory: config.logPath,

0 commit comments

Comments
 (0)