• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mongodb-js / mongodb-mcp-server / 16218434167

11 Jul 2025 11:01AM UTC coverage: 75.33%. Remained the same
16218434167

Pull #356

github

web-flow
Merge d54d2595e into c6ba47b57
Pull Request #356: chore: moving common classes into src/common

368 of 579 branches covered (63.56%)

Branch coverage included in aggregate %.

887 of 1087 relevant lines covered (81.6%)

128.08 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

66.51
/src/common/atlas/apiClient.ts
1
import createClient, { Client, Middleware } from "openapi-fetch";
2
import type { FetchOptions } from "openapi-fetch";
3
import { AccessToken, ClientCredentials } from "simple-oauth2";
4
import { ApiClientError } from "./apiClientError.js";
5
import { paths, operations } from "./openapi.js";
6
import { CommonProperties, TelemetryEvent } from "../../telemetry/types.js";
7
import { packageInfo } from "../packageInfo.js";
8
import logger, { LogId } from "../logger.js";
9

10
const ATLAS_API_VERSION = "2025-03-12";
76✔
11

12
export interface ApiClientCredentials {
13
    clientId: string;
14
    clientSecret: string;
15
}
16

17
export interface ApiClientOptions {
18
    credentials?: ApiClientCredentials;
19
    baseUrl: string;
20
    userAgent?: string;
21
}
22

23
export class ApiClient {
24
    private options: {
25
        baseUrl: string;
26
        userAgent: string;
27
        credentials?: {
28
            clientId: string;
29
            clientSecret: string;
30
        };
31
    };
32
    private client: Client<paths>;
33
    private oauth2Client?: ClientCredentials;
34
    private accessToken?: AccessToken;
35

36
    private getAccessToken = async () => {
110✔
37
        if (this.oauth2Client && (!this.accessToken || this.accessToken.expired())) {
903✔
38
            this.accessToken = await this.oauth2Client.getToken({});
12✔
39
        }
40
        return this.accessToken?.token.access_token as string | undefined;
903✔
41
    };
42

43
    private authMiddleware: Middleware = {
110✔
44
        onRequest: async ({ request, schemaPath }) => {
45
            if (schemaPath.startsWith("/api/private/unauth") || schemaPath.startsWith("/api/oauth")) {
899!
46
                return undefined;
×
47
            }
48

49
            try {
899✔
50
                const accessToken = await this.getAccessToken();
899✔
51
                if (accessToken) {
899!
52
                    request.headers.set("Authorization", `Bearer ${accessToken}`);
899✔
53
                }
54
                return request;
899✔
55
            } catch {
56
                // ignore not availble tokens, API will return 401
57
            }
58
        },
59
    };
60

61
    constructor(options: ApiClientOptions) {
62
        this.options = {
110✔
63
            ...options,
64
            userAgent:
65
                options.userAgent ||
204✔
66
                `AtlasMCP/${packageInfo.version} (${process.platform}; ${process.arch}; ${process.env.HOSTNAME || "unknown"})`,
188✔
67
        };
68

69
        this.client = createClient<paths>({
110✔
70
            baseUrl: this.options.baseUrl,
71
            headers: {
72
                "User-Agent": this.options.userAgent,
73
                Accept: `application/vnd.atlas.${ATLAS_API_VERSION}+json`,
74
            },
75
        });
76
        if (this.options.credentials?.clientId && this.options.credentials?.clientSecret) {
110✔
77
            this.oauth2Client = new ClientCredentials({
32✔
78
                client: {
79
                    id: this.options.credentials.clientId,
80
                    secret: this.options.credentials.clientSecret,
81
                },
82
                auth: {
83
                    tokenHost: this.options.baseUrl,
84
                    tokenPath: "/api/oauth/token",
85
                    revokePath: "/api/oauth/revoke",
86
                },
87
                http: {
88
                    headers: {
89
                        "User-Agent": this.options.userAgent,
90
                    },
91
                },
92
            });
93
            this.client.use(this.authMiddleware);
32✔
94
        }
95
    }
96

97
    public hasCredentials(): boolean {
98
        return !!this.oauth2Client;
6✔
99
    }
100

101
    public async validateAccessToken(): Promise<void> {
102
        await this.getAccessToken();
×
103
    }
104

105
    public async close(): Promise<void> {
106
        if (this.accessToken) {
68✔
107
            try {
12✔
108
                await this.accessToken.revoke("access_token");
12✔
109
            } catch (error: unknown) {
110
                const err = error instanceof Error ? error : new Error(String(error));
1!
111
                logger.error(LogId.atlasApiRevokeFailure, "apiClient", `Failed to revoke access token: ${err.message}`);
1✔
112
            }
113
            this.accessToken = undefined;
12✔
114
        }
115
    }
116

117
    public async getIpInfo(): Promise<{
118
        currentIpv4Address: string;
119
    }> {
120
        const accessToken = await this.getAccessToken();
4✔
121

122
        const endpoint = "api/private/ipinfo";
4✔
123
        const url = new URL(endpoint, this.options.baseUrl);
4✔
124
        const response = await fetch(url, {
4✔
125
            method: "GET",
126
            headers: {
127
                Accept: "application/json",
128
                Authorization: `Bearer ${accessToken}`,
129
                "User-Agent": this.options.userAgent,
130
            },
131
        });
132

133
        if (!response.ok) {
4!
134
            throw await ApiClientError.fromResponse(response);
×
135
        }
136

137
        return (await response.json()) as Promise<{
4✔
138
            currentIpv4Address: string;
139
        }>;
140
    }
141

142
    public async sendEvents(events: TelemetryEvent<CommonProperties>[]): Promise<void> {
143
        if (!this.options.credentials) {
10!
144
            await this.sendUnauthEvents(events);
×
145
            return;
×
146
        }
147

148
        try {
10✔
149
            await this.sendAuthEvents(events);
10✔
150
        } catch (error) {
151
            if (error instanceof ApiClientError) {
8✔
152
                if (error.response.status !== 401) {
4!
153
                    throw error;
×
154
                }
155
            }
156

157
            // send unauth events if any of the following are true:
158
            // 1: the token is not valid (not ApiClientError)
159
            // 2: if the api responded with 401 (ApiClientError with status 401)
160
            await this.sendUnauthEvents(events);
8✔
161
        }
162
    }
163

164
    private async sendAuthEvents(events: TelemetryEvent<CommonProperties>[]): Promise<void> {
165
        const accessToken = await this.getAccessToken();
10✔
166
        if (!accessToken) {
8✔
167
            throw new Error("No access token available");
2✔
168
        }
169
        const authUrl = new URL("api/private/v1.0/telemetry/events", this.options.baseUrl);
6✔
170
        const response = await fetch(authUrl, {
6✔
171
            method: "POST",
172
            headers: {
173
                Accept: "application/json",
174
                "Content-Type": "application/json",
175
                "User-Agent": this.options.userAgent,
176
                Authorization: `Bearer ${accessToken}`,
177
            },
178
            body: JSON.stringify(events),
179
        });
180

181
        if (!response.ok) {
6✔
182
            throw await ApiClientError.fromResponse(response);
4✔
183
        }
184
    }
185

186
    private async sendUnauthEvents(events: TelemetryEvent<CommonProperties>[]): Promise<void> {
187
        const headers: Record<string, string> = {
8✔
188
            Accept: "application/json",
189
            "Content-Type": "application/json",
190
            "User-Agent": this.options.userAgent,
191
        };
192

193
        const unauthUrl = new URL("api/private/unauth/telemetry/events", this.options.baseUrl);
8✔
194
        const response = await fetch(unauthUrl, {
8✔
195
            method: "POST",
196
            headers,
197
            body: JSON.stringify(events),
198
        });
199

200
        if (!response.ok) {
8✔
201
            throw await ApiClientError.fromResponse(response);
2✔
202
        }
203
    }
204

205
    // DO NOT EDIT. This is auto-generated code.
206
    async listClustersForAllProjects(options?: FetchOptions<operations["listClustersForAllProjects"]>) {
207
        const { data, error, response } = await this.client.GET("/api/atlas/v2/clusters", options);
×
208
        if (error) {
×
209
            throw ApiClientError.fromError(response, error);
×
210
        }
211
        return data;
×
212
    }
213

214
    async listProjects(options?: FetchOptions<operations["listProjects"]>) {
215
        const { data, error, response } = await this.client.GET("/api/atlas/v2/groups", options);
8✔
216
        if (error) {
8✔
217
            throw ApiClientError.fromError(response, error);
2✔
218
        }
219
        return data;
6✔
220
    }
221

222
    async createProject(options: FetchOptions<operations["createProject"]>) {
223
        const { data, error, response } = await this.client.POST("/api/atlas/v2/groups", options);
10✔
224
        if (error) {
10!
225
            throw ApiClientError.fromError(response, error);
×
226
        }
227
        return data;
10✔
228
    }
229

230
    async deleteProject(options: FetchOptions<operations["deleteProject"]>) {
231
        const { error, response } = await this.client.DELETE("/api/atlas/v2/groups/{groupId}", options);
10✔
232
        if (error) {
10!
233
            throw ApiClientError.fromError(response, error);
×
234
        }
235
    }
236

237
    async getProject(options: FetchOptions<operations["getProject"]>) {
238
        const { data, error, response } = await this.client.GET("/api/atlas/v2/groups/{groupId}", options);
2✔
239
        if (error) {
2!
240
            throw ApiClientError.fromError(response, error);
×
241
        }
242
        return data;
2✔
243
    }
244

245
    async listProjectIpAccessLists(options: FetchOptions<operations["listProjectIpAccessLists"]>) {
246
        const { data, error, response } = await this.client.GET("/api/atlas/v2/groups/{groupId}/accessList", options);
2✔
247
        if (error) {
2!
248
            throw ApiClientError.fromError(response, error);
×
249
        }
250
        return data;
2✔
251
    }
252

253
    async createProjectIpAccessList(options: FetchOptions<operations["createProjectIpAccessList"]>) {
254
        const { data, error, response } = await this.client.POST("/api/atlas/v2/groups/{groupId}/accessList", options);
4✔
255
        if (error) {
4!
256
            throw ApiClientError.fromError(response, error);
×
257
        }
258
        return data;
4✔
259
    }
260

261
    async deleteProjectIpAccessList(options: FetchOptions<operations["deleteProjectIpAccessList"]>) {
262
        const { error, response } = await this.client.DELETE(
10✔
263
            "/api/atlas/v2/groups/{groupId}/accessList/{entryValue}",
264
            options
265
        );
266
        if (error) {
10!
267
            throw ApiClientError.fromError(response, error);
×
268
        }
269
    }
270

271
    async listAlerts(options: FetchOptions<operations["listAlerts"]>) {
272
        const { data, error, response } = await this.client.GET("/api/atlas/v2/groups/{groupId}/alerts", options);
2✔
273
        if (error) {
2!
274
            throw ApiClientError.fromError(response, error);
×
275
        }
276
        return data;
2✔
277
    }
278

279
    async listClusters(options: FetchOptions<operations["listClusters"]>) {
280
        const { data, error, response } = await this.client.GET("/api/atlas/v2/groups/{groupId}/clusters", options);
2✔
281
        if (error) {
2!
282
            throw ApiClientError.fromError(response, error);
×
283
        }
284
        return data;
2✔
285
    }
286

287
    async createCluster(options: FetchOptions<operations["createCluster"]>) {
288
        const { data, error, response } = await this.client.POST("/api/atlas/v2/groups/{groupId}/clusters", options);
2✔
289
        if (error) {
2!
290
            throw ApiClientError.fromError(response, error);
×
291
        }
292
        return data;
2✔
293
    }
294

295
    async deleteCluster(options: FetchOptions<operations["deleteCluster"]>) {
296
        const { error, response } = await this.client.DELETE(
2✔
297
            "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}",
298
            options
299
        );
300
        if (error) {
2!
301
            throw ApiClientError.fromError(response, error);
×
302
        }
303
    }
304

305
    async getCluster(options: FetchOptions<operations["getCluster"]>) {
306
        const { data, error, response } = await this.client.GET(
813✔
307
            "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}",
308
            options
309
        );
310

311
        if (error) {
813✔
312
            throw ApiClientError.fromError(response, error);
2✔
313
        }
314
        return data;
811✔
315
    }
316

317
    async listDatabaseUsers(options: FetchOptions<operations["listDatabaseUsers"]>) {
318
        const { data, error, response } = await this.client.GET(
2✔
319
            "/api/atlas/v2/groups/{groupId}/databaseUsers",
320
            options
321
        );
322
        if (error) {
2!
323
            throw ApiClientError.fromError(response, error);
×
324
        }
325
        return data;
2✔
326
    }
327

328
    async createDatabaseUser(options: FetchOptions<operations["createDatabaseUser"]>) {
329
        const { data, error, response } = await this.client.POST(
8✔
330
            "/api/atlas/v2/groups/{groupId}/databaseUsers",
331
            options
332
        );
333
        if (error) {
8!
334
            throw ApiClientError.fromError(response, error);
×
335
        }
336
        return data;
8✔
337
    }
338

339
    async deleteDatabaseUser(options: FetchOptions<operations["deleteDatabaseUser"]>) {
340
        const { error, response } = await this.client.DELETE(
12✔
341
            "/api/atlas/v2/groups/{groupId}/databaseUsers/{databaseName}/{username}",
342
            options
343
        );
344
        if (error) {
12✔
345
            throw ApiClientError.fromError(response, error);
6✔
346
        }
347
    }
348

349
    async listFlexClusters(options: FetchOptions<operations["listFlexClusters"]>) {
350
        const { data, error, response } = await this.client.GET("/api/atlas/v2/groups/{groupId}/flexClusters", options);
×
351
        if (error) {
×
352
            throw ApiClientError.fromError(response, error);
×
353
        }
354
        return data;
×
355
    }
356

357
    async createFlexCluster(options: FetchOptions<operations["createFlexCluster"]>) {
358
        const { data, error, response } = await this.client.POST(
×
359
            "/api/atlas/v2/groups/{groupId}/flexClusters",
360
            options
361
        );
362
        if (error) {
×
363
            throw ApiClientError.fromError(response, error);
×
364
        }
365
        return data;
×
366
    }
367

368
    async deleteFlexCluster(options: FetchOptions<operations["deleteFlexCluster"]>) {
369
        const { error, response } = await this.client.DELETE(
×
370
            "/api/atlas/v2/groups/{groupId}/flexClusters/{name}",
371
            options
372
        );
373
        if (error) {
×
374
            throw ApiClientError.fromError(response, error);
×
375
        }
376
    }
377

378
    async getFlexCluster(options: FetchOptions<operations["getFlexCluster"]>) {
379
        const { data, error, response } = await this.client.GET(
×
380
            "/api/atlas/v2/groups/{groupId}/flexClusters/{name}",
381
            options
382
        );
383
        if (error) {
×
384
            throw ApiClientError.fromError(response, error);
×
385
        }
386
        return data;
×
387
    }
388

389
    async listOrganizations(options?: FetchOptions<operations["listOrganizations"]>) {
390
        const { data, error, response } = await this.client.GET("/api/atlas/v2/orgs", options);
14✔
391
        if (error) {
14!
392
            throw ApiClientError.fromError(response, error);
×
393
        }
394
        return data;
14✔
395
    }
396

397
    async listOrganizationProjects(options: FetchOptions<operations["listOrganizationProjects"]>) {
398
        const { data, error, response } = await this.client.GET("/api/atlas/v2/orgs/{orgId}/groups", options);
×
399
        if (error) {
×
400
            throw ApiClientError.fromError(response, error);
×
401
        }
402
        return data;
×
403
    }
404

405
    // DO NOT EDIT. This is auto-generated code.
406
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc