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

mongodb-js / mongodb-mcp-server / 20934316151

12 Jan 2026 08:42PM UTC coverage: 79.702% (-0.06%) from 79.762%
20934316151

push

github

web-flow
feat: Add configurable api / auth client support (#836)

1532 of 1994 branches covered (76.83%)

Branch coverage included in aggregate %.

196 of 202 new or added lines in 8 files covered. (97.03%)

25 existing lines in 1 file now uncovered.

6985 of 8692 relevant lines covered (80.36%)

87.88 hits per line

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

69.0
/src/common/atlas/apiClient.ts
1
import createClient from "openapi-fetch";
3✔
2
import type { ClientOptions, FetchOptions, Client, Middleware } 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 isAuthConfigured(): boolean {
3✔
43
        return !!this.authProvider;
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.createAuthMiddleware());
25✔
92
        }
25✔
93
    }
49✔
94

95
    private createAuthMiddleware(): Middleware {
3✔
96
        return {
25✔
97
            onRequest: async ({ request, schemaPath }): Promise<Request | undefined> => {
25✔
98
                if (schemaPath.startsWith("/api/private/unauth") || schemaPath.startsWith("/api/oauth")) {
95!
NEW
99
                    return undefined;
×
NEW
100
                }
×
101

102
                try {
95✔
103
                    const authHeaders = (await this.authProvider?.getAuthHeaders()) ?? {};
95!
104
                    for (const [key, value] of Object.entries(authHeaders)) {
95✔
105
                        request.headers.set(key, value);
94✔
106
                    }
94✔
107
                    return request;
95✔
108
                } catch {
95!
109
                    // ignore not available tokens, API will return 401
NEW
110
                    return undefined;
×
NEW
111
                }
×
112
            },
95✔
113
        };
25✔
114
    }
25✔
115

116
    public async validateAuthConfig(): Promise<void> {
3✔
117
        await this.authProvider?.validate();
20✔
118
    }
20✔
119

120
    public async close(): Promise<void> {
3✔
121
        await this.authProvider?.revoke();
34✔
122
    }
34✔
123

124
    public async getIpInfo(): Promise<{
3✔
125
        currentIpv4Address: string;
126
    }> {
26✔
127
        const authHeaders = (await this.authProvider?.getAuthHeaders()) ?? {};
26!
128

129
        const endpoint = "api/private/ipinfo";
26✔
130
        const url = new URL(endpoint, this.options.baseUrl);
26✔
131
        const response = await fetch(url, {
26✔
132
            method: "GET",
26✔
133
            headers: {
26✔
134
                ...authHeaders,
26✔
135
                Accept: "application/json",
26✔
136
                "User-Agent": this.options.userAgent,
26✔
137
            },
26✔
138
        });
26✔
139

140
        if (!response.ok) {
26!
141
            throw await ApiClientError.fromResponse(response);
2✔
142
        }
2!
143

144
        return (await response.json()) as Promise<{
24✔
145
            currentIpv4Address: string;
146
        }>;
147
    }
26✔
148

149
    public async sendEvents(events: TelemetryEvent<CommonProperties>[]): Promise<void> {
3✔
150
        if (!this.authProvider) {
36!
151
            await this.sendUnauthEvents(events);
18✔
152
            return;
16✔
153
        }
16✔
154

155
        try {
18✔
156
            await this.sendAuthEvents(events);
18✔
157
        } catch (error) {
18!
158
            if (error instanceof ApiClientError) {
11✔
159
                if (error.response.status !== 401) {
2!
160
                    throw error;
×
161
                }
×
162
            }
2✔
163

164
            // send unauth events if any of the following are true:
165
            // 1: the token is not valid (not ApiClientError)
166
            // 2: if the api responded with 401 (ApiClientError with status 401)
167
            await this.sendUnauthEvents(events);
11✔
168
        }
10✔
169
    }
36✔
170

171
    private async sendAuthEvents(events: TelemetryEvent<CommonProperties>[]): Promise<void> {
3✔
172
        const authHeaders = await this.authProvider?.getAuthHeaders();
18✔
173
        if (!authHeaders) {
18!
174
            throw new Error("No access token available");
8✔
175
        }
8✔
176
        const authUrl = new URL("api/private/v1.0/telemetry/events", this.options.baseUrl);
9✔
177
        const response = await fetch(authUrl, {
9✔
178
            method: "POST",
9✔
179
            headers: {
9✔
180
                ...authHeaders,
9✔
181
                Accept: "application/json",
9✔
182
                "Content-Type": "application/json",
9✔
183
                "User-Agent": this.options.userAgent,
9✔
184
            },
9✔
185
            body: JSON.stringify(events),
9✔
186
        });
9✔
187

188
        if (!response.ok) {
11!
189
            throw await ApiClientError.fromResponse(response);
2✔
190
        }
2✔
191
    }
18✔
192

193
    private async sendUnauthEvents(events: TelemetryEvent<CommonProperties>[]): Promise<void> {
3✔
194
        const headers: Record<string, string> = {
29✔
195
            Accept: "application/json",
29✔
196
            "Content-Type": "application/json",
29✔
197
            "User-Agent": this.options.userAgent,
29✔
198
        };
29✔
199

200
        const unauthUrl = new URL("api/private/unauth/telemetry/events", this.options.baseUrl);
29✔
201
        const response = await fetch(unauthUrl, {
29✔
202
            method: "POST",
29✔
203
            headers,
29✔
204
            body: JSON.stringify(events),
29✔
205
        });
29✔
206

207
        if (!response.ok) {
28✔
208
            throw await ApiClientError.fromResponse(response);
1✔
209
        }
1✔
210
    }
29✔
211

212
    // DO NOT EDIT. This is auto-generated code.
213
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
214
    async listClusterDetails(options?: FetchOptions<operations["listClusterDetails"]>) {
3✔
215
        const { data, error, response } = await this.client.GET("/api/atlas/v2/clusters", options);
×
216
        if (error) {
×
217
            throw ApiClientError.fromError(response, error);
×
218
        }
×
219
        return data;
×
220
    }
×
221

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

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

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

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

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

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

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

286
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
287
    async listAlerts(options: FetchOptions<operations["listGroupAlerts"]>) {
3✔
288
        const { data, error, response } = await this.client.GET("/api/atlas/v2/groups/{groupId}/alerts", options);
1✔
289
        if (error) {
1!
290
            throw ApiClientError.fromError(response, error);
×
291
        }
×
292
        return data;
1✔
293
    }
1✔
294

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

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

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

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

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

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

362
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
363
    async listClusterSuggestedIndexes(
3✔
364
        options: FetchOptions<operations["listGroupClusterPerformanceAdvisorSuggestedIndexes"]>
×
365
    ) {
×
366
        const { data, error, response } = await this.client.GET(
×
367
            "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/performanceAdvisor/suggestedIndexes",
×
368
            options
×
369
        );
×
370
        if (error) {
×
371
            throw ApiClientError.fromError(response, error);
×
372
        }
×
373
        return data;
×
374
    }
×
375

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

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

400
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
401
    async deleteDatabaseUser(options: FetchOptions<operations["deleteGroupDatabaseUser"]>) {
3✔
402
        const { error, response } = await this.client.DELETE(
9✔
403
            "/api/atlas/v2/groups/{groupId}/databaseUsers/{databaseName}/{username}",
9✔
404
            options
9✔
405
        );
9✔
406
        if (error) {
9✔
407
            throw ApiClientError.fromError(response, error);
2✔
408
        }
2✔
409
    }
9✔
410

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

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

432
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
433
    async deleteFlexCluster(options: FetchOptions<operations["deleteGroupFlexCluster"]>) {
3✔
434
        const { error, response } = await this.client.DELETE(
×
435
            "/api/atlas/v2/groups/{groupId}/flexClusters/{name}",
×
436
            options
×
437
        );
×
438
        if (error) {
×
439
            throw ApiClientError.fromError(response, error);
×
440
        }
×
441
    }
×
442

443
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
444
    async getFlexCluster(options: FetchOptions<operations["getGroupFlexCluster"]>) {
3✔
445
        const { data, error, response } = await this.client.GET(
×
446
            "/api/atlas/v2/groups/{groupId}/flexClusters/{name}",
×
447
            options
×
448
        );
×
449
        if (error) {
×
450
            throw ApiClientError.fromError(response, error);
×
451
        }
×
452
        return data;
×
453
    }
×
454

455
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
456
    async listSlowQueryLogs(options: FetchOptions<operations["listGroupProcessPerformanceAdvisorSlowQueryLogs"]>) {
3✔
457
        const { data, error, response } = await this.client.GET(
×
458
            "/api/atlas/v2/groups/{groupId}/processes/{processId}/performanceAdvisor/slowQueryLogs",
×
459
            options
×
460
        );
×
461
        if (error) {
×
462
            throw ApiClientError.fromError(response, error);
×
463
        }
×
464
        return data;
×
465
    }
×
466

467
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
468
    async listOrgs(options?: FetchOptions<operations["listOrgs"]>) {
3✔
469
        const { data, error, response } = await this.client.GET("/api/atlas/v2/orgs", options);
12!
470
        if (error) {
10!
471
            throw ApiClientError.fromError(response, error);
×
472
        }
×
473
        return data;
10✔
474
    }
12✔
475

476
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
477
    async getOrgGroups(options: FetchOptions<operations["getOrgGroups"]>) {
3✔
478
        const { data, error, response } = await this.client.GET("/api/atlas/v2/orgs/{orgId}/groups", options);
1✔
479
        if (error) {
1!
480
            throw ApiClientError.fromError(response, error);
×
481
        }
×
482
        return data;
1✔
483
    }
1✔
484

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