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

mongodb-js / mongodb-mcp-server / 20853900721

09 Jan 2026 01:45PM UTC coverage: 79.931% (+0.3%) from 79.647%
20853900721

Pull #836

github

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

1536 of 1988 branches covered (77.26%)

Branch coverage included in aggregate %.

206 of 209 new or added lines in 6 files covered. (98.56%)

66 existing lines in 2 files now uncovered.

6979 of 8665 relevant lines covered (80.54%)

88.9 hits per line

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

68.86
/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);
145✔
30
};
145✔
31

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

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

46
    private client: Client<paths>;
47

48
    public hasCredentials(): boolean {
3✔
49
        return !!this.authProvider?.hasCredentials();
32✔
50
    }
32✔
51

52
    constructor(
3✔
53
        options: ApiClientOptions,
157✔
54
        public readonly logger: LoggerBase,
157✔
55
        public readonly authProvider?: AuthProvider
157✔
56
    ) {
157✔
57
        this.options = {
157✔
58
            ...options,
157✔
59
            userAgent:
157✔
60
                options.userAgent ??
157✔
61
                `AtlasMCP/${packageInfo.version} (${process.platform}; ${process.arch}; ${process.env.HOSTNAME || "unknown"})`,
145✔
62
        };
157✔
63

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

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

88
        if (this.authProvider) {
157!
89
            this.client.use(this.authProvider.middleware());
25✔
90
        }
25✔
91
    }
157✔
92

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

97
    public async close(): Promise<void> {
3✔
98
        await this.authProvider?.revokeAccessToken();
126!
99
    }
126✔
100

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

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

117
        if (!response.ok) {
26!
118
            throw await ApiClientError.fromResponse(response);
2✔
119
        }
2!
120

121
        return (await response.json()) as Promise<{
24✔
122
            currentIpv4Address: string;
123
        }>;
124
    }
26✔
125

126
    public async sendEvents(events: TelemetryEvent<CommonProperties>[]): Promise<void> {
3✔
127
        if (!this.authProvider) {
129!
128
            await this.sendUnauthEvents(events);
111✔
129
            return;
109✔
130
        }
109!
131

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

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

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

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

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

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

184
        if (!response.ok) {
121!
185
            throw await ApiClientError.fromResponse(response);
1✔
186
        }
1✔
187
    }
122✔
188

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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