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

mongodb-js / mongodb-mcp-server / 14903288550

08 May 2025 09:30AM UTC coverage: 75.441%. First build
14903288550

Pull #222

github

web-flow
Merge ffeea6bfb into 9e76f9506
Pull Request #222: fix: validate creds

207 of 355 branches covered (58.31%)

Branch coverage included in aggregate %.

12 of 22 new or added lines in 2 files covered. (54.55%)

776 of 948 relevant lines covered (81.86%)

48.16 hits per line

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

60.59
/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";
35✔
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 () => {
49✔
36
        if (this.oauth2Client && (!this.accessToken || this.accessToken.expired())) {
82✔
37
            this.accessToken = await this.oauth2Client.getToken({});
5✔
38
        }
39
        return this.accessToken?.token.access_token as string | undefined;
82✔
40
    };
41

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

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

58
    constructor(options: ApiClientOptions) {
59
        this.options = {
49✔
60
            ...options,
61
            userAgent:
62
                options.userAgent ||
98✔
63
                `AtlasMCP/${packageInfo.version} (${process.platform}; ${process.arch}; ${process.env.HOSTNAME || "unknown"})`,
98✔
64
        };
65

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

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

92
    public async hasValidAccessToken(): Promise<boolean> {
NEW
93
        const accessToken = await this.getAccessToken();
×
NEW
94
        return accessToken !== undefined;
×
95
    }
96

97
    public async getIpInfo(): Promise<{
98
        currentIpv4Address: string;
99
    }> {
100
        const accessToken = await this.getAccessToken();
2✔
101

102
        const endpoint = "api/private/ipinfo";
2✔
103
        const url = new URL(endpoint, this.options.baseUrl);
2✔
104
        const response = await fetch(url, {
2✔
105
            method: "GET",
106
            headers: {
107
                Accept: "application/json",
108
                Authorization: `Bearer ${accessToken}`,
109
                "User-Agent": this.options.userAgent,
110
            },
111
        });
112

113
        if (!response.ok) {
2!
114
            throw await ApiClientError.fromResponse(response);
×
115
        }
116

117
        return (await response.json()) as Promise<{
2✔
118
            currentIpv4Address: string;
119
        }>;
120
    }
121

122
    async sendEvents(events: TelemetryEvent<CommonProperties>[]): Promise<void> {
123
        const headers: Record<string, string> = {
4✔
124
            Accept: "application/json",
125
            "Content-Type": "application/json",
126
            "User-Agent": this.options.userAgent,
127
        };
128

129
        const accessToken = await this.getAccessToken();
4✔
130
        if (accessToken) {
4✔
131
            const authUrl = new URL("api/private/v1.0/telemetry/events", this.options.baseUrl);
3✔
132
            headers["Authorization"] = `Bearer ${accessToken}`;
3✔
133

134
            try {
3✔
135
                const response = await fetch(authUrl, {
3✔
136
                    method: "POST",
137
                    headers,
138
                    body: JSON.stringify(events),
139
                });
140

141
                if (response.ok) {
3✔
142
                    return;
1✔
143
                }
144

145
                // If anything other than 401, throw the error
146
                if (response.status !== 401) {
2!
NEW
147
                    throw await ApiClientError.fromResponse(response);
×
148
                }
149

150
                // For 401, fall through to unauthenticated endpoint
151
                delete headers["Authorization"];
2✔
152
            } catch (error) {
153
                // If the error is not a 401, rethrow it
NEW
154
                if (!(error instanceof ApiClientError) || error.response.status !== 401) {
×
NEW
155
                    throw error;
×
156
                }
157

158
                // For 401 errors, fall through to unauthenticated endpoint
NEW
159
                delete headers["Authorization"];
×
160
            }
161
        }
162

163
        // Send to unauthenticated endpoint (either as fallback from 401 or direct if no token)
164
        const unauthUrl = new URL("api/private/unauth/telemetry/events", this.options.baseUrl);
3✔
165
        const response = await fetch(unauthUrl, {
3✔
166
            method: "POST",
167
            headers,
168
            body: JSON.stringify(events),
169
        });
170

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

176
    // DO NOT EDIT. This is auto-generated code.
177
    async listClustersForAllProjects(options?: FetchOptions<operations["listClustersForAllProjects"]>) {
178
        const { data, error, response } = await this.client.GET("/api/atlas/v2/clusters", options);
×
179
        if (error) {
×
180
            throw ApiClientError.fromError(response, error);
×
181
        }
182
        return data;
×
183
    }
184

185
    async listProjects(options?: FetchOptions<operations["listProjects"]>) {
186
        const { data, error, response } = await this.client.GET("/api/atlas/v2/groups", options);
4✔
187
        if (error) {
4✔
188
            throw ApiClientError.fromError(response, error);
1✔
189
        }
190
        return data;
3✔
191
    }
192

193
    async createProject(options: FetchOptions<operations["createProject"]>) {
194
        const { data, error, response } = await this.client.POST("/api/atlas/v2/groups", options);
4✔
195
        if (error) {
4!
196
            throw ApiClientError.fromError(response, error);
×
197
        }
198
        return data;
4✔
199
    }
200

201
    async deleteProject(options: FetchOptions<operations["deleteProject"]>) {
202
        const { error, response } = await this.client.DELETE("/api/atlas/v2/groups/{groupId}", options);
4✔
203
        if (error) {
4!
204
            throw ApiClientError.fromError(response, error);
×
205
        }
206
    }
207

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

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

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

232
    async deleteProjectIpAccessList(options: FetchOptions<operations["deleteProjectIpAccessList"]>) {
233
        const { error, response } = await this.client.DELETE(
5✔
234
            "/api/atlas/v2/groups/{groupId}/accessList/{entryValue}",
235
            options
236
        );
237
        if (error) {
5!
238
            throw ApiClientError.fromError(response, error);
×
239
        }
240
    }
241

242
    async listClusters(options: FetchOptions<operations["listClusters"]>) {
243
        const { data, error, response } = await this.client.GET("/api/atlas/v2/groups/{groupId}/clusters", options);
1✔
244
        if (error) {
1!
245
            throw ApiClientError.fromError(response, error);
×
246
        }
247
        return data;
1✔
248
    }
249

250
    async createCluster(options: FetchOptions<operations["createCluster"]>) {
251
        const { data, error, response } = await this.client.POST("/api/atlas/v2/groups/{groupId}/clusters", options);
1✔
252
        if (error) {
1!
253
            throw ApiClientError.fromError(response, error);
×
254
        }
255
        return data;
1✔
256
    }
257

258
    async deleteCluster(options: FetchOptions<operations["deleteCluster"]>) {
259
        const { error, response } = await this.client.DELETE(
1✔
260
            "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}",
261
            options
262
        );
263
        if (error) {
1!
264
            throw ApiClientError.fromError(response, error);
×
265
        }
266
    }
267

268
    async getCluster(options: FetchOptions<operations["getCluster"]>) {
269
        const { data, error, response } = await this.client.GET(
41✔
270
            "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}",
271
            options
272
        );
273

274
        if (error) {
41✔
275
            throw ApiClientError.fromError(response, error);
1✔
276
        }
277
        return data;
40✔
278
    }
279

280
    async listDatabaseUsers(options: FetchOptions<operations["listDatabaseUsers"]>) {
281
        const { data, error, response } = await this.client.GET(
1✔
282
            "/api/atlas/v2/groups/{groupId}/databaseUsers",
283
            options
284
        );
285
        if (error) {
1!
286
            throw ApiClientError.fromError(response, error);
×
287
        }
288
        return data;
1✔
289
    }
290

291
    async createDatabaseUser(options: FetchOptions<operations["createDatabaseUser"]>) {
292
        const { data, error, response } = await this.client.POST(
4✔
293
            "/api/atlas/v2/groups/{groupId}/databaseUsers",
294
            options
295
        );
296
        if (error) {
4!
297
            throw ApiClientError.fromError(response, error);
×
298
        }
299
        return data;
4✔
300
    }
301

302
    async deleteDatabaseUser(options: FetchOptions<operations["deleteDatabaseUser"]>) {
303
        const { error, response } = await this.client.DELETE(
6✔
304
            "/api/atlas/v2/groups/{groupId}/databaseUsers/{databaseName}/{username}",
305
            options
306
        );
307
        if (error) {
6✔
308
            throw ApiClientError.fromError(response, error);
2✔
309
        }
310
    }
311

312
    async listFlexClusters(options: FetchOptions<operations["listFlexClusters"]>) {
313
        const { data, error, response } = await this.client.GET("/api/atlas/v2/groups/{groupId}/flexClusters", options);
×
314
        if (error) {
×
315
            throw ApiClientError.fromError(response, error);
×
316
        }
317
        return data;
×
318
    }
319

320
    async createFlexCluster(options: FetchOptions<operations["createFlexCluster"]>) {
321
        const { data, error, response } = await this.client.POST(
×
322
            "/api/atlas/v2/groups/{groupId}/flexClusters",
323
            options
324
        );
325
        if (error) {
×
326
            throw ApiClientError.fromError(response, error);
×
327
        }
328
        return data;
×
329
    }
330

331
    async deleteFlexCluster(options: FetchOptions<operations["deleteFlexCluster"]>) {
332
        const { error, response } = await this.client.DELETE(
×
333
            "/api/atlas/v2/groups/{groupId}/flexClusters/{name}",
334
            options
335
        );
336
        if (error) {
×
337
            throw ApiClientError.fromError(response, error);
×
338
        }
339
    }
340

341
    async getFlexCluster(options: FetchOptions<operations["getFlexCluster"]>) {
342
        const { data, error, response } = await this.client.GET(
×
343
            "/api/atlas/v2/groups/{groupId}/flexClusters/{name}",
344
            options
345
        );
346
        if (error) {
×
347
            throw ApiClientError.fromError(response, error);
×
348
        }
349
        return data;
×
350
    }
351

352
    async listOrganizations(options?: FetchOptions<operations["listOrganizations"]>) {
353
        const { data, error, response } = await this.client.GET("/api/atlas/v2/orgs", options);
6✔
354
        if (error) {
6!
355
            throw ApiClientError.fromError(response, error);
×
356
        }
357
        return data;
6✔
358
    }
359

360
    async listOrganizationProjects(options: FetchOptions<operations["listOrganizationProjects"]>) {
361
        const { data, error, response } = await this.client.GET("/api/atlas/v2/orgs/{orgId}/groups", options);
×
362
        if (error) {
×
363
            throw ApiClientError.fromError(response, error);
×
364
        }
365
        return data;
×
366
    }
367

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

© 2025 Coveralls, Inc