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

mongodb-js / mongodb-mcp-server / 20924126887

12 Jan 2026 03:04PM UTC coverage: 80.021% (+0.5%) from 79.476%
20924126887

Pull #836

github

web-flow
Merge 91a94d791 into da1a1852f
Pull Request #836: feat: Add configurable api / auth client support

1545 of 2003 branches covered (77.13%)

Branch coverage included in aggregate %.

202 of 205 new or added lines in 6 files covered. (98.54%)

61 existing lines in 1 file now uncovered.

7018 of 8698 relevant lines covered (80.69%)

88.36 hits per line

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

69.07
/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, AuthProvider } from "./auth/authProvider.js";
11
import { AuthProviderFactory } from "./auth/authProvider.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
    requestContext?: RequestContext;
20
}
21

22
type RequestContext = {
23
    headers?: Record<string, string | string[] | undefined>;
24
};
25

26
export type ApiClientFactoryFn = (options: ApiClientOptions, logger: LoggerBase) => ApiClient;
27

28
export const createAtlasApiClient: ApiClientFactoryFn = (options, logger) => {
3✔
29
    return new ApiClient(options, logger);
37✔
30
};
37✔
31

32
export class ApiClient {
3✔
33
    private readonly options: {
34
        baseUrl: string;
35
        userAgent: string;
36
    };
37

38
    private customFetch: typeof fetch;
39

40
    private client: Client<paths>;
41

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

46
    constructor(
3✔
47
        options: ApiClientOptions,
49✔
48
        public readonly logger: LoggerBase,
49✔
49
        public readonly authProvider?: AuthProvider
49✔
50
    ) {
49✔
51
        // createFetch assumes that the first parameter of fetch is always a string
52
        // with the URL. However, fetch can also receive a Request object. While
53
        // the typechecking complains, createFetch does passthrough the parameters
54
        // so it works fine. That said, node-fetch has incompatibilities with the web version
55
        // of fetch and can lead to genuine issues so we would like to move away of node-fetch dependency.
56
        this.customFetch = createFetch({
49✔
57
            useEnvironmentVariableProxies: true,
49✔
58
        }) as unknown as typeof fetch;
49✔
59
        this.options = {
49✔
60
            ...options,
49✔
61
            userAgent:
49✔
62
                options.userAgent ??
49✔
63
                `AtlasMCP/${packageInfo.version} (${process.platform}; ${process.arch}; ${process.env.HOSTNAME || "unknown"})`,
37✔
64
        };
49✔
65

66
        this.authProvider =
49✔
67
            authProvider ??
49✔
68
            AuthProviderFactory.create(
49✔
69
                {
49✔
70
                    apiBaseUrl: this.options.baseUrl,
49✔
71
                    userAgent: this.options.userAgent,
49✔
72
                    credentials: options.credentials ?? {},
49!
73
                },
49✔
74
                logger
49✔
75
            );
49✔
76

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

90
        if (this.authProvider) {
49✔
91
            this.client.use(this.authProvider.middleware());
25✔
92
        }
25✔
93
    }
49✔
94

95
    public async validateAccessToken(): Promise<void> {
3✔
96
        await this.authProvider?.getAccessToken();
20✔
97
    }
20✔
98

99
    public async close(): Promise<void> {
3✔
100
        await this.authProvider?.revokeAccessToken();
33✔
101
    }
33✔
102

103
    public async getIpInfo(): Promise<{
3✔
104
        currentIpv4Address: string;
105
    }> {
27✔
106
        const authHeaders = (await this.authProvider?.getAuthHeaders()) ?? {};
27!
107

108
        const endpoint = "api/private/ipinfo";
27✔
109
        const url = new URL(endpoint, this.options.baseUrl);
27✔
110
        const response = await fetch(url, {
27✔
111
            method: "GET",
27✔
112
            headers: {
27✔
113
                ...authHeaders,
27✔
114
                Accept: "application/json",
27✔
115
                "User-Agent": this.options.userAgent,
27✔
116
            },
27✔
117
        });
27✔
118

119
        if (!response.ok) {
27!
120
            throw await ApiClientError.fromResponse(response);
2✔
121
        }
2!
122

123
        return (await response.json()) as Promise<{
25✔
124
            currentIpv4Address: string;
125
        }>;
126
    }
27✔
127

128
    public async sendEvents(events: TelemetryEvent<CommonProperties>[]): Promise<void> {
3✔
129
        if (!this.authProvider) {
36!
130
            await this.sendUnauthEvents(events);
18✔
131
            return;
15✔
132
        }
15✔
133

134
        try {
18✔
135
            await this.sendAuthEvents(events);
18✔
136
        } catch (error) {
18!
137
            if (error instanceof ApiClientError) {
11✔
138
                if (error.response.status !== 401) {
2!
UNCOV
139
                    throw error;
×
UNCOV
140
                }
×
141
            }
2✔
142

143
            // send unauth events if any of the following are true:
144
            // 1: the token is not valid (not ApiClientError)
145
            // 2: if the api responded with 401 (ApiClientError with status 401)
146
            await this.sendUnauthEvents(events);
11✔
147
        }
10✔
148
    }
36✔
149

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

167
        if (!response.ok) {
11!
168
            throw await ApiClientError.fromResponse(response);
2✔
169
        }
2✔
170
    }
18✔
171

172
    private async sendUnauthEvents(events: TelemetryEvent<CommonProperties>[]): Promise<void> {
3✔
173
        const headers: Record<string, string> = {
29✔
174
            Accept: "application/json",
29✔
175
            "Content-Type": "application/json",
29✔
176
            "User-Agent": this.options.userAgent,
29✔
177
        };
29✔
178

179
        const unauthUrl = new URL("api/private/unauth/telemetry/events", this.options.baseUrl);
29✔
180
        const response = await fetch(unauthUrl, {
29✔
181
            method: "POST",
29✔
182
            headers,
29✔
183
            body: JSON.stringify(events),
29✔
184
        });
29✔
185

186
        if (!response.ok) {
28✔
187
            throw await ApiClientError.fromResponse(response);
1✔
188
        }
1✔
189
    }
29✔
190

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

201
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
202
    async listGroups(options?: FetchOptions<operations["listGroups"]>) {
3✔
203
        const { data, error, response } = await this.client.GET("/api/atlas/v2/groups", options);
4✔
204
        if (error) {
4!
205
            throw ApiClientError.fromError(response, error);
1✔
206
        }
1✔
207
        return data;
3✔
208
    }
4✔
209

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

219
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
220
    async deleteGroup(options: FetchOptions<operations["deleteGroup"]>) {
3✔
221
        const { error, response } = await this.client.DELETE("/api/atlas/v2/groups/{groupId}", options);
7✔
222
        if (error) {
7✔
223
            throw ApiClientError.fromError(response, error);
2✔
224
        }
2✔
225
    }
7✔
226

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

236
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
237
    async listAccessListEntries(options: FetchOptions<operations["listGroupAccessListEntries"]>) {
3✔
238
        const { data, error, response } = await this.client.GET("/api/atlas/v2/groups/{groupId}/accessList", options);
4✔
239
        if (error) {
4!
UNCOV
240
            throw ApiClientError.fromError(response, error);
×
UNCOV
241
        }
×
242
        return data;
4✔
243
    }
4✔
244

245
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
246
    async createAccessListEntry(options: FetchOptions<operations["createGroupAccessListEntry"]>) {
3✔
247
        const { data, error, response } = await this.client.POST("/api/atlas/v2/groups/{groupId}/accessList", options);
19✔
248
        if (error) {
19!
249
            throw ApiClientError.fromError(response, error);
1✔
250
        }
1!
251
        return data;
18✔
252
    }
19✔
253

254
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
255
    async deleteAccessListEntry(options: FetchOptions<operations["deleteGroupAccessListEntry"]>) {
3✔
256
        const { error, response } = await this.client.DELETE(
5✔
257
            "/api/atlas/v2/groups/{groupId}/accessList/{entryValue}",
5✔
258
            options
5✔
259
        );
5✔
260
        if (error) {
5!
261
            throw ApiClientError.fromError(response, error);
×
262
        }
×
263
    }
5✔
264

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

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

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

292
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
293
    async deleteCluster(options: FetchOptions<operations["deleteGroupCluster"]>) {
3✔
294
        const { error, response } = await this.client.DELETE(
2✔
295
            "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}",
2✔
296
            options
2✔
297
        );
2✔
298
        if (error) {
2!
299
            throw ApiClientError.fromError(response, error);
×
300
        }
×
301
    }
2✔
302

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

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

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

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

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

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

379
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
380
    async deleteDatabaseUser(options: FetchOptions<operations["deleteGroupDatabaseUser"]>) {
3✔
381
        const { error, response } = await this.client.DELETE(
9✔
382
            "/api/atlas/v2/groups/{groupId}/databaseUsers/{databaseName}/{username}",
9✔
383
            options
9✔
384
        );
9✔
385
        if (error) {
9✔
386
            throw ApiClientError.fromError(response, error);
2✔
387
        }
2✔
388
    }
9✔
389

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

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

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

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

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

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

455
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
456
    async getOrgGroups(options: FetchOptions<operations["getOrgGroups"]>) {
3✔
457
        const { data, error, response } = await this.client.GET("/api/atlas/v2/orgs/{orgId}/groups", options);
1✔
458
        if (error) {
1!
459
            throw ApiClientError.fromError(response, error);
×
460
        }
×
461
        return data;
1✔
462
    }
1✔
463

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