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

mongodb-js / mongodb-mcp-server / 16770485719

06 Aug 2025 07:30AM UTC coverage: 81.319% (-0.06%) from 81.381%
16770485719

push

github

web-flow
chore: enable some eslint rules (#426)

598 of 778 branches covered (76.86%)

Branch coverage included in aggregate %.

5 of 13 new or added lines in 4 files covered. (38.46%)

6 existing lines in 2 files now uncovered.

3298 of 4013 relevant lines covered (82.18%)

53.37 hits per line

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

76.98
/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 logger, { 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 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({
72✔
45
        useEnvironmentVariableProxies: true,
72✔
46
    }) as unknown as typeof fetch;
72✔
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 {
72✔
55
        return !!this.oauth2Client && !!this.oauth2Issuer;
110✔
56
    }
110✔
57

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

66
    private getAccessToken = async () => {
72✔
67
        if (!this.hasCredentials()) {
93!
68
            return undefined;
×
69
        }
×
70

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

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

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

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

97
    constructor(options: ApiClientOptions) {
72✔
98
        this.options = {
100✔
99
            ...options,
100✔
100
            userAgent:
100✔
101
                options.userAgent ||
100✔
102
                `AtlasMCP/${packageInfo.version} (${process.platform}; ${process.arch}; ${process.env.HOSTNAME || "unknown"})`,
76✔
103
        };
100✔
104

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

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

127
            this.oauth2Client = {
60✔
128
                client_id: this.options.credentials.clientId,
60✔
129
                client_secret: this.options.credentials.clientSecret,
60✔
130
            };
60✔
131

132
            this.client.use(this.authMiddleware);
60✔
133
        }
60✔
134
    }
100✔
135

136
    private getOauthClientAuth(): { client: oauth.Client | undefined; clientAuth: oauth.ClientAuth | undefined } {
72✔
137
        if (this.options.credentials?.clientId && this.options.credentials.clientSecret) {
85✔
138
            const clientSecret = this.options.credentials.clientSecret;
57✔
139
            const clientId = this.options.credentials.clientId;
57✔
140

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

152
        return { client: undefined, clientAuth: undefined };
28✔
153
    }
85✔
154

155
    private async getNewAccessToken(): Promise<AccessToken | undefined> {
72✔
156
        if (!this.hasCredentials() || !this.oauth2Issuer) {
11!
157
            return undefined;
×
158
        }
×
159

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

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

NEW
188
        return undefined;
×
189
    }
11✔
190

191
    public async validateAccessToken(): Promise<void> {
72✔
192
        await this.getAccessToken();
5✔
193
    }
5✔
194

195
    public async close(): Promise<void> {
72✔
196
        const { client, clientAuth } = this.getOauthClientAuth();
74✔
197
        try {
74✔
198
            if (this.oauth2Issuer && this.accessToken && client && clientAuth) {
74✔
199
                await oauth.revocationRequest(this.oauth2Issuer, client, clientAuth, this.accessToken.access_token);
13✔
200
            }
8✔
201
        } catch (error: unknown) {
74✔
202
            const err = error instanceof Error ? error : new Error(String(error));
4!
203
            logger.error(LogId.atlasApiRevokeFailure, "apiClient", `Failed to revoke access token: ${err.message}`);
4✔
204
        }
4✔
205
        this.accessToken = undefined;
73✔
206
    }
74✔
207

208
    public async getIpInfo(): Promise<{
72✔
209
        currentIpv4Address: string;
210
    }> {
13✔
211
        const accessToken = await this.getAccessToken();
13✔
212

213
        const endpoint = "api/private/ipinfo";
13✔
214
        const url = new URL(endpoint, this.options.baseUrl);
13✔
215
        const response = await fetch(url, {
13✔
216
            method: "GET",
13✔
217
            headers: {
13✔
218
                Accept: "application/json",
13✔
219
                Authorization: `Bearer ${accessToken}`,
13✔
220
                "User-Agent": this.options.userAgent,
13✔
221
            },
13✔
222
        });
13✔
223

224
        if (!response.ok) {
13!
225
            throw await ApiClientError.fromResponse(response);
×
226
        }
×
227

228
        return (await response.json()) as Promise<{
13✔
229
            currentIpv4Address: string;
230
        }>;
231
    }
13✔
232

233
    public async sendEvents(events: TelemetryEvent<CommonProperties>[]): Promise<void> {
72✔
234
        if (!this.options.credentials) {
10!
235
            await this.sendUnauthEvents(events);
×
236
            return;
×
237
        }
×
238

239
        try {
10✔
240
            await this.sendAuthEvents(events);
10✔
241
        } catch (error) {
10✔
242
            if (error instanceof ApiClientError) {
8✔
243
                if (error.response.status !== 401) {
4!
244
                    throw error;
×
245
                }
×
246
            }
4✔
247

248
            // send unauth events if any of the following are true:
249
            // 1: the token is not valid (not ApiClientError)
250
            // 2: if the api responded with 401 (ApiClientError with status 401)
251
            await this.sendUnauthEvents(events);
8✔
252
        }
6✔
253
    }
10✔
254

255
    private async sendAuthEvents(events: TelemetryEvent<CommonProperties>[]): Promise<void> {
72✔
256
        const accessToken = await this.getAccessToken();
10✔
257
        if (!accessToken) {
10✔
258
            throw new Error("No access token available");
2✔
259
        }
2✔
260
        const authUrl = new URL("api/private/v1.0/telemetry/events", this.options.baseUrl);
6✔
261
        const response = await fetch(authUrl, {
6✔
262
            method: "POST",
6✔
263
            headers: {
6✔
264
                Accept: "application/json",
6✔
265
                "Content-Type": "application/json",
6✔
266
                "User-Agent": this.options.userAgent,
6✔
267
                Authorization: `Bearer ${accessToken}`,
6✔
268
            },
6✔
269
            body: JSON.stringify(events),
6✔
270
        });
6✔
271

272
        if (!response.ok) {
10✔
273
            throw await ApiClientError.fromResponse(response);
4✔
274
        }
4✔
275
    }
10✔
276

277
    private async sendUnauthEvents(events: TelemetryEvent<CommonProperties>[]): Promise<void> {
72✔
278
        const headers: Record<string, string> = {
8✔
279
            Accept: "application/json",
8✔
280
            "Content-Type": "application/json",
8✔
281
            "User-Agent": this.options.userAgent,
8✔
282
        };
8✔
283

284
        const unauthUrl = new URL("api/private/unauth/telemetry/events", this.options.baseUrl);
8✔
285
        const response = await fetch(unauthUrl, {
8✔
286
            method: "POST",
8✔
287
            headers,
8✔
288
            body: JSON.stringify(events),
8✔
289
        });
8✔
290

291
        if (!response.ok) {
8✔
292
            throw await ApiClientError.fromResponse(response);
2✔
293
        }
2✔
294
    }
8✔
295

296
    // DO NOT EDIT. This is auto-generated code.
297
    async listClustersForAllProjects(options?: FetchOptions<operations["listClustersForAllProjects"]>) {
72✔
298
        const { data, error, response } = await this.client.GET("/api/atlas/v2/clusters", options);
×
299
        if (error) {
×
300
            throw ApiClientError.fromError(response, error);
×
301
        }
×
302
        return data;
×
303
    }
×
304

305
    async listProjects(options?: FetchOptions<operations["listProjects"]>) {
72✔
306
        const { data, error, response } = await this.client.GET("/api/atlas/v2/groups", options);
6✔
307
        if (error) {
6✔
308
            throw ApiClientError.fromError(response, error);
2✔
309
        }
2✔
310
        return data;
4✔
311
    }
6✔
312

313
    async createProject(options: FetchOptions<operations["createProject"]>) {
72✔
314
        const { data, error, response } = await this.client.POST("/api/atlas/v2/groups", options);
5✔
315
        if (error) {
5!
316
            throw ApiClientError.fromError(response, error);
×
317
        }
×
318
        return data;
5✔
319
    }
5✔
320

321
    async deleteProject(options: FetchOptions<operations["deleteProject"]>) {
72✔
322
        const { error, response } = await this.client.DELETE("/api/atlas/v2/groups/{groupId}", options);
5✔
323
        if (error) {
5!
324
            throw ApiClientError.fromError(response, error);
×
325
        }
×
326
    }
5✔
327

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

336
    async listProjectIpAccessLists(options: FetchOptions<operations["listProjectIpAccessLists"]>) {
72✔
337
        const { data, error, response } = await this.client.GET("/api/atlas/v2/groups/{groupId}/accessList", options);
4✔
338
        if (error) {
4!
339
            throw ApiClientError.fromError(response, error);
×
340
        }
×
341
        return data;
4✔
342
    }
4✔
343

344
    async createProjectIpAccessList(options: FetchOptions<operations["createProjectIpAccessList"]>) {
72✔
345
        const { data, error, response } = await this.client.POST("/api/atlas/v2/groups/{groupId}/accessList", options);
10✔
346
        if (error) {
10!
347
            throw ApiClientError.fromError(response, error);
×
348
        }
×
349
        return data;
10✔
350
    }
10✔
351

352
    async deleteProjectIpAccessList(options: FetchOptions<operations["deleteProjectIpAccessList"]>) {
72✔
353
        const { error, response } = await this.client.DELETE(
5✔
354
            "/api/atlas/v2/groups/{groupId}/accessList/{entryValue}",
5✔
355
            options
5✔
356
        );
5✔
357
        if (error) {
5!
358
            throw ApiClientError.fromError(response, error);
×
359
        }
×
360
    }
5✔
361

362
    async listAlerts(options: FetchOptions<operations["listAlerts"]>) {
72✔
363
        const { data, error, response } = await this.client.GET("/api/atlas/v2/groups/{groupId}/alerts", options);
1✔
364
        if (error) {
1!
365
            throw ApiClientError.fromError(response, error);
×
366
        }
×
367
        return data;
1✔
368
    }
1✔
369

370
    async listClusters(options: FetchOptions<operations["listClusters"]>) {
72✔
371
        const { data, error, response } = await this.client.GET("/api/atlas/v2/groups/{groupId}/clusters", options);
1✔
372
        if (error) {
1!
373
            throw ApiClientError.fromError(response, error);
×
374
        }
×
375
        return data;
1✔
376
    }
1✔
377

378
    async createCluster(options: FetchOptions<operations["createCluster"]>) {
72✔
379
        const { data, error, response } = await this.client.POST("/api/atlas/v2/groups/{groupId}/clusters", options);
1✔
380
        if (error) {
1!
381
            throw ApiClientError.fromError(response, error);
×
382
        }
×
383
        return data;
1✔
384
    }
1✔
385

386
    async deleteCluster(options: FetchOptions<operations["deleteCluster"]>) {
72✔
387
        const { error, response } = await this.client.DELETE(
1✔
388
            "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}",
1✔
389
            options
1✔
390
        );
1✔
391
        if (error) {
1!
392
            throw ApiClientError.fromError(response, error);
×
393
        }
×
394
    }
1✔
395

396
    async getCluster(options: FetchOptions<operations["getCluster"]>) {
72✔
397
        const { data, error, response } = await this.client.GET(
15✔
398
            "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}",
15✔
399
            options
15✔
400
        );
15✔
401

402
        if (error) {
15✔
403
            throw ApiClientError.fromError(response, error);
1✔
404
        }
1✔
405
        return data;
14✔
406
    }
15✔
407

408
    async listDatabaseUsers(options: FetchOptions<operations["listDatabaseUsers"]>) {
72✔
409
        const { data, error, response } = await this.client.GET(
1✔
410
            "/api/atlas/v2/groups/{groupId}/databaseUsers",
1✔
411
            options
1✔
412
        );
1✔
413
        if (error) {
1!
414
            throw ApiClientError.fromError(response, error);
×
415
        }
×
416
        return data;
1✔
417
    }
1✔
418

419
    async createDatabaseUser(options: FetchOptions<operations["createDatabaseUser"]>) {
72✔
420
        const { data, error, response } = await this.client.POST(
5✔
421
            "/api/atlas/v2/groups/{groupId}/databaseUsers",
5✔
422
            options
5✔
423
        );
5✔
424
        if (error) {
5!
425
            throw ApiClientError.fromError(response, error);
×
426
        }
×
427
        return data;
5✔
428
    }
5✔
429

430
    async deleteDatabaseUser(options: FetchOptions<operations["deleteDatabaseUser"]>) {
72✔
431
        const { error, response } = await this.client.DELETE(
7✔
432
            "/api/atlas/v2/groups/{groupId}/databaseUsers/{databaseName}/{username}",
7✔
433
            options
7✔
434
        );
7✔
435
        if (error) {
7✔
436
            throw ApiClientError.fromError(response, error);
2✔
437
        }
2✔
438
    }
7✔
439

440
    async listFlexClusters(options: FetchOptions<operations["listFlexClusters"]>) {
72✔
441
        const { data, error, response } = await this.client.GET("/api/atlas/v2/groups/{groupId}/flexClusters", options);
×
442
        if (error) {
×
443
            throw ApiClientError.fromError(response, error);
×
444
        }
×
445
        return data;
×
446
    }
×
447

448
    async createFlexCluster(options: FetchOptions<operations["createFlexCluster"]>) {
72✔
449
        const { data, error, response } = await this.client.POST(
×
450
            "/api/atlas/v2/groups/{groupId}/flexClusters",
×
451
            options
×
452
        );
×
453
        if (error) {
×
454
            throw ApiClientError.fromError(response, error);
×
455
        }
×
456
        return data;
×
457
    }
×
458

459
    async deleteFlexCluster(options: FetchOptions<operations["deleteFlexCluster"]>) {
72✔
460
        const { error, response } = await this.client.DELETE(
×
461
            "/api/atlas/v2/groups/{groupId}/flexClusters/{name}",
×
462
            options
×
463
        );
×
464
        if (error) {
×
465
            throw ApiClientError.fromError(response, error);
×
466
        }
×
467
    }
×
468

469
    async getFlexCluster(options: FetchOptions<operations["getFlexCluster"]>) {
72✔
470
        const { data, error, response } = await this.client.GET(
×
471
            "/api/atlas/v2/groups/{groupId}/flexClusters/{name}",
×
472
            options
×
473
        );
×
474
        if (error) {
×
475
            throw ApiClientError.fromError(response, error);
×
476
        }
×
477
        return data;
×
478
    }
×
479

480
    async listOrganizations(options?: FetchOptions<operations["listOrganizations"]>) {
72✔
481
        const { data, error, response } = await this.client.GET("/api/atlas/v2/orgs", options);
11!
482
        if (error) {
7!
483
            throw ApiClientError.fromError(response, error);
×
484
        }
×
485
        return data;
7✔
486
    }
11✔
487

488
    async listOrganizationProjects(options: FetchOptions<operations["listOrganizationProjects"]>) {
72✔
489
        const { data, error, response } = await this.client.GET("/api/atlas/v2/orgs/{orgId}/groups", options);
×
490
        if (error) {
×
491
            throw ApiClientError.fromError(response, error);
×
492
        }
×
493
        return data;
×
494
    }
×
495

496
    // DO NOT EDIT. This is auto-generated code.
497
}
72✔
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