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

mongodb-js / mongodb-mcp-server / 20834320981

08 Jan 2026 10:44PM UTC coverage: 80.143% (+0.5%) from 79.647%
20834320981

Pull #836

github

web-flow
Merge 7c4970c65 into 7043c219d
Pull Request #836: [DRAFT] Add configurable api / auth client support

1524 of 1979 branches covered (77.01%)

Branch coverage included in aggregate %.

197 of 206 new or added lines in 5 files covered. (95.63%)

71 existing lines in 3 files now uncovered.

7004 of 8662 relevant lines covered (80.86%)

89.63 hits per line

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

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

13
const ATLAS_API_VERSION = "2025-03-12";
3✔
14

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

21
export type ApiClientFactoryFn = (options: ApiClientOptions, logger: LoggerBase) => ApiClient;
22

23
export const createAtlasApiClient: ApiClientFactoryFn = (options, logger) => {
3✔
24
    return new ApiClient(options, logger);
145✔
25
};
145✔
26

27
export class ApiClient {
3✔
28
    private readonly options: {
29
        baseUrl: string;
30
        userAgent: string;
31
    };
32

33
    // createFetch assumes that the first parameter of fetch is always a string
34
    // with the URL. However, fetch can also receive a Request object. While
35
    // the typechecking complains, createFetch does passthrough the parameters
36
    // so it works fine.
37
    private static customFetch: typeof fetch = createFetch({
3✔
38
        useEnvironmentVariableProxies: true,
3✔
39
    }) as unknown as typeof fetch;
3✔
40

41
    private client: Client<paths>;
42

43
    public hasCredentials(): boolean {
3✔
44
        return !!this.authClient?.hasCredentials();
32✔
45
    }
32✔
46

47
    constructor(
3✔
48
        options: ApiClientOptions,
157✔
49
        public readonly logger: LoggerBase,
157✔
50
        public readonly authClient?: AuthClient
157✔
51
    ) {
157✔
52
        this.options = {
157✔
53
            ...options,
157✔
54
            userAgent:
157✔
55
                options.userAgent ??
157✔
56
                `AtlasMCP/${packageInfo.version} (${process.platform}; ${process.arch}; ${process.env.HOSTNAME || "unknown"})`,
145✔
57
        };
157✔
58

59
        this.authClient =
157✔
60
            authClient ??
157✔
61
            AuthClientBuilder.build(
157✔
62
                {
157✔
63
                    apiBaseUrl: this.options.baseUrl,
157✔
64
                    userAgent: this.options.userAgent,
157✔
65
                    credentials: options.credentials ?? {},
157!
66
                },
157✔
67
                logger
157✔
68
            );
157✔
69

70
        this.client = createClient<paths>({
157✔
71
            baseUrl: this.options.baseUrl,
157✔
72
            headers: {
157✔
73
                "User-Agent": this.options.userAgent,
157✔
74
                Accept: `application/vnd.atlas.${ATLAS_API_VERSION}+json`,
157✔
75
            },
157✔
76
            fetch: ApiClient.customFetch,
157✔
77
            // NodeFetchRequest has more overloadings than the native Request
78
            // so it complains here. However, the interfaces are actually compatible
79
            // so it's not a real problem, just a type checking problem.
80
            Request: NodeFetchRequest as unknown as ClientOptions["Request"],
157✔
81
        });
157✔
82

83
        if (this.authClient) {
157!
84
            this.client.use(this.authClient.createAuthMiddleware());
25✔
85
        }
25✔
86
    }
157✔
87

88
    public async validateAccessToken(): Promise<void> {
3✔
89
        await this.authClient?.validateAccessToken();
20✔
90
    }
20✔
91

92
    public async close(): Promise<void> {
3✔
93
        await this.authClient?.revokeAccessToken();
125!
94
    }
125✔
95

96
    public async getIpInfo(): Promise<{
3✔
97
        currentIpv4Address: string;
98
    }> {
26✔
99
        const authHeaders = (await this.authClient?.authHeaders()) ?? {};
26!
100

101
        const endpoint = "api/private/ipinfo";
26✔
102
        const url = new URL(endpoint, this.options.baseUrl);
26✔
103
        const response = await fetch(url, {
26✔
104
            method: "GET",
26✔
105
            headers: {
26✔
106
                ...authHeaders,
26✔
107
                Accept: "application/json",
26✔
108
                "User-Agent": this.options.userAgent,
26✔
109
            },
26✔
110
        });
26✔
111

112
        if (!response.ok) {
26!
113
            throw await ApiClientError.fromResponse(response);
2✔
114
        }
2!
115

116
        return (await response.json()) as Promise<{
24✔
117
            currentIpv4Address: string;
118
        }>;
119
    }
26✔
120

121
    public async sendEvents(events: TelemetryEvent<CommonProperties>[]): Promise<void> {
3✔
122
        if (!this.authClient) {
129!
123
            await this.sendUnauthEvents(events);
111✔
124
            return;
108✔
125
        }
108!
126

127
        try {
18✔
128
            await this.sendAuthEvents(events);
18✔
129
        } catch (error) {
19!
130
            if (error instanceof ApiClientError) {
11✔
131
                if (error.response.status !== 401) {
2!
UNCOV
132
                    throw error;
×
UNCOV
133
                }
×
134
            }
2✔
135

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

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

160
        if (!response.ok) {
11!
161
            throw await ApiClientError.fromResponse(response);
2✔
162
        }
2✔
163
    }
18✔
164

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

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

179
        if (!response.ok) {
121!
180
            throw await ApiClientError.fromResponse(response);
1✔
181
        }
1✔
182
    }
122✔
183

184
    // DO NOT EDIT. This is auto-generated code.
185
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
186
    async listClusterDetails(options?: FetchOptions<operations["listClusterDetails"]>) {
3✔
UNCOV
187
        const { data, error, response } = await this.client.GET("/api/atlas/v2/clusters", options);
×
UNCOV
188
        if (error) {
×
UNCOV
189
            throw ApiClientError.fromError(response, error);
×
UNCOV
190
        }
×
UNCOV
191
        return data;
×
UNCOV
192
    }
×
193

194
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
195
    async listGroups(options?: FetchOptions<operations["listGroups"]>) {
3✔
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
        }
1✔
200
        return data;
3✔
201
    }
4✔
202

203
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
204
    async createGroup(options: FetchOptions<operations["createGroup"]>) {
3✔
205
        const { data, error, response } = await this.client.POST("/api/atlas/v2/groups", options);
7✔
206
        if (error) {
7!
UNCOV
207
            throw ApiClientError.fromError(response, error);
×
UNCOV
208
        }
×
209
        return data;
7✔
210
    }
7✔
211

212
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
213
    async deleteGroup(options: FetchOptions<operations["deleteGroup"]>) {
3✔
214
        const { error, response } = await this.client.DELETE("/api/atlas/v2/groups/{groupId}", options);
7✔
215
        if (error) {
7✔
216
            throw ApiClientError.fromError(response, error);
2✔
217
        }
2✔
218
    }
7✔
219

220
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
221
    async getGroup(options: FetchOptions<operations["getGroup"]>) {
3✔
222
        const { data, error, response } = await this.client.GET("/api/atlas/v2/groups/{groupId}", options);
1✔
223
        if (error) {
1!
UNCOV
224
            throw ApiClientError.fromError(response, error);
×
UNCOV
225
        }
×
226
        return data;
1✔
227
    }
1✔
228

229
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
230
    async listAccessListEntries(options: FetchOptions<operations["listGroupAccessListEntries"]>) {
3✔
231
        const { data, error, response } = await this.client.GET("/api/atlas/v2/groups/{groupId}/accessList", options);
4✔
232
        if (error) {
4!
233
            throw ApiClientError.fromError(response, error);
×
234
        }
×
235
        return data;
4✔
236
    }
4✔
237

238
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
239
    async createAccessListEntry(options: FetchOptions<operations["createGroupAccessListEntry"]>) {
3✔
240
        const { data, error, response } = await this.client.POST("/api/atlas/v2/groups/{groupId}/accessList", options);
18✔
241
        if (error) {
18!
242
            throw ApiClientError.fromError(response, error);
1✔
243
        }
1!
244
        return data;
17✔
245
    }
18✔
246

247
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
248
    async deleteAccessListEntry(options: FetchOptions<operations["deleteGroupAccessListEntry"]>) {
3✔
249
        const { error, response } = await this.client.DELETE(
5✔
250
            "/api/atlas/v2/groups/{groupId}/accessList/{entryValue}",
5✔
251
            options
5✔
252
        );
5✔
253
        if (error) {
5!
UNCOV
254
            throw ApiClientError.fromError(response, error);
×
UNCOV
255
        }
×
256
    }
5✔
257

258
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
259
    async listAlerts(options: FetchOptions<operations["listGroupAlerts"]>) {
3✔
260
        const { data, error, response } = await this.client.GET("/api/atlas/v2/groups/{groupId}/alerts", options);
1✔
261
        if (error) {
1!
UNCOV
262
            throw ApiClientError.fromError(response, error);
×
UNCOV
263
        }
×
264
        return data;
1✔
265
    }
1✔
266

267
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
268
    async listClusters(options: FetchOptions<operations["listGroupClusters"]>) {
3✔
269
        const { data, error, response } = await this.client.GET("/api/atlas/v2/groups/{groupId}/clusters", options);
1✔
270
        if (error) {
1!
UNCOV
271
            throw ApiClientError.fromError(response, error);
×
UNCOV
272
        }
×
273
        return data;
1✔
274
    }
1✔
275

276
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
277
    async createCluster(options: FetchOptions<operations["createGroupCluster"]>) {
3✔
278
        const { data, error, response } = await this.client.POST("/api/atlas/v2/groups/{groupId}/clusters", options);
2✔
279
        if (error) {
2!
280
            throw ApiClientError.fromError(response, error);
×
281
        }
×
282
        return data;
2✔
283
    }
2✔
284

285
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
286
    async deleteCluster(options: FetchOptions<operations["deleteGroupCluster"]>) {
3✔
287
        const { error, response } = await this.client.DELETE(
2✔
288
            "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}",
2✔
289
            options
2✔
290
        );
2✔
291
        if (error) {
2!
UNCOV
292
            throw ApiClientError.fromError(response, error);
×
UNCOV
293
        }
×
294
    }
2✔
295

296
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
297
    async getCluster(options: FetchOptions<operations["getGroupCluster"]>) {
3✔
298
        const { data, error, response } = await this.client.GET(
17✔
299
            "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}",
17✔
300
            options
17✔
301
        );
17✔
302
        if (error) {
17!
UNCOV
303
            throw ApiClientError.fromError(response, error);
×
UNCOV
304
        }
×
305
        return data;
17✔
306
    }
17✔
307

308
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
309
    async listDropIndexSuggestions(
3✔
UNCOV
310
        options: FetchOptions<operations["listGroupClusterPerformanceAdvisorDropIndexSuggestions"]>
×
UNCOV
311
    ) {
×
UNCOV
312
        const { data, error, response } = await this.client.GET(
×
UNCOV
313
            "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/performanceAdvisor/dropIndexSuggestions",
×
UNCOV
314
            options
×
UNCOV
315
        );
×
UNCOV
316
        if (error) {
×
UNCOV
317
            throw ApiClientError.fromError(response, error);
×
318
        }
×
319
        return data;
×
UNCOV
320
    }
×
321

322
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
323
    async listSchemaAdvice(options: FetchOptions<operations["listGroupClusterPerformanceAdvisorSchemaAdvice"]>) {
3✔
UNCOV
324
        const { data, error, response } = await this.client.GET(
×
UNCOV
325
            "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/performanceAdvisor/schemaAdvice",
×
UNCOV
326
            options
×
UNCOV
327
        );
×
UNCOV
328
        if (error) {
×
329
            throw ApiClientError.fromError(response, error);
×
330
        }
×
UNCOV
331
        return data;
×
UNCOV
332
    }
×
333

334
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
335
    async listClusterSuggestedIndexes(
3✔
336
        options: FetchOptions<operations["listGroupClusterPerformanceAdvisorSuggestedIndexes"]>
×
337
    ) {
×
338
        const { data, error, response } = await this.client.GET(
×
339
            "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/performanceAdvisor/suggestedIndexes",
×
340
            options
×
341
        );
×
342
        if (error) {
×
343
            throw ApiClientError.fromError(response, error);
×
344
        }
×
345
        return data;
×
346
    }
×
347

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

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

372
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
373
    async deleteDatabaseUser(options: FetchOptions<operations["deleteGroupDatabaseUser"]>) {
3✔
374
        const { error, response } = await this.client.DELETE(
11✔
375
            "/api/atlas/v2/groups/{groupId}/databaseUsers/{databaseName}/{username}",
11✔
376
            options
11✔
377
        );
11✔
378
        if (error) {
11✔
379
            throw ApiClientError.fromError(response, error);
3✔
380
        }
3✔
381
    }
11✔
382

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

392
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
393
    async createFlexCluster(options: FetchOptions<operations["createGroupFlexCluster"]>) {
3✔
394
        const { data, error, response } = await this.client.POST(
×
UNCOV
395
            "/api/atlas/v2/groups/{groupId}/flexClusters",
×
UNCOV
396
            options
×
UNCOV
397
        );
×
UNCOV
398
        if (error) {
×
UNCOV
399
            throw ApiClientError.fromError(response, error);
×
UNCOV
400
        }
×
UNCOV
401
        return data;
×
UNCOV
402
    }
×
403

404
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
405
    async deleteFlexCluster(options: FetchOptions<operations["deleteGroupFlexCluster"]>) {
3✔
UNCOV
406
        const { error, response } = await this.client.DELETE(
×
UNCOV
407
            "/api/atlas/v2/groups/{groupId}/flexClusters/{name}",
×
UNCOV
408
            options
×
UNCOV
409
        );
×
UNCOV
410
        if (error) {
×
411
            throw ApiClientError.fromError(response, error);
×
412
        }
×
413
    }
×
414

415
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
416
    async getFlexCluster(options: FetchOptions<operations["getGroupFlexCluster"]>) {
3✔
UNCOV
417
        const { data, error, response } = await this.client.GET(
×
UNCOV
418
            "/api/atlas/v2/groups/{groupId}/flexClusters/{name}",
×
UNCOV
419
            options
×
420
        );
×
421
        if (error) {
×
422
            throw ApiClientError.fromError(response, error);
×
423
        }
×
424
        return data;
×
425
    }
×
426

427
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
428
    async listSlowQueryLogs(options: FetchOptions<operations["listGroupProcessPerformanceAdvisorSlowQueryLogs"]>) {
3✔
UNCOV
429
        const { data, error, response } = await this.client.GET(
×
UNCOV
430
            "/api/atlas/v2/groups/{groupId}/processes/{processId}/performanceAdvisor/slowQueryLogs",
×
UNCOV
431
            options
×
432
        );
×
433
        if (error) {
×
434
            throw ApiClientError.fromError(response, error);
×
435
        }
×
436
        return data;
×
437
    }
×
438

439
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
440
    async listOrgs(options?: FetchOptions<operations["listOrgs"]>) {
3✔
441
        const { data, error, response } = await this.client.GET("/api/atlas/v2/orgs", options);
12!
442
        if (error) {
10!
443
            throw ApiClientError.fromError(response, error);
×
444
        }
×
445
        return data;
10✔
446
    }
12✔
447

448
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
449
    async getOrgGroups(options: FetchOptions<operations["getOrgGroups"]>) {
3✔
450
        const { data, error, response } = await this.client.GET("/api/atlas/v2/orgs/{orgId}/groups", options);
1✔
451
        if (error) {
1!
UNCOV
452
            throw ApiClientError.fromError(response, error);
×
UNCOV
453
        }
×
454
        return data;
1✔
455
    }
1✔
456

457
    // DO NOT EDIT. This is auto-generated code.
458
}
3✔
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