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

mongodb-js / mongodb-mcp-server / 20829584944

08 Jan 2026 07:45PM UTC coverage: 79.774% (+0.1%) from 79.647%
20829584944

Pull #836

github

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

1519 of 1975 branches covered (76.91%)

Branch coverage included in aggregate %.

182 of 191 new or added lines in 5 files covered. (95.29%)

92 existing lines in 3 files now uncovered.

6953 of 8645 relevant lines covered (80.43%)

89.35 hits per line

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

68.36
/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 { AuthClientBuilder, Credentials, AuthClient } from "./auth/authClient.js";
3✔
11

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

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

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

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

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

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

40
    private client: Client<paths>;
41

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

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

56
        this.authClient = authClient ?? AuthClientBuilder.build({
157✔
57
            apiBaseUrl: this.options.baseUrl,
157✔
58
            userAgent: this.options.userAgent,
157✔
59
            credentials: options.credentials ?? {},
157!
60
        }, logger);
157✔
61

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

75
        if (this.authClient) {
157!
76
            this.client.use(this.authClient.createAuthMiddleware());
25✔
77
        }
25✔
78
    }
157✔
79

80
    public async validateAccessToken(): Promise<void> {
3✔
81
        await this.authClient?.validateAccessToken();
20✔
82
    }
20✔
83

84
    public async close(): Promise<void> {
3✔
85
        await this.authClient?.revokeAccessToken();
124!
86
    }
124✔
87

88
    public async getIpInfo(): Promise<{
3✔
89
        currentIpv4Address: string;
90
    }> {
26✔
91
        const authHeaders = await this.authClient?.authHeaders() ?? {};
26!
92

93
        const endpoint = "api/private/ipinfo";
26✔
94
        const url = new URL(endpoint, this.options.baseUrl);
26✔
95
        const response = await fetch(url, {
26✔
96
            method: "GET",
26✔
97
            headers: {
26✔
98
                ...authHeaders,
26✔
99
                Accept: "application/json",
26✔
100
                "User-Agent": this.options.userAgent,
26✔
101
            },
26✔
102
        });
26✔
103

104
        if (!response.ok) {
26!
105
            throw await ApiClientError.fromResponse(response);
2✔
106
        }
2!
107

108
        return (await response.json()) as Promise<{
24✔
109
            currentIpv4Address: string;
110
        }>;
111
    }
26✔
112

113
    public async sendEvents(events: TelemetryEvent<CommonProperties>[]): Promise<void> {
3✔
114
        if (!this.authClient) {
129!
115
            await this.sendUnauthEvents(events);
111✔
116
            return;
107✔
117
        }
107!
118

119
        try {
18✔
120
            await this.sendAuthEvents(events);
18✔
121
        } catch (error) {
19!
122
            if (error instanceof ApiClientError) {
11✔
123
                if (error.response.status !== 401) {
2!
UNCOV
124
                    throw error;
×
UNCOV
125
                }
×
126
            }
2✔
127

128
            // send unauth events if any of the following are true:
129
            // 1: the token is not valid (not ApiClientError)
130
            // 2: if the api responded with 401 (ApiClientError with status 401)
131
            await this.sendUnauthEvents(events);
11✔
132
        }
10✔
133
    }
129✔
134

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

152
        if (!response.ok) {
11!
153
            throw await ApiClientError.fromResponse(response);
2✔
154
        }
2✔
155
    }
18✔
156

157
    private async sendUnauthEvents(events: TelemetryEvent<CommonProperties>[]): Promise<void> {
3✔
158
        const headers: Record<string, string> = {
122✔
159
            Accept: "application/json",
122✔
160
            "Content-Type": "application/json",
122✔
161
            "User-Agent": this.options.userAgent,
122✔
162
        };
122✔
163

164
        const unauthUrl = new URL("api/private/unauth/telemetry/events", this.options.baseUrl);
122✔
165
        const response = await fetch(unauthUrl, {
122✔
166
            method: "POST",
122✔
167
            headers,
122✔
168
            body: JSON.stringify(events),
122✔
169
        });
122✔
170

171
        if (!response.ok) {
121!
172
            throw await ApiClientError.fromResponse(response);
1✔
173
        }
1✔
174
    }
122✔
175

176
    // DO NOT EDIT. This is auto-generated code.
177
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
178
    async listClusterDetails(options?: FetchOptions<operations["listClusterDetails"]>) {
3✔
UNCOV
179
        const { data, error, response } = await this.client.GET("/api/atlas/v2/clusters", options);
×
180
        if (error) {
×
UNCOV
181
            throw ApiClientError.fromError(response, error);
×
UNCOV
182
        }
×
UNCOV
183
        return data;
×
UNCOV
184
    }
×
185

186
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
187
    async listGroups(options?: FetchOptions<operations["listGroups"]>) {
3✔
188
        const { data, error, response } = await this.client.GET("/api/atlas/v2/groups", options);
4✔
189
        if (error) {
4!
190
            throw ApiClientError.fromError(response, error);
1✔
191
        }
1✔
192
        return data;
3✔
193
    }
4✔
194

195
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
196
    async createGroup(options: FetchOptions<operations["createGroup"]>) {
3✔
197
        const { data, error, response } = await this.client.POST("/api/atlas/v2/groups", options);
7✔
198
        if (error) {
7!
UNCOV
199
            throw ApiClientError.fromError(response, error);
×
UNCOV
200
        }
×
201
        return data;
7✔
202
    }
7✔
203

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

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

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

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

239
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
240
    async deleteAccessListEntry(options: FetchOptions<operations["deleteGroupAccessListEntry"]>) {
3✔
241
        const { error, response } = await this.client.DELETE(
5✔
242
            "/api/atlas/v2/groups/{groupId}/accessList/{entryValue}",
5✔
243
            options
5✔
244
        );
5✔
245
        if (error) {
5!
UNCOV
246
            throw ApiClientError.fromError(response, error);
×
UNCOV
247
        }
×
248
    }
5✔
249

250
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
251
    async listAlerts(options: FetchOptions<operations["listGroupAlerts"]>) {
3✔
252
        const { data, error, response } = await this.client.GET("/api/atlas/v2/groups/{groupId}/alerts", options);
1✔
253
        if (error) {
1!
UNCOV
254
            throw ApiClientError.fromError(response, error);
×
UNCOV
255
        }
×
256
        return data;
1✔
257
    }
1✔
258

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

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

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

288
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
289
    async getCluster(options: FetchOptions<operations["getGroupCluster"]>) {
3✔
290
        const { data, error, response } = await this.client.GET(
18✔
291
            "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}",
18✔
292
            options
18✔
293
        );
18✔
294
        if (error) {
18!
UNCOV
295
            throw ApiClientError.fromError(response, error);
×
UNCOV
296
        }
×
297
        return data;
18✔
298
    }
18✔
299

300
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
301
    async listDropIndexSuggestions(
3✔
UNCOV
302
        options: FetchOptions<operations["listGroupClusterPerformanceAdvisorDropIndexSuggestions"]>
×
UNCOV
303
    ) {
×
UNCOV
304
        const { data, error, response } = await this.client.GET(
×
UNCOV
305
            "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/performanceAdvisor/dropIndexSuggestions",
×
306
            options
×
307
        );
×
UNCOV
308
        if (error) {
×
UNCOV
309
            throw ApiClientError.fromError(response, error);
×
UNCOV
310
        }
×
UNCOV
311
        return data;
×
UNCOV
312
    }
×
313

314
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
315
    async listSchemaAdvice(options: FetchOptions<operations["listGroupClusterPerformanceAdvisorSchemaAdvice"]>) {
3✔
UNCOV
316
        const { data, error, response } = await this.client.GET(
×
UNCOV
317
            "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/performanceAdvisor/schemaAdvice",
×
318
            options
×
319
        );
×
UNCOV
320
        if (error) {
×
UNCOV
321
            throw ApiClientError.fromError(response, error);
×
UNCOV
322
        }
×
UNCOV
323
        return data;
×
UNCOV
324
    }
×
325

326
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
327
    async listClusterSuggestedIndexes(
3✔
UNCOV
328
        options: FetchOptions<operations["listGroupClusterPerformanceAdvisorSuggestedIndexes"]>
×
329
    ) {
×
330
        const { data, error, response } = await this.client.GET(
×
UNCOV
331
            "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/performanceAdvisor/suggestedIndexes",
×
UNCOV
332
            options
×
UNCOV
333
        );
×
UNCOV
334
        if (error) {
×
UNCOV
335
            throw ApiClientError.fromError(response, error);
×
336
        }
×
337
        return data;
×
338
    }
×
339

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

352
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
353
    async createDatabaseUser(options: FetchOptions<operations["createGroupDatabaseUser"]>) {
3✔
354
        const { data, error, response } = await this.client.POST(
7✔
355
            "/api/atlas/v2/groups/{groupId}/databaseUsers",
7✔
356
            options
7✔
357
        );
7✔
358
        if (error) {
7!
UNCOV
359
            throw ApiClientError.fromError(response, error);
×
UNCOV
360
        }
×
361
        return data;
7✔
362
    }
7✔
363

364
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
365
    async deleteDatabaseUser(options: FetchOptions<operations["deleteGroupDatabaseUser"]>) {
3✔
366
        const { error, response } = await this.client.DELETE(
9✔
367
            "/api/atlas/v2/groups/{groupId}/databaseUsers/{databaseName}/{username}",
9✔
368
            options
9✔
369
        );
9✔
370
        if (error) {
9✔
371
            throw ApiClientError.fromError(response, error);
2✔
372
        }
2✔
373
    }
9✔
374

375
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
376
    async listFlexClusters(options: FetchOptions<operations["listGroupFlexClusters"]>) {
3✔
UNCOV
377
        const { data, error, response } = await this.client.GET("/api/atlas/v2/groups/{groupId}/flexClusters", options);
×
UNCOV
378
        if (error) {
×
UNCOV
379
            throw ApiClientError.fromError(response, error);
×
UNCOV
380
        }
×
381
        return data;
×
382
    }
×
383

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

396
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
397
    async deleteFlexCluster(options: FetchOptions<operations["deleteGroupFlexCluster"]>) {
3✔
UNCOV
398
        const { error, response } = await this.client.DELETE(
×
UNCOV
399
            "/api/atlas/v2/groups/{groupId}/flexClusters/{name}",
×
UNCOV
400
            options
×
UNCOV
401
        );
×
UNCOV
402
        if (error) {
×
UNCOV
403
            throw ApiClientError.fromError(response, error);
×
UNCOV
404
        }
×
UNCOV
405
    }
×
406

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

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

431
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
432
    async listOrgs(options?: FetchOptions<operations["listOrgs"]>) {
3✔
433
        const { data, error, response } = await this.client.GET("/api/atlas/v2/orgs", options);
12!
434
        if (error) {
10!
435
            throw ApiClientError.fromError(response, error);
×
436
        }
×
437
        return data;
10✔
438
    }
12✔
439

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

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