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

mongodb-js / mongodb-mcp-server / 17158105595

22 Aug 2025 02:33PM UTC coverage: 72.664% (-9.4%) from 82.024%
17158105595

push

github

web-flow
chore(deps): bump the npm_and_yarn group with 2 updates (#415)

749 of 884 branches covered (84.73%)

Branch coverage included in aggregate %.

3730 of 5280 relevant lines covered (70.64%)

64.72 hits per line

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

57.96
/src/common/atlas/apiClient.ts
1
import createClient, { Client, Middleware } from "openapi-fetch";
2✔
2
import type { ClientOptions, FetchOptions } from "openapi-fetch";
3
import { ApiClientError } from "./apiClientError.js";
2✔
4
import { paths, operations } from "./openapi.js";
5
import { CommonProperties, TelemetryEvent } from "../../telemetry/types.js";
6
import { packageInfo } from "../packageInfo.js";
2✔
7
import { LoggerBase, LogId } from "../logger.js";
2✔
8
import { createFetch } from "@mongodb-js/devtools-proxy-support";
2✔
9
import * as oauth from "oauth4webapi";
2✔
10
import { Request as NodeFetchRequest } from "node-fetch";
2✔
11

12
const ATLAS_API_VERSION = "2025-03-12";
2✔
13

14
export interface ApiClientCredentials {
15
    clientId: string;
16
    clientSecret: string;
17
}
18

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

25
export interface AccessToken {
26
    access_token: string;
27
    expires_at?: number;
28
}
29

30
export class ApiClient {
2✔
31
    private readonly options: {
32
        baseUrl: string;
33
        userAgent: string;
34
        credentials?: {
35
            clientId: string;
36
            clientSecret: string;
37
        };
38
    };
39

40
    // createFetch assumes that the first parameter of fetch is always a string
41
    // with the URL. However, fetch can also receive a Request object. While
42
    // the typechecking complains, createFetch does passthrough the parameters
43
    // so it works fine.
44
    private static customFetch: typeof fetch = createFetch({
84✔
45
        useEnvironmentVariableProxies: true,
84✔
46
    }) as unknown as typeof fetch;
84✔
47

48
    private client: Client<paths>;
49

50
    private oauth2Client?: oauth.Client;
51
    private oauth2Issuer?: oauth.AuthorizationServer;
52
    private accessToken?: AccessToken;
53

54
    public hasCredentials(): boolean {
84✔
55
        return !!this.oauth2Client && !!this.oauth2Issuer;
18✔
56
    }
18✔
57

58
    private isAccessTokenValid(): boolean {
84✔
59
        return !!(
8✔
60
            this.accessToken &&
8✔
61
            this.accessToken.expires_at !== undefined &&
6✔
62
            this.accessToken.expires_at > Date.now()
6✔
63
        );
64
    }
8✔
65

66
    private getAccessToken = async (): Promise<string | undefined> => {
84✔
67
        if (!this.hasCredentials()) {
8!
68
            return undefined;
×
69
        }
×
70

71
        if (!this.isAccessTokenValid()) {
8✔
72
            this.accessToken = await this.getNewAccessToken();
4✔
73
        }
4✔
74

75
        return this.accessToken?.access_token;
8✔
76
    };
8✔
77

78
    private authMiddleware: Middleware = {
84✔
79
        onRequest: async ({ request, schemaPath }) => {
84✔
80
            if (schemaPath.startsWith("/api/private/unauth") || schemaPath.startsWith("/api/oauth")) {
4!
81
                return undefined;
×
82
            }
×
83

84
            try {
4✔
85
                const accessToken = await this.getAccessToken();
4✔
86
                if (accessToken) {
4✔
87
                    request.headers.set("Authorization", `Bearer ${accessToken}`);
4✔
88
                }
4✔
89
                return request;
4✔
90
            } catch {
4!
91
                // ignore not availble tokens, API will return 401
92
                return undefined;
×
93
            }
×
94
        },
4✔
95
    };
84✔
96

97
    constructor(
84✔
98
        options: ApiClientOptions,
114✔
99
        public readonly logger: LoggerBase
114✔
100
    ) {
114✔
101
        this.options = {
114✔
102
            ...options,
114✔
103
            userAgent:
114✔
104
                options.userAgent ||
114✔
105
                `AtlasMCP/${packageInfo.version} (${process.platform}; ${process.arch}; ${process.env.HOSTNAME || "unknown"})`,
90✔
106
        };
114✔
107

108
        this.client = createClient<paths>({
114✔
109
            baseUrl: this.options.baseUrl,
114✔
110
            headers: {
114✔
111
                "User-Agent": this.options.userAgent,
114✔
112
                Accept: `application/vnd.atlas.${ATLAS_API_VERSION}+json`,
114✔
113
            },
114✔
114
            fetch: ApiClient.customFetch,
114✔
115
            // NodeFetchRequest has more overloadings than the native Request
116
            // so it complains here. However, the interfaces are actually compatible
117
            // so it's not a real problem, just a type checking problem.
118
            Request: NodeFetchRequest as unknown as ClientOptions["Request"],
114✔
119
        });
114✔
120

121
        if (this.options.credentials?.clientId && this.options.credentials?.clientSecret) {
114✔
122
            this.oauth2Issuer = {
28✔
123
                issuer: this.options.baseUrl,
28✔
124
                token_endpoint: new URL("/api/oauth/token", this.options.baseUrl).toString(),
28✔
125
                revocation_endpoint: new URL("/api/oauth/revoke", this.options.baseUrl).toString(),
28✔
126
                token_endpoint_auth_methods_supported: ["client_secret_basic"],
28✔
127
                grant_types_supported: ["client_credentials"],
28✔
128
            };
28✔
129

130
            this.oauth2Client = {
28✔
131
                client_id: this.options.credentials.clientId,
28✔
132
                client_secret: this.options.credentials.clientSecret,
28✔
133
            };
28✔
134

135
            this.client.use(this.authMiddleware);
28✔
136
        }
28✔
137
    }
114✔
138

139
    private getOauthClientAuth(): { client: oauth.Client | undefined; clientAuth: oauth.ClientAuth | undefined } {
84✔
140
        if (this.options.credentials?.clientId && this.options.credentials.clientSecret) {
88✔
141
            const clientSecret = this.options.credentials.clientSecret;
18✔
142
            const clientId = this.options.credentials.clientId;
18✔
143

144
            // We are using our own ClientAuth because ClientSecretBasic URL encodes wrongly
145
            // the username and password (for example, encodes `_` to %5F, which is wrong).
146
            return {
18✔
147
                client: { client_id: clientId },
18✔
148
                clientAuth: (_as, client, _body, headers): void => {
18✔
149
                    const credentials = Buffer.from(`${clientId}:${clientSecret}`).toString("base64");
10✔
150
                    headers.set("Authorization", `Basic ${credentials}`);
10✔
151
                },
10✔
152
            };
18✔
153
        }
18✔
154

155
        return { client: undefined, clientAuth: undefined };
70✔
156
    }
88✔
157

158
    private async getNewAccessToken(): Promise<AccessToken | undefined> {
84✔
159
        if (!this.hasCredentials() || !this.oauth2Issuer) {
4!
160
            return undefined;
×
161
        }
×
162

163
        const { client, clientAuth } = this.getOauthClientAuth();
4✔
164
        if (client && clientAuth) {
4✔
165
            try {
4✔
166
                const response = await oauth.clientCredentialsGrantRequest(
4✔
167
                    this.oauth2Issuer,
4✔
168
                    client,
4✔
169
                    clientAuth,
4✔
170
                    new URLSearchParams(),
4✔
171
                    {
4✔
172
                        [oauth.customFetch]: ApiClient.customFetch,
4✔
173
                        headers: {
4✔
174
                            "User-Agent": this.options.userAgent,
4✔
175
                        },
4✔
176
                    }
4✔
177
                );
4✔
178

179
                const result = await oauth.processClientCredentialsResponse(this.oauth2Issuer, client, response);
4!
180
                this.accessToken = {
×
181
                    access_token: result.access_token,
×
182
                    expires_at: Date.now() + (result.expires_in ?? 0) * 1000,
4✔
183
                };
4✔
184
            } catch (error: unknown) {
4✔
185
                const err = error instanceof Error ? error : new Error(String(error));
4!
186
                this.logger.error({
4✔
187
                    id: LogId.atlasConnectFailure,
4✔
188
                    context: "apiClient",
4✔
189
                    message: `Failed to request access token: ${err.message}`,
4✔
190
                });
4✔
191
            }
4✔
192
            return this.accessToken;
4✔
193
        }
4!
194

195
        return undefined;
×
196
    }
4✔
197

198
    public async validateAccessToken(): Promise<void> {
84✔
199
        await this.getAccessToken();
4✔
200
    }
4✔
201

202
    public async close(): Promise<void> {
84✔
203
        const { client, clientAuth } = this.getOauthClientAuth();
84✔
204
        try {
84✔
205
            if (this.oauth2Issuer && this.accessToken && client && clientAuth) {
84✔
206
                await oauth.revocationRequest(this.oauth2Issuer, client, clientAuth, this.accessToken.access_token);
6✔
207
            }
2✔
208
        } catch (error: unknown) {
84✔
209
            const err = error instanceof Error ? error : new Error(String(error));
4!
210
            this.logger.error({
4✔
211
                id: LogId.atlasApiRevokeFailure,
4✔
212
                context: "apiClient",
4✔
213
                message: `Failed to revoke access token: ${err.message}`,
4✔
214
            });
4✔
215
        }
4✔
216
        this.accessToken = undefined;
84✔
217
    }
84✔
218

219
    public async getIpInfo(): Promise<{
84✔
220
        currentIpv4Address: string;
221
    }> {
×
222
        const accessToken = await this.getAccessToken();
×
223

224
        const endpoint = "api/private/ipinfo";
×
225
        const url = new URL(endpoint, this.options.baseUrl);
×
226
        const response = await fetch(url, {
×
227
            method: "GET",
×
228
            headers: {
×
229
                Accept: "application/json",
×
230
                Authorization: `Bearer ${accessToken}`,
×
231
                "User-Agent": this.options.userAgent,
×
232
            },
×
233
        });
×
234

235
        if (!response.ok) {
×
236
            throw await ApiClientError.fromResponse(response);
×
237
        }
×
238

239
        return (await response.json()) as Promise<{
×
240
            currentIpv4Address: string;
241
        }>;
242
    }
×
243

244
    public async sendEvents(events: TelemetryEvent<CommonProperties>[]): Promise<void> {
84✔
245
        if (!this.options.credentials) {
10!
246
            await this.sendUnauthEvents(events);
×
247
            return;
×
248
        }
×
249

250
        try {
10✔
251
            await this.sendAuthEvents(events);
10✔
252
        } catch (error) {
10✔
253
            if (error instanceof ApiClientError) {
8✔
254
                if (error.response.status !== 401) {
4!
255
                    throw error;
×
256
                }
×
257
            }
4✔
258

259
            // send unauth events if any of the following are true:
260
            // 1: the token is not valid (not ApiClientError)
261
            // 2: if the api responded with 401 (ApiClientError with status 401)
262
            await this.sendUnauthEvents(events);
8✔
263
        }
6✔
264
    }
10✔
265

266
    private async sendAuthEvents(events: TelemetryEvent<CommonProperties>[]): Promise<void> {
84✔
267
        const accessToken = await this.getAccessToken();
10✔
268
        if (!accessToken) {
10✔
269
            throw new Error("No access token available");
2✔
270
        }
2✔
271
        const authUrl = new URL("api/private/v1.0/telemetry/events", this.options.baseUrl);
6✔
272
        const response = await fetch(authUrl, {
6✔
273
            method: "POST",
6✔
274
            headers: {
6✔
275
                Accept: "application/json",
6✔
276
                "Content-Type": "application/json",
6✔
277
                "User-Agent": this.options.userAgent,
6✔
278
                Authorization: `Bearer ${accessToken}`,
6✔
279
            },
6✔
280
            body: JSON.stringify(events),
6✔
281
        });
6✔
282

283
        if (!response.ok) {
10✔
284
            throw await ApiClientError.fromResponse(response);
4✔
285
        }
4✔
286
    }
10✔
287

288
    private async sendUnauthEvents(events: TelemetryEvent<CommonProperties>[]): Promise<void> {
84✔
289
        const headers: Record<string, string> = {
8✔
290
            Accept: "application/json",
8✔
291
            "Content-Type": "application/json",
8✔
292
            "User-Agent": this.options.userAgent,
8✔
293
        };
8✔
294

295
        const unauthUrl = new URL("api/private/unauth/telemetry/events", this.options.baseUrl);
8✔
296
        const response = await fetch(unauthUrl, {
8✔
297
            method: "POST",
8✔
298
            headers,
8✔
299
            body: JSON.stringify(events),
8✔
300
        });
8✔
301

302
        if (!response.ok) {
8✔
303
            throw await ApiClientError.fromResponse(response);
2✔
304
        }
2✔
305
    }
8✔
306

307
    // DO NOT EDIT. This is auto-generated code.
308
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
309
    async listClustersForAllProjects(options?: FetchOptions<operations["listClustersForAllProjects"]>) {
84✔
310
        const { data, error, response } = await this.client.GET("/api/atlas/v2/clusters", options);
×
311
        if (error) {
×
312
            throw ApiClientError.fromError(response, error);
×
313
        }
×
314
        return data;
×
315
    }
×
316

317
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
318
    async listProjects(options?: FetchOptions<operations["listProjects"]>) {
84✔
319
        const { data, error, response } = await this.client.GET("/api/atlas/v2/groups", options);
4✔
320
        if (error) {
4✔
321
            throw ApiClientError.fromError(response, error);
2✔
322
        }
2✔
323
        return data;
2✔
324
    }
4✔
325

326
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
327
    async createProject(options: FetchOptions<operations["createProject"]>) {
84✔
328
        const { data, error, response } = await this.client.POST("/api/atlas/v2/groups", options);
×
329
        if (error) {
×
330
            throw ApiClientError.fromError(response, error);
×
331
        }
×
332
        return data;
×
333
    }
×
334

335
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
336
    async deleteProject(options: FetchOptions<operations["deleteProject"]>) {
84✔
337
        const { error, response } = await this.client.DELETE("/api/atlas/v2/groups/{groupId}", options);
×
338
        if (error) {
×
339
            throw ApiClientError.fromError(response, error);
×
340
        }
×
341
    }
×
342

343
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
344
    async getProject(options: FetchOptions<operations["getProject"]>) {
84✔
345
        const { data, error, response } = await this.client.GET("/api/atlas/v2/groups/{groupId}", options);
×
346
        if (error) {
×
347
            throw ApiClientError.fromError(response, error);
×
348
        }
×
349
        return data;
×
350
    }
×
351

352
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
353
    async listProjectIpAccessLists(options: FetchOptions<operations["listProjectIpAccessLists"]>) {
84✔
354
        const { data, error, response } = await this.client.GET("/api/atlas/v2/groups/{groupId}/accessList", options);
×
355
        if (error) {
×
356
            throw ApiClientError.fromError(response, error);
×
357
        }
×
358
        return data;
×
359
    }
×
360

361
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
362
    async createProjectIpAccessList(options: FetchOptions<operations["createProjectIpAccessList"]>) {
84✔
363
        const { data, error, response } = await this.client.POST("/api/atlas/v2/groups/{groupId}/accessList", options);
×
364
        if (error) {
×
365
            throw ApiClientError.fromError(response, error);
×
366
        }
×
367
        return data;
×
368
    }
×
369

370
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
371
    async deleteProjectIpAccessList(options: FetchOptions<operations["deleteProjectIpAccessList"]>) {
84✔
372
        const { error, response } = await this.client.DELETE(
×
373
            "/api/atlas/v2/groups/{groupId}/accessList/{entryValue}",
×
374
            options
×
375
        );
×
376
        if (error) {
×
377
            throw ApiClientError.fromError(response, error);
×
378
        }
×
379
    }
×
380

381
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
382
    async listAlerts(options: FetchOptions<operations["listAlerts"]>) {
84✔
383
        const { data, error, response } = await this.client.GET("/api/atlas/v2/groups/{groupId}/alerts", options);
×
384
        if (error) {
×
385
            throw ApiClientError.fromError(response, error);
×
386
        }
×
387
        return data;
×
388
    }
×
389

390
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
391
    async listClusters(options: FetchOptions<operations["listClusters"]>) {
84✔
392
        const { data, error, response } = await this.client.GET("/api/atlas/v2/groups/{groupId}/clusters", options);
×
393
        if (error) {
×
394
            throw ApiClientError.fromError(response, error);
×
395
        }
×
396
        return data;
×
397
    }
×
398

399
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
400
    async createCluster(options: FetchOptions<operations["createCluster"]>) {
84✔
401
        const { data, error, response } = await this.client.POST("/api/atlas/v2/groups/{groupId}/clusters", options);
×
402
        if (error) {
×
403
            throw ApiClientError.fromError(response, error);
×
404
        }
×
405
        return data;
×
406
    }
×
407

408
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
409
    async deleteCluster(options: FetchOptions<operations["deleteCluster"]>) {
84✔
410
        const { error, response } = await this.client.DELETE(
×
411
            "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}",
×
412
            options
×
413
        );
×
414
        if (error) {
×
415
            throw ApiClientError.fromError(response, error);
×
416
        }
×
417
    }
×
418

419
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
420
    async getCluster(options: FetchOptions<operations["getCluster"]>) {
84✔
421
        const { data, error, response } = await this.client.GET(
×
422
            "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}",
×
423
            options
×
424
        );
×
425
        if (error) {
×
426
            throw ApiClientError.fromError(response, error);
×
427
        }
×
428
        return data;
×
429
    }
×
430

431
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
432
    async listDatabaseUsers(options: FetchOptions<operations["listDatabaseUsers"]>) {
84✔
433
        const { data, error, response } = await this.client.GET(
×
434
            "/api/atlas/v2/groups/{groupId}/databaseUsers",
×
435
            options
×
436
        );
×
437
        if (error) {
×
438
            throw ApiClientError.fromError(response, error);
×
439
        }
×
440
        return data;
×
441
    }
×
442

443
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
444
    async createDatabaseUser(options: FetchOptions<operations["createDatabaseUser"]>) {
84✔
445
        const { data, error, response } = await this.client.POST(
×
446
            "/api/atlas/v2/groups/{groupId}/databaseUsers",
×
447
            options
×
448
        );
×
449
        if (error) {
×
450
            throw ApiClientError.fromError(response, error);
×
451
        }
×
452
        return data;
×
453
    }
×
454

455
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
456
    async deleteDatabaseUser(options: FetchOptions<operations["deleteDatabaseUser"]>) {
84✔
457
        const { error, response } = await this.client.DELETE(
×
458
            "/api/atlas/v2/groups/{groupId}/databaseUsers/{databaseName}/{username}",
×
459
            options
×
460
        );
×
461
        if (error) {
×
462
            throw ApiClientError.fromError(response, error);
×
463
        }
×
464
    }
×
465

466
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
467
    async listFlexClusters(options: FetchOptions<operations["listFlexClusters"]>) {
84✔
468
        const { data, error, response } = await this.client.GET("/api/atlas/v2/groups/{groupId}/flexClusters", options);
×
469
        if (error) {
×
470
            throw ApiClientError.fromError(response, error);
×
471
        }
×
472
        return data;
×
473
    }
×
474

475
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
476
    async createFlexCluster(options: FetchOptions<operations["createFlexCluster"]>) {
84✔
477
        const { data, error, response } = await this.client.POST(
×
478
            "/api/atlas/v2/groups/{groupId}/flexClusters",
×
479
            options
×
480
        );
×
481
        if (error) {
×
482
            throw ApiClientError.fromError(response, error);
×
483
        }
×
484
        return data;
×
485
    }
×
486

487
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
488
    async deleteFlexCluster(options: FetchOptions<operations["deleteFlexCluster"]>) {
84✔
489
        const { error, response } = await this.client.DELETE(
×
490
            "/api/atlas/v2/groups/{groupId}/flexClusters/{name}",
×
491
            options
×
492
        );
×
493
        if (error) {
×
494
            throw ApiClientError.fromError(response, error);
×
495
        }
×
496
    }
×
497

498
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
499
    async getFlexCluster(options: FetchOptions<operations["getFlexCluster"]>) {
84✔
500
        const { data, error, response } = await this.client.GET(
×
501
            "/api/atlas/v2/groups/{groupId}/flexClusters/{name}",
×
502
            options
×
503
        );
×
504
        if (error) {
×
505
            throw ApiClientError.fromError(response, error);
×
506
        }
×
507
        return data;
×
508
    }
×
509

510
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
511
    async listOrganizations(options?: FetchOptions<operations["listOrganizations"]>) {
84✔
512
        const { data, error, response } = await this.client.GET("/api/atlas/v2/orgs", options);
4!
513
        if (error) {
×
514
            throw ApiClientError.fromError(response, error);
×
515
        }
×
516
        return data;
×
517
    }
4✔
518

519
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
520
    async listOrganizationProjects(options: FetchOptions<operations["listOrganizationProjects"]>) {
84✔
521
        const { data, error, response } = await this.client.GET("/api/atlas/v2/orgs/{orgId}/groups", options);
×
522
        if (error) {
×
523
            throw ApiClientError.fromError(response, error);
×
524
        }
×
525
        return data;
×
526
    }
×
527

528
    // DO NOT EDIT. This is auto-generated code.
529
}
84✔
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

© 2026 Coveralls, Inc