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

mongodb-js / mongodb-mcp-server / 16194945357

10 Jul 2025 12:20PM UTC coverage: 74.908% (+0.08%) from 74.831%
16194945357

Pull #352

github

web-flow
Merge 67263f699 into 27c52b49f
Pull Request #352: chore: revoke access tokens on server shutdown [MCP-53]

358 of 566 branches covered (63.25%)

Branch coverage included in aggregate %.

10 of 10 new or added lines in 3 files covered. (100.0%)

49 existing lines in 2 files now uncovered.

866 of 1068 relevant lines covered (81.09%)

56.78 hits per line

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

66.35
/src/common/atlas/apiClient.ts
1
import createClient, { Client, Middleware } from "openapi-fetch";
2
import type { FetchOptions } from "openapi-fetch";
3
import { AccessToken, ClientCredentials } from "simple-oauth2";
4
import { ApiClientError } from "./apiClientError.js";
5
import { paths, operations } from "./openapi.js";
6
import { CommonProperties, TelemetryEvent } from "../../telemetry/types.js";
7
import { packageInfo } from "../../helpers/packageInfo.js";
8

9
const ATLAS_API_VERSION = "2025-03-12";
38✔
10

11
export interface ApiClientCredentials {
12
    clientId: string;
13
    clientSecret: string;
14
}
15

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

22
export class ApiClient {
23
    private options: {
24
        baseUrl: string;
25
        userAgent: string;
26
        credentials?: {
27
            clientId: string;
28
            clientSecret: string;
29
        };
30
    };
31
    private client: Client<paths>;
32
    private oauth2Client?: ClientCredentials;
33
    private accessToken?: AccessToken;
34

35
    private getAccessToken = async () => {
54✔
36
        if (this.oauth2Client && (!this.accessToken || this.accessToken.expired())) {
92✔
37
            this.accessToken = await this.oauth2Client.getToken({
6✔
38
                agent: this.options.userAgent,
39
            });
40
        }
41
        return this.accessToken?.token.access_token as string | undefined;
92✔
42
    };
43

44
    private authMiddleware: Middleware = {
54✔
45
        onRequest: async ({ request, schemaPath }) => {
46
            if (schemaPath.startsWith("/api/private/unauth") || schemaPath.startsWith("/api/oauth")) {
90!
UNCOV
47
                return undefined;
×
48
            }
49

50
            try {
90✔
51
                const accessToken = await this.getAccessToken();
90✔
52
                request.headers.set("Authorization", `Bearer ${accessToken}`);
90✔
53
                return request;
90✔
54
            } catch {
55
                // ignore not availble tokens, API will return 401
56
            }
57
        },
58
    };
59

60
    constructor(options: ApiClientOptions) {
61
        this.options = {
54✔
62
            ...options,
63
            userAgent:
64
                options.userAgent ||
100✔
65
                `AtlasMCP/${packageInfo.version} (${process.platform}; ${process.arch}; ${process.env.HOSTNAME || "unknown"})`,
92✔
66
        };
67

68
        this.client = createClient<paths>({
54✔
69
            baseUrl: this.options.baseUrl,
70
            headers: {
71
                "User-Agent": this.options.userAgent,
72
                Accept: `application/vnd.atlas.${ATLAS_API_VERSION}+json`,
73
            },
74
        });
75
        if (this.options.credentials?.clientId && this.options.credentials?.clientSecret) {
54✔
76
            this.oauth2Client = new ClientCredentials({
16✔
77
                client: {
78
                    id: this.options.credentials.clientId,
79
                    secret: this.options.credentials.clientSecret,
80
                },
81
                auth: {
82
                    tokenHost: this.options.baseUrl,
83
                    tokenPath: "/api/oauth/token",
84
                },
85
            });
86
            this.client.use(this.authMiddleware);
16✔
87
        }
88
    }
89

90
    public hasCredentials(): boolean {
91
        return !!(this.oauth2Client && this.accessToken);
3✔
92
    }
93

94
    public async validateAccessToken(): Promise<void> {
UNCOV
95
        await this.getAccessToken();
×
96
    }
97

98
    public async close(): Promise<void> {
99
        if (this.accessToken) {
336✔
100
            await this.accessToken.revokeAll();
28✔
101
        }
102
    }
103

104
    public async getIpInfo(): Promise<{
105
        currentIpv4Address: string;
106
    }> {
107
        const accessToken = await this.getAccessToken();
2✔
108

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

120
        if (!response.ok) {
2!
UNCOV
121
            throw await ApiClientError.fromResponse(response);
×
122
        }
123

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

129
    public async sendEvents(events: TelemetryEvent<CommonProperties>[]): Promise<void> {
130
        if (!this.options.credentials) {
5!
UNCOV
131
            await this.sendUnauthEvents(events);
×
UNCOV
132
            return;
×
133
        }
134

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

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

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

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

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

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

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

192
    // DO NOT EDIT. This is auto-generated code.
193
    async listClustersForAllProjects(options?: FetchOptions<operations["listClustersForAllProjects"]>) {
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);
×
197
        }
UNCOV
198
        return data;
×
199
    }
200

201
    async listProjects(options?: FetchOptions<operations["listProjects"]>) {
202
        const { data, error, response } = await this.client.GET("/api/atlas/v2/groups", options);
4✔
203
        if (error) {
4✔
204
            throw ApiClientError.fromError(response, error);
1✔
205
        }
206
        return data;
3✔
207
    }
208

209
    async createProject(options: FetchOptions<operations["createProject"]>) {
210
        const { data, error, response } = await this.client.POST("/api/atlas/v2/groups", options);
5✔
211
        if (error) {
5!
UNCOV
212
            throw ApiClientError.fromError(response, error);
×
213
        }
214
        return data;
5✔
215
    }
216

217
    async deleteProject(options: FetchOptions<operations["deleteProject"]>) {
218
        const { error, response } = await this.client.DELETE("/api/atlas/v2/groups/{groupId}", options);
5✔
219
        if (error) {
5!
UNCOV
220
            throw ApiClientError.fromError(response, error);
×
221
        }
222
    }
223

224
    async getProject(options: FetchOptions<operations["getProject"]>) {
225
        const { data, error, response } = await this.client.GET("/api/atlas/v2/groups/{groupId}", options);
1✔
226
        if (error) {
1!
UNCOV
227
            throw ApiClientError.fromError(response, error);
×
228
        }
229
        return data;
1✔
230
    }
231

232
    async listProjectIpAccessLists(options: FetchOptions<operations["listProjectIpAccessLists"]>) {
233
        const { data, error, response } = await this.client.GET("/api/atlas/v2/groups/{groupId}/accessList", options);
1✔
234
        if (error) {
1!
UNCOV
235
            throw ApiClientError.fromError(response, error);
×
236
        }
237
        return data;
1✔
238
    }
239

240
    async createProjectIpAccessList(options: FetchOptions<operations["createProjectIpAccessList"]>) {
241
        const { data, error, response } = await this.client.POST("/api/atlas/v2/groups/{groupId}/accessList", options);
2✔
242
        if (error) {
2!
UNCOV
243
            throw ApiClientError.fromError(response, error);
×
244
        }
245
        return data;
2✔
246
    }
247

248
    async deleteProjectIpAccessList(options: FetchOptions<operations["deleteProjectIpAccessList"]>) {
249
        const { error, response } = await this.client.DELETE(
5✔
250
            "/api/atlas/v2/groups/{groupId}/accessList/{entryValue}",
251
            options
252
        );
253
        if (error) {
5!
UNCOV
254
            throw ApiClientError.fromError(response, error);
×
255
        }
256
    }
257

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

266
    async listClusters(options: FetchOptions<operations["listClusters"]>) {
267
        const { data, error, response } = await this.client.GET("/api/atlas/v2/groups/{groupId}/clusters", options);
1✔
268
        if (error) {
1!
UNCOV
269
            throw ApiClientError.fromError(response, error);
×
270
        }
271
        return data;
1✔
272
    }
273

274
    async createCluster(options: FetchOptions<operations["createCluster"]>) {
275
        const { data, error, response } = await this.client.POST("/api/atlas/v2/groups/{groupId}/clusters", options);
1✔
276
        if (error) {
1!
UNCOV
277
            throw ApiClientError.fromError(response, error);
×
278
        }
279
        return data;
1✔
280
    }
281

282
    async deleteCluster(options: FetchOptions<operations["deleteCluster"]>) {
283
        const { error, response } = await this.client.DELETE(
1✔
284
            "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}",
285
            options
286
        );
287
        if (error) {
1!
UNCOV
288
            throw ApiClientError.fromError(response, error);
×
289
        }
290
    }
291

292
    async getCluster(options: FetchOptions<operations["getCluster"]>) {
293
        const { data, error, response } = await this.client.GET(
47✔
294
            "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}",
295
            options
296
        );
297

298
        if (error) {
47✔
299
            throw ApiClientError.fromError(response, error);
1✔
300
        }
301
        return data;
46✔
302
    }
303

304
    async listDatabaseUsers(options: FetchOptions<operations["listDatabaseUsers"]>) {
305
        const { data, error, response } = await this.client.GET(
1✔
306
            "/api/atlas/v2/groups/{groupId}/databaseUsers",
307
            options
308
        );
309
        if (error) {
1!
UNCOV
310
            throw ApiClientError.fromError(response, error);
×
311
        }
312
        return data;
1✔
313
    }
314

315
    async createDatabaseUser(options: FetchOptions<operations["createDatabaseUser"]>) {
316
        const { data, error, response } = await this.client.POST(
4✔
317
            "/api/atlas/v2/groups/{groupId}/databaseUsers",
318
            options
319
        );
320
        if (error) {
4!
UNCOV
321
            throw ApiClientError.fromError(response, error);
×
322
        }
323
        return data;
4✔
324
    }
325

326
    async deleteDatabaseUser(options: FetchOptions<operations["deleteDatabaseUser"]>) {
327
        const { error, response } = await this.client.DELETE(
6✔
328
            "/api/atlas/v2/groups/{groupId}/databaseUsers/{databaseName}/{username}",
329
            options
330
        );
331
        if (error) {
6✔
332
            throw ApiClientError.fromError(response, error);
2✔
333
        }
334
    }
335

336
    async listFlexClusters(options: FetchOptions<operations["listFlexClusters"]>) {
UNCOV
337
        const { data, error, response } = await this.client.GET("/api/atlas/v2/groups/{groupId}/flexClusters", options);
×
UNCOV
338
        if (error) {
×
UNCOV
339
            throw ApiClientError.fromError(response, error);
×
340
        }
UNCOV
341
        return data;
×
342
    }
343

344
    async createFlexCluster(options: FetchOptions<operations["createFlexCluster"]>) {
UNCOV
345
        const { data, error, response } = await this.client.POST(
×
346
            "/api/atlas/v2/groups/{groupId}/flexClusters",
347
            options
348
        );
UNCOV
349
        if (error) {
×
350
            throw ApiClientError.fromError(response, error);
×
351
        }
352
        return data;
×
353
    }
354

355
    async deleteFlexCluster(options: FetchOptions<operations["deleteFlexCluster"]>) {
UNCOV
356
        const { error, response } = await this.client.DELETE(
×
357
            "/api/atlas/v2/groups/{groupId}/flexClusters/{name}",
358
            options
359
        );
UNCOV
360
        if (error) {
×
UNCOV
361
            throw ApiClientError.fromError(response, error);
×
362
        }
363
    }
364

365
    async getFlexCluster(options: FetchOptions<operations["getFlexCluster"]>) {
UNCOV
366
        const { data, error, response } = await this.client.GET(
×
367
            "/api/atlas/v2/groups/{groupId}/flexClusters/{name}",
368
            options
369
        );
UNCOV
370
        if (error) {
×
UNCOV
371
            throw ApiClientError.fromError(response, error);
×
372
        }
373
        return data;
×
374
    }
375

376
    async listOrganizations(options?: FetchOptions<operations["listOrganizations"]>) {
377
        const { data, error, response } = await this.client.GET("/api/atlas/v2/orgs", options);
7✔
378
        if (error) {
7!
379
            throw ApiClientError.fromError(response, error);
×
380
        }
381
        return data;
7✔
382
    }
383

384
    async listOrganizationProjects(options: FetchOptions<operations["listOrganizationProjects"]>) {
UNCOV
385
        const { data, error, response } = await this.client.GET("/api/atlas/v2/orgs/{orgId}/groups", options);
×
386
        if (error) {
×
UNCOV
387
            throw ApiClientError.fromError(response, error);
×
388
        }
UNCOV
389
        return data;
×
390
    }
391

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