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

mongodb-js / mongodb-mcp-server / 16292877152

15 Jul 2025 12:10PM UTC coverage: 77.385% (+2.1%) from 75.27%
16292877152

push

github

web-flow
chore(tests): switch to vitest (#363)

498 of 687 branches covered (72.49%)

Branch coverage included in aggregate %.

0 of 27 new or added lines in 2 files covered. (0.0%)

293 existing lines in 26 files now uncovered.

2828 of 3611 relevant lines covered (78.32%)

28.46 hits per line

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

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

10
const ATLAS_API_VERSION = "2025-03-12";
2✔
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 {
2✔
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 () => {
2✔
37
        if (this.oauth2Client && (!this.accessToken || this.accessToken.expired())) {
88✔
38
            this.accessToken = await this.oauth2Client.getToken({});
6✔
39
        }
6✔
40
        return this.accessToken?.token.access_token as string | undefined;
88✔
41
    };
88✔
42

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

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

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

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

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

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

105
    public async close(): Promise<void> {
2✔
106
        if (this.accessToken) {
34!
107
            try {
6✔
108
                await this.accessToken.revoke("access_token");
6✔
109
            } catch (error: unknown) {
6✔
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
            }
1✔
113
            this.accessToken = undefined;
6✔
114
        }
6✔
115
    }
34✔
116

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

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

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

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

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

148
        try {
5✔
149
            await this.sendAuthEvents(events);
5✔
150
        } catch (error) {
5✔
151
            if (error instanceof ApiClientError) {
4✔
152
                if (error.response.status !== 401) {
2!
153
                    throw error;
×
UNCOV
154
                }
×
155
            }
2✔
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);
4✔
161
        }
3✔
162
    }
5✔
163

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

311
        if (error) {
43✔
312
            throw ApiClientError.fromError(response, error);
1✔
313
        }
1✔
314
        return data;
42✔
315
    }
43✔
316

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

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

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

349
    async listFlexClusters(options: FetchOptions<operations["listFlexClusters"]>) {
2✔
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);
×
UNCOV
353
        }
×
354
        return data;
×
UNCOV
355
    }
×
356

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

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

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

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

397
    async listOrganizationProjects(options: FetchOptions<operations["listOrganizationProjects"]>) {
2✔
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);
×
UNCOV
401
        }
×
402
        return data;
×
UNCOV
403
    }
×
404

405
    // DO NOT EDIT. This is auto-generated code.
406
}
2✔
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