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

mongodb-js / mongodb-mcp-server / 15848919576

24 Jun 2025 11:14AM UTC coverage: 73.971%. First build
15848919576

Pull #318

github

web-flow
Merge e134a2230 into 96c8f62be
Pull Request #318: chore: handle other signals

214 of 377 branches covered (56.76%)

Branch coverage included in aggregate %.

792 of 983 relevant lines covered (80.57%)

55.99 hits per line

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

62.86
/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 "../../helpers/packageInfo.js";
8

9
const ATLAS_API_VERSION = "2025-03-12";
36✔
10

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

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

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

35
    private getAccessToken = async () => {
51✔
36
        if (this.oauth2Client && (!this.accessToken || this.accessToken.expired())) {
100✔
37
            this.accessToken = await this.oauth2Client.getToken({
6✔
38
                agent: this.options.userAgent,
39
            });
40
        }
41
        return this.accessToken?.token.access_token as string | undefined;
100✔
42
    };
43

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

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

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

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

90
    public hasCredentials(): boolean {
91
        return !!(this.oauth2Client && this.accessToken);
1✔
92
    }
93

94
    public async validateAccessToken(): Promise<void> {
95
        await this.getAccessToken();
×
96
    }
97

98
    public async getIpInfo(): Promise<{
99
        currentIpv4Address: string;
100
    }> {
101
        const accessToken = await this.getAccessToken();
2✔
102

103
        const endpoint = "api/private/ipinfo";
2✔
104
        const url = new URL(endpoint, this.options.baseUrl);
2✔
105
        const response = await fetch(url, {
2✔
106
            method: "GET",
107
            headers: {
108
                Accept: "application/json",
109
                Authorization: `Bearer ${accessToken}`,
110
                "User-Agent": this.options.userAgent,
111
            },
112
        });
113

114
        if (!response.ok) {
2!
115
            throw await ApiClientError.fromResponse(response);
×
116
        }
117

118
        return (await response.json()) as Promise<{
2✔
119
            currentIpv4Address: string;
120
        }>;
121
    }
122

123
    public async sendEvents(events: TelemetryEvent<CommonProperties>[]): Promise<void> {
124
        if (!this.options.credentials) {
5!
125
            await this.sendUnauthEvents(events);
×
126
            return;
×
127
        }
128

129
        try {
5✔
130
            await this.sendAuthEvents(events);
5✔
131
        } catch (error) {
132
            if (error instanceof ApiClientError) {
4✔
133
                if (error.response.status !== 401) {
2!
134
                    throw error;
×
135
                }
136
            }
137

138
            // send unauth events if any of the following are true:
139
            // 1: the token is not valid (not ApiClientError)
140
            // 2: if the api responded with 401 (ApiClientError with status 401)
141
            await this.sendUnauthEvents(events);
4✔
142
        }
143
    }
144

145
    private async sendAuthEvents(events: TelemetryEvent<CommonProperties>[]): Promise<void> {
146
        const accessToken = await this.getAccessToken();
5✔
147
        if (!accessToken) {
4✔
148
            throw new Error("No access token available");
1✔
149
        }
150
        const authUrl = new URL("api/private/v1.0/telemetry/events", this.options.baseUrl);
3✔
151
        const response = await fetch(authUrl, {
3✔
152
            method: "POST",
153
            headers: {
154
                Accept: "application/json",
155
                "Content-Type": "application/json",
156
                "User-Agent": this.options.userAgent,
157
                Authorization: `Bearer ${accessToken}`,
158
            },
159
            body: JSON.stringify(events),
160
        });
161

162
        if (!response.ok) {
3✔
163
            throw await ApiClientError.fromResponse(response);
2✔
164
        }
165
    }
166

167
    private async sendUnauthEvents(events: TelemetryEvent<CommonProperties>[]): Promise<void> {
168
        const headers: Record<string, string> = {
4✔
169
            Accept: "application/json",
170
            "Content-Type": "application/json",
171
            "User-Agent": this.options.userAgent,
172
        };
173

174
        const unauthUrl = new URL("api/private/unauth/telemetry/events", this.options.baseUrl);
4✔
175
        const response = await fetch(unauthUrl, {
4✔
176
            method: "POST",
177
            headers,
178
            body: JSON.stringify(events),
179
        });
180

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

186
    // DO NOT EDIT. This is auto-generated code.
187
    async listClustersForAllProjects(options?: FetchOptions<operations["listClustersForAllProjects"]>) {
188
        const { data, error, response } = await this.client.GET("/api/atlas/v2/clusters", options);
×
189
        if (error) {
×
190
            throw ApiClientError.fromError(response, error);
×
191
        }
192
        return data;
×
193
    }
194

195
    async listProjects(options?: FetchOptions<operations["listProjects"]>) {
196
        const { data, error, response } = await this.client.GET("/api/atlas/v2/groups", options);
4✔
197
        if (error) {
4✔
198
            throw ApiClientError.fromError(response, error);
1✔
199
        }
200
        return data;
3✔
201
    }
202

203
    async createProject(options: FetchOptions<operations["createProject"]>) {
204
        const { data, error, response } = await this.client.POST("/api/atlas/v2/groups", options);
5✔
205
        if (error) {
5!
206
            throw ApiClientError.fromError(response, error);
×
207
        }
208
        return data;
5✔
209
    }
210

211
    async deleteProject(options: FetchOptions<operations["deleteProject"]>) {
212
        const { error, response } = await this.client.DELETE("/api/atlas/v2/groups/{groupId}", options);
5✔
213
        if (error) {
5!
214
            throw ApiClientError.fromError(response, error);
×
215
        }
216
    }
217

218
    async getProject(options: FetchOptions<operations["getProject"]>) {
219
        const { data, error, response } = await this.client.GET("/api/atlas/v2/groups/{groupId}", options);
1✔
220
        if (error) {
1!
221
            throw ApiClientError.fromError(response, error);
×
222
        }
223
        return data;
1✔
224
    }
225

226
    async listProjectIpAccessLists(options: FetchOptions<operations["listProjectIpAccessLists"]>) {
227
        const { data, error, response } = await this.client.GET("/api/atlas/v2/groups/{groupId}/accessList", options);
1✔
228
        if (error) {
1!
229
            throw ApiClientError.fromError(response, error);
×
230
        }
231
        return data;
1✔
232
    }
233

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

242
    async deleteProjectIpAccessList(options: FetchOptions<operations["deleteProjectIpAccessList"]>) {
243
        const { error, response } = await this.client.DELETE(
5✔
244
            "/api/atlas/v2/groups/{groupId}/accessList/{entryValue}",
245
            options
246
        );
247
        if (error) {
5!
248
            throw ApiClientError.fromError(response, error);
×
249
        }
250
    }
251

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

260
    async listClusters(options: FetchOptions<operations["listClusters"]>) {
261
        const { data, error, response } = await this.client.GET("/api/atlas/v2/groups/{groupId}/clusters", options);
1✔
262
        if (error) {
1!
263
            throw ApiClientError.fromError(response, error);
×
264
        }
265
        return data;
1✔
266
    }
267

268
    async createCluster(options: FetchOptions<operations["createCluster"]>) {
269
        const { data, error, response } = await this.client.POST("/api/atlas/v2/groups/{groupId}/clusters", options);
1✔
270
        if (error) {
1!
271
            throw ApiClientError.fromError(response, error);
×
272
        }
273
        return data;
1✔
274
    }
275

276
    async deleteCluster(options: FetchOptions<operations["deleteCluster"]>) {
277
        const { error, response } = await this.client.DELETE(
1✔
278
            "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}",
279
            options
280
        );
281
        if (error) {
1!
282
            throw ApiClientError.fromError(response, error);
×
283
        }
284
    }
285

286
    async getCluster(options: FetchOptions<operations["getCluster"]>) {
287
        const { data, error, response } = await this.client.GET(
55✔
288
            "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}",
289
            options
290
        );
291

292
        if (error) {
55✔
293
            throw ApiClientError.fromError(response, error);
1✔
294
        }
295
        return data;
54✔
296
    }
297

298
    async listDatabaseUsers(options: FetchOptions<operations["listDatabaseUsers"]>) {
299
        const { data, error, response } = await this.client.GET(
1✔
300
            "/api/atlas/v2/groups/{groupId}/databaseUsers",
301
            options
302
        );
303
        if (error) {
1!
304
            throw ApiClientError.fromError(response, error);
×
305
        }
306
        return data;
1✔
307
    }
308

309
    async createDatabaseUser(options: FetchOptions<operations["createDatabaseUser"]>) {
310
        const { data, error, response } = await this.client.POST(
4✔
311
            "/api/atlas/v2/groups/{groupId}/databaseUsers",
312
            options
313
        );
314
        if (error) {
4!
315
            throw ApiClientError.fromError(response, error);
×
316
        }
317
        return data;
4✔
318
    }
319

320
    async deleteDatabaseUser(options: FetchOptions<operations["deleteDatabaseUser"]>) {
321
        const { error, response } = await this.client.DELETE(
6✔
322
            "/api/atlas/v2/groups/{groupId}/databaseUsers/{databaseName}/{username}",
323
            options
324
        );
325
        if (error) {
6✔
326
            throw ApiClientError.fromError(response, error);
2✔
327
        }
328
    }
329

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

338
    async createFlexCluster(options: FetchOptions<operations["createFlexCluster"]>) {
339
        const { data, error, response } = await this.client.POST(
×
340
            "/api/atlas/v2/groups/{groupId}/flexClusters",
341
            options
342
        );
343
        if (error) {
×
344
            throw ApiClientError.fromError(response, error);
×
345
        }
346
        return data;
×
347
    }
348

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

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

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

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

386
    // DO NOT EDIT. This is auto-generated code.
387
}
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