From e0e4bf703ab0d3e397358696827aa90effdb17e4 Mon Sep 17 00:00:00 2001 From: Filipe C Menezes Date: Thu, 10 Apr 2025 14:28:07 +0100 Subject: [PATCH 01/12] refactor: use openapi-fetch --- package-lock.json | 16 +++ package.json | 1 + scripts/filter.ts | 1 + src/common/atlas/apiClient.ts | 198 +++++++++++++++------------ src/common/atlas/openapi.d.ts | 58 ++++++++ src/tools/atlas/createAccessList.ts | 2 +- src/tools/atlas/inspectAccessList.ts | 2 +- src/tools/atlas/inspectCluster.ts | 6 +- src/tools/atlas/listClusters.ts | 8 +- src/tools/atlas/listDBUsers.ts | 2 +- src/tools/atlas/listProjects.ts | 20 ++- 11 files changed, 207 insertions(+), 107 deletions(-) diff --git a/package-lock.json b/package-lock.json index 83af188c..d309004d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "mongodb-log-writer": "^2.4.1", "mongodb-redact": "^1.1.6", "mongodb-schema": "^12.6.2", + "openapi-fetch": "^0.13.5", "zod": "^3.24.2" }, "bin": { @@ -8620,6 +8621,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/openapi-fetch": { + "version": "0.13.5", + "resolved": "https://registry.npmjs.org/openapi-fetch/-/openapi-fetch-0.13.5.tgz", + "integrity": "sha512-AQK8T9GSKFREFlN1DBXTYsLjs7YV2tZcJ7zUWxbjMoQmj8dDSFRrzhLCbHPZWA1TMV3vACqfCxLEZcwf2wxV6Q==", + "license": "MIT", + "dependencies": { + "openapi-typescript-helpers": "^0.0.15" + } + }, "node_modules/openapi-sampler": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/openapi-sampler/-/openapi-sampler-1.6.1.tgz", @@ -8679,6 +8689,12 @@ "typescript": "^5.x" } }, + "node_modules/openapi-typescript-helpers": { + "version": "0.0.15", + "resolved": "https://registry.npmjs.org/openapi-typescript-helpers/-/openapi-typescript-helpers-0.0.15.tgz", + "integrity": "sha512-opyTPaunsklCBpTK8JGef6mfPhLSnyy5a0IN9vKtx3+4aExf+KxEqYwIy3hqkedXIB97u357uLMJsOnm3GVjsw==", + "license": "MIT" + }, "node_modules/openapi-typescript/node_modules/supports-color": { "version": "9.4.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-9.4.0.tgz", diff --git a/package.json b/package.json index 94a85898..bbfbc74f 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "mongodb-log-writer": "^2.4.1", "mongodb-redact": "^1.1.6", "mongodb-schema": "^12.6.2", + "openapi-fetch": "^0.13.5", "zod": "^3.24.2" }, "engines": { diff --git a/scripts/filter.ts b/scripts/filter.ts index a543229c..7379a2d9 100644 --- a/scripts/filter.ts +++ b/scripts/filter.ts @@ -22,6 +22,7 @@ function filterOpenapi(openapi: OpenAPIV3_1.Document): OpenAPIV3_1.Document { "getProject", "createProject", "listClusters", + "getCluster", "createCluster", "listClustersForAllProjects", "createDatabaseUser", diff --git a/src/common/atlas/apiClient.ts b/src/common/atlas/apiClient.ts index 6d745792..a132f881 100644 --- a/src/common/atlas/apiClient.ts +++ b/src/common/atlas/apiClient.ts @@ -1,15 +1,11 @@ import config from "../../config.js"; +import createClient, { Middleware } from "openapi-fetch"; import { - Group, - PaginatedOrgGroupView, - PaginatedAtlasGroupView, + paths, ClusterDescription20240805, - PaginatedClusterDescription20240805, - PaginatedNetworkAccessView, NetworkPermissionEntry, CloudDatabaseUser, - PaginatedApiAtlasDatabaseUserView, } from "./openapi.js"; export interface OAuthToken { @@ -48,32 +44,40 @@ export interface ApiClientOptions { } export class ApiClient { - token?: OAuthToken; - saveToken?: saveTokenFunction; + private token?: OAuthToken; + private saveToken?: saveTokenFunction; + private client = createClient({ + baseUrl: config.apiBaseURL, + headers: { + "User-Agent": config.userAgent, + Accept: `application/vnd.atlas.${config.atlasApiVersion}+json`, + }, + }); + private authMiddleware = (apiClient: ApiClient): Middleware => ({ + async onRequest({ request, schemaPath }) { + if (schemaPath.startsWith("/api/private/unauth") || schemaPath.startsWith("/api/oauth")) { + return undefined; + } + if (await apiClient.validateToken()) { + request.headers.set("Authorization", `Bearer ${apiClient.token?.access_token}`); + return request; + } + }, + }); + private errorMiddleware = (): Middleware => ({ + async onResponse({ response }) { + if (!response.ok) { + throw new ApiClientError(`Error calling Atlas API: ${await response.text()}`, response); + } + }, + }); constructor(options: ApiClientOptions) { const { token, saveToken } = options; this.token = token; this.saveToken = saveToken; - } - - private defaultOptions(): RequestInit { - const authHeaders = !this.token?.access_token - ? null - : { - Authorization: `Bearer ${this.token.access_token}`, - }; - - return { - method: "GET", - credentials: !this.token?.access_token ? undefined : "include", - headers: { - "Content-Type": "application/json", - Accept: `application/vnd.atlas.${config.atlasApiVersion}+json`, - "User-Agent": config.userAgent, - ...authHeaders, - }, - }; + this.client.use(this.authMiddleware(this)); + this.client.use(this.errorMiddleware()); } async storeToken(token: OAuthToken): Promise { @@ -86,36 +90,6 @@ export class ApiClient { return token; } - async do(endpoint: string, options?: RequestInit): Promise { - if (!this.token || !this.token.access_token) { - throw new Error("Not authenticated. Please run the auth tool first."); - } - - const url = new URL(`api/atlas/v2${endpoint}`, `${config.apiBaseURL}`); - - if (!this.checkTokenExpiry()) { - await this.refreshToken(); - } - - const defaultOpt = this.defaultOptions(); - const opt = { - ...defaultOpt, - ...options, - headers: { - ...defaultOpt.headers, - ...options?.headers, - }, - }; - - const response = await fetch(url, opt); - - if (!response.ok) { - throw new ApiClientError(`Error calling Atlas API: ${await response.text()}`, response); - } - - return (await response.json()) as T; - } - async authenticate(): Promise { const endpoint = "api/private/unauth/account/device/authorize"; @@ -269,58 +243,108 @@ export class ApiClient { } } - async listProjects(): Promise { - return await this.do("/groups"); + async listProjects() { + const { data } = await this.client.GET(`/api/atlas/v2/groups`); + return data; } - async listProjectIpAccessLists(groupId: string): Promise { - return await this.do(`/groups/${groupId}/accessList`); + async listProjectIpAccessLists(groupId: string) { + const { data } = await this.client.GET(`/api/atlas/v2/groups/{groupId}/accessList`, { + params: { + path: { + groupId, + } + } + }); + return data; } async createProjectIpAccessList( groupId: string, - entries: NetworkPermissionEntry[] - ): Promise { - return await this.do(`/groups/${groupId}/accessList`, { - method: "POST", - body: JSON.stringify(entries), + ...entries: NetworkPermissionEntry[] + ) { + const { data } = await this.client.POST(`/api/atlas/v2/groups/{groupId}/accessList`, { + params: { + path: { + groupId, + } + }, + body: entries, }); + return data; } - async getProject(groupId: string): Promise { - return await this.do(`/groups/${groupId}`); + async getProject(groupId: string) { + const { data } = await this.client.GET(`/api/atlas/v2/groups/{groupId}`, { + params: { + path: { + groupId, + } + } + }); + return data; } - async listClusters(groupId: string): Promise { - return await this.do(`/groups/${groupId}/clusters`); + async listClusters(groupId: string) { + const { data } = await this.client.GET(`/api/atlas/v2/groups/{groupId}/clusters`, { + params: { + path: { + groupId, + } + } + }); + return data; } - async listClustersForAllProjects(): Promise { - return await this.do(`/clusters`); + async listClustersForAllProjects() { + const { data } = await this.client.GET(`/api/atlas/v2/clusters`); + return data; } - async getCluster(groupId: string, clusterName: string): Promise { - return await this.do(`/groups/${groupId}/clusters/${clusterName}`); + async getCluster(groupId: string, clusterName: string) { + const { data } = await this.client.GET(`/api/atlas/v2/groups/{groupId}/clusters/{clusterName}`, { + params: { + path: { + groupId, + clusterName, + } + } + }); + return data; } - async createCluster(groupId: string, cluster: ClusterDescription20240805): Promise { - if (!cluster.groupId) { - throw new Error("Cluster groupId is required"); - } - return await this.do(`/groups/${groupId}/clusters`, { - method: "POST", - body: JSON.stringify(cluster), + async createCluster(groupId: string, cluster: ClusterDescription20240805) { + const { data } = await this.client.POST('/api/atlas/v2/groups/{groupId}/clusters', { + params: { + path: { + groupId, + } + }, + body: cluster, }); + return data; } - async createDatabaseUser(groupId: string, user: CloudDatabaseUser): Promise { - return await this.do(`/groups/${groupId}/databaseUsers`, { - method: "POST", - body: JSON.stringify(user), + async createDatabaseUser(groupId: string, user: CloudDatabaseUser) { + const { data } = await this.client.POST('/api/atlas/v2/groups/{groupId}/databaseUsers', { + params: { + path: { + groupId, + } + }, + body: user, }); + return data; } - async listDatabaseUsers(groupId: string): Promise { - return await this.do(`/groups/${groupId}/databaseUsers`); + async listDatabaseUsers(groupId: string) { + const { data } = await this.client.GET(`/api/atlas/v2/groups/{groupId}/databaseUsers`, { + params: { + path: { + groupId, + } + } + }); + return data; } } diff --git a/src/common/atlas/openapi.d.ts b/src/common/atlas/openapi.d.ts index fc9ee873..8ff7878c 100644 --- a/src/common/atlas/openapi.d.ts +++ b/src/common/atlas/openapi.d.ts @@ -120,6 +120,28 @@ export interface paths { patch?: never; trace?: never; }; + "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Return One Cluster from One Project + * @description Returns the details for one cluster in the specified project. Clusters contain a group of hosts that maintain the same data set. The response includes clusters with asymmetrically-sized shards. To use this resource, the requesting Service Account or API Key must have the Project Read Only role. This feature is not available for serverless clusters. + * + * This endpoint can also be used on Flex clusters that were created using the [createCluster](https://www.mongodb.com/docs/atlas/reference/api-resources-spec/v2/#tag/Clusters/operation/createCluster) endpoint or former M2/M5 clusters that have been migrated to Flex clusters until January 2026. Please use the getFlexCluster endpoint for Flex clusters instead. Deprecated versions: v2-{2023-02-01}, v2-{2023-01-01} + */ + get: operations["getCluster"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/atlas/v2/groups/{groupId}/databaseUsers": { parameters: { query?: never; @@ -7467,6 +7489,42 @@ export interface operations { 500: components["responses"]["internalServerError"]; }; }; + getCluster: { + parameters: { + query?: { + /** @description Flag that indicates whether Application wraps the response in an `envelope` JSON object. Some API clients cannot access the HTTP response headers or status code. To remediate this, set envelope=true in the query. Endpoints that return a list of results use the results object as an envelope. Application adds the status parameter to the response body. */ + envelope?: components["parameters"]["envelope"]; + /** @description Flag that indicates whether the response body should be in the prettyprint format. */ + pretty?: components["parameters"]["pretty"]; + }; + header?: never; + path: { + /** @description Unique 24-hexadecimal digit string that identifies your project. Use the [/groups](#tag/Projects/operation/listProjects) endpoint to retrieve all projects to which the authenticated user has access. + * + * **NOTE**: Groups and projects are synonymous terms. Your group id is the same as your project id. For existing groups, your group/project id remains the same. The resource and corresponding endpoints use the term groups. */ + groupId: components["parameters"]["groupId"]; + /** @description Human-readable label that identifies this cluster. */ + clusterName: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/vnd.atlas.2024-08-05+json": components["schemas"]["ClusterDescription20240805"]; + }; + }; + 401: components["responses"]["unauthorized"]; + 404: components["responses"]["notFound"]; + 409: components["responses"]["conflict"]; + 500: components["responses"]["internalServerError"]; + }; + }; listDatabaseUsers: { parameters: { query?: { diff --git a/src/tools/atlas/createAccessList.ts b/src/tools/atlas/createAccessList.ts index 0a6afeae..b56a98ac 100644 --- a/src/tools/atlas/createAccessList.ts +++ b/src/tools/atlas/createAccessList.ts @@ -44,7 +44,7 @@ export class CreateAccessListTool extends AtlasToolBase { const inputs = [...ipInputs, ...cidrInputs]; - await this.apiClient.createProjectIpAccessList(projectId, inputs); + await this.apiClient.createProjectIpAccessList(projectId, ...inputs); return { content: [ diff --git a/src/tools/atlas/inspectAccessList.ts b/src/tools/atlas/inspectAccessList.ts index 79fea3b0..9df8711a 100644 --- a/src/tools/atlas/inspectAccessList.ts +++ b/src/tools/atlas/inspectAccessList.ts @@ -15,7 +15,7 @@ export class InspectAccessListTool extends AtlasToolBase { const accessList = await this.apiClient.listProjectIpAccessLists(projectId); - if (!accessList.results?.length) { + if (!accessList?.results?.length) { throw new Error("No access list entries found."); } diff --git a/src/tools/atlas/inspectCluster.ts b/src/tools/atlas/inspectCluster.ts index 3a074db4..1c700162 100644 --- a/src/tools/atlas/inspectCluster.ts +++ b/src/tools/atlas/inspectCluster.ts @@ -20,7 +20,11 @@ export class InspectClusterTool extends AtlasToolBase { return this.formatOutput(cluster); } - private formatOutput(cluster: ClusterDescription20240805): CallToolResult { + private formatOutput(cluster?: ClusterDescription20240805): CallToolResult { + if (!cluster) { + throw new Error("Cluster not found"); + } + return { content: [ { diff --git a/src/tools/atlas/listClusters.ts b/src/tools/atlas/listClusters.ts index 2be4cea4..df6356c6 100644 --- a/src/tools/atlas/listClusters.ts +++ b/src/tools/atlas/listClusters.ts @@ -31,8 +31,8 @@ export class ListClustersTool extends AtlasToolBase { } } - private formatAllClustersTable(clusters: PaginatedOrgGroupView): CallToolResult { - if (!clusters.results?.length) { + private formatAllClustersTable(clusters?: PaginatedOrgGroupView): CallToolResult { + if (!clusters?.results?.length) { throw new Error("No clusters found."); } const rows = clusters @@ -59,8 +59,8 @@ ${rows}`, }; } - private formatClustersTable(project: Group, clusters: PaginatedClusterDescription20240805): CallToolResult { - if (!clusters.results?.length) { + private formatClustersTable(project: Group, clusters?: PaginatedClusterDescription20240805): CallToolResult { + if (!clusters?.results?.length) { throw new Error("No clusters found."); } const rows = clusters diff --git a/src/tools/atlas/listDBUsers.ts b/src/tools/atlas/listDBUsers.ts index 95677ea7..5ec11bc0 100644 --- a/src/tools/atlas/listDBUsers.ts +++ b/src/tools/atlas/listDBUsers.ts @@ -16,7 +16,7 @@ export class ListDBUsersTool extends AtlasToolBase { const data = await this.apiClient!.listDatabaseUsers(projectId); - if (!data.results?.length) { + if (!data?.results?.length) { throw new Error("No database users found."); } diff --git a/src/tools/atlas/listProjects.ts b/src/tools/atlas/listProjects.ts index b117fdd7..3999aa1a 100644 --- a/src/tools/atlas/listProjects.ts +++ b/src/tools/atlas/listProjects.ts @@ -9,29 +9,25 @@ export class ListProjectsTool extends AtlasToolBase { protected async execute(): Promise { await this.ensureAuthenticated(); - const projectsData = await this.apiClient!.listProjects(); - const projects = projectsData.results || []; + const data = await this.apiClient!.listProjects(); - if (projects.length === 0) { - return { - content: [{ type: "text", text: "No projects found in your MongoDB Atlas account." }], - }; + if (data?.results?.length) { + throw new Error("No projects found in your MongoDB Atlas account.") } // Format projects as a table - const header = `Project Name | Project ID | Created At -----------------|----------------|----------------`; - const rows = projects + const rows = data!.results! .map((project) => { - const created = project.created as any as { $date: string }; // eslint-disable-line @typescript-eslint/no-explicit-any + const created = project.created as unknown as { $date: string }; const createdAt = created ? new Date(created.$date).toLocaleString() : "N/A"; return `${project.name} | ${project.id} | ${createdAt}`; }) .join("\n"); - const formattedProjects = `${header}\n${rows}`; + const formattedProjects = `Project Name | Project ID | Created At +----------------| ----------------| ---------------- +${rows}`; return { content: [ - { type: "text", text: "Here are your MongoDB Atlas projects:" }, { type: "text", text: formattedProjects }, ], }; From b91a4560847c4d2eed33226a77b09c5e4358e8be Mon Sep 17 00:00:00 2001 From: Filipe C Menezes Date: Thu, 10 Apr 2025 14:31:14 +0100 Subject: [PATCH 02/12] fix: styles --- src/common/atlas/apiClient.ts | 42 +++++++++++++------------------ src/tools/atlas/inspectCluster.ts | 2 +- src/tools/atlas/listProjects.ts | 10 +++----- 3 files changed, 22 insertions(+), 32 deletions(-) diff --git a/src/common/atlas/apiClient.ts b/src/common/atlas/apiClient.ts index 6d085395..9551efb9 100644 --- a/src/common/atlas/apiClient.ts +++ b/src/common/atlas/apiClient.ts @@ -1,12 +1,7 @@ import config from "../../config.js"; import createClient, { Middleware } from "openapi-fetch"; -import { - paths, - ClusterDescription20240805, - NetworkPermissionEntry, - CloudDatabaseUser, -} from "./openapi.js"; +import { paths, ClusterDescription20240805, NetworkPermissionEntry, CloudDatabaseUser } from "./openapi.js"; export interface OAuthToken { access_token: string; @@ -253,21 +248,18 @@ export class ApiClient { params: { path: { groupId, - } - } + }, + }, }); return data; } - async createProjectIpAccessList( - groupId: string, - ...entries: NetworkPermissionEntry[] - ) { + async createProjectIpAccessList(groupId: string, ...entries: NetworkPermissionEntry[]) { const { data } = await this.client.POST(`/api/atlas/v2/groups/{groupId}/accessList`, { params: { path: { groupId, - } + }, }, body: entries, }); @@ -279,8 +271,8 @@ export class ApiClient { params: { path: { groupId, - } - } + }, + }, }); return data; } @@ -290,8 +282,8 @@ export class ApiClient { params: { path: { groupId, - } - } + }, + }, }); return data; } @@ -307,18 +299,18 @@ export class ApiClient { path: { groupId, clusterName, - } - } + }, + }, }); return data; } async createCluster(groupId: string, cluster: ClusterDescription20240805) { - const { data } = await this.client.POST('/api/atlas/v2/groups/{groupId}/clusters', { + const { data } = await this.client.POST("/api/atlas/v2/groups/{groupId}/clusters", { params: { path: { groupId, - } + }, }, body: cluster, }); @@ -326,11 +318,11 @@ export class ApiClient { } async createDatabaseUser(groupId: string, user: CloudDatabaseUser) { - const { data } = await this.client.POST('/api/atlas/v2/groups/{groupId}/databaseUsers', { + const { data } = await this.client.POST("/api/atlas/v2/groups/{groupId}/databaseUsers", { params: { path: { groupId, - } + }, }, body: user, }); @@ -342,8 +334,8 @@ export class ApiClient { params: { path: { groupId, - } - } + }, + }, }); return data; } diff --git a/src/tools/atlas/inspectCluster.ts b/src/tools/atlas/inspectCluster.ts index 1c700162..afce8d0c 100644 --- a/src/tools/atlas/inspectCluster.ts +++ b/src/tools/atlas/inspectCluster.ts @@ -24,7 +24,7 @@ export class InspectClusterTool extends AtlasToolBase { if (!cluster) { throw new Error("Cluster not found"); } - + return { content: [ { diff --git a/src/tools/atlas/listProjects.ts b/src/tools/atlas/listProjects.ts index 3999aa1a..48915fe9 100644 --- a/src/tools/atlas/listProjects.ts +++ b/src/tools/atlas/listProjects.ts @@ -12,12 +12,12 @@ export class ListProjectsTool extends AtlasToolBase { const data = await this.apiClient!.listProjects(); if (data?.results?.length) { - throw new Error("No projects found in your MongoDB Atlas account.") + throw new Error("No projects found in your MongoDB Atlas account."); } // Format projects as a table - const rows = data!.results! - .map((project) => { + const rows = data! + .results!.map((project) => { const created = project.created as unknown as { $date: string }; const createdAt = created ? new Date(created.$date).toLocaleString() : "N/A"; return `${project.name} | ${project.id} | ${createdAt}`; @@ -27,9 +27,7 @@ export class ListProjectsTool extends AtlasToolBase { ----------------| ----------------| ---------------- ${rows}`; return { - content: [ - { type: "text", text: formattedProjects }, - ], + content: [{ type: "text", text: formattedProjects }], }; } } From 7d64982c49a5f48a7c55569e00d64c0d2eb7e3a5 Mon Sep 17 00:00:00 2001 From: Filipe C Menezes Date: Thu, 10 Apr 2025 15:06:28 +0100 Subject: [PATCH 03/12] fix: parameters --- src/common/atlas/apiClient.ts | 96 +++++++--------------------- src/tools/atlas/createAccessList.ts | 9 ++- src/tools/atlas/createDBUser.ts | 9 ++- src/tools/atlas/createFreeCluster.ts | 9 ++- src/tools/atlas/inspectAccessList.ts | 19 ++++-- src/tools/atlas/inspectCluster.ts | 9 ++- src/tools/atlas/listClusters.ts | 16 ++++- src/tools/atlas/listDBUsers.ts | 8 ++- 8 files changed, 87 insertions(+), 88 deletions(-) diff --git a/src/common/atlas/apiClient.ts b/src/common/atlas/apiClient.ts index 9551efb9..33136703 100644 --- a/src/common/atlas/apiClient.ts +++ b/src/common/atlas/apiClient.ts @@ -1,7 +1,7 @@ import config from "../../config.js"; -import createClient, { Middleware } from "openapi-fetch"; +import createClient, { FetchOptions, Middleware } from "openapi-fetch"; -import { paths, ClusterDescription20240805, NetworkPermissionEntry, CloudDatabaseUser } from "./openapi.js"; +import { paths, operations } from "./openapi.js"; export interface OAuthToken { access_token: string; @@ -238,105 +238,53 @@ export class ApiClient { } } - async listProjects() { - const { data } = await this.client.GET(`/api/atlas/v2/groups`); + async listProjects(options?: FetchOptions) { + const { data } = await this.client.GET(`/api/atlas/v2/groups`, options); return data; } - async listProjectIpAccessLists(groupId: string) { - const { data } = await this.client.GET(`/api/atlas/v2/groups/{groupId}/accessList`, { - params: { - path: { - groupId, - }, - }, - }); + async listProjectIpAccessLists(options: FetchOptions) { + const { data } = await this.client.GET(`/api/atlas/v2/groups/{groupId}/accessList`, options); return data; } - async createProjectIpAccessList(groupId: string, ...entries: NetworkPermissionEntry[]) { - const { data } = await this.client.POST(`/api/atlas/v2/groups/{groupId}/accessList`, { - params: { - path: { - groupId, - }, - }, - body: entries, - }); + async createProjectIpAccessList(options: FetchOptions) { + const { data } = await this.client.POST(`/api/atlas/v2/groups/{groupId}/accessList`, options); return data; } - async getProject(groupId: string) { - const { data } = await this.client.GET(`/api/atlas/v2/groups/{groupId}`, { - params: { - path: { - groupId, - }, - }, - }); + async getProject(options: FetchOptions) { + const { data } = await this.client.GET(`/api/atlas/v2/groups/{groupId}`, options); return data; } - async listClusters(groupId: string) { - const { data } = await this.client.GET(`/api/atlas/v2/groups/{groupId}/clusters`, { - params: { - path: { - groupId, - }, - }, - }); + async listClusters(options: FetchOptions) { + const { data } = await this.client.GET(`/api/atlas/v2/groups/{groupId}/clusters`, options); return data; } - async listClustersForAllProjects() { - const { data } = await this.client.GET(`/api/atlas/v2/clusters`); + async listClustersForAllProjects(options?: FetchOptions) { + const { data } = await this.client.GET(`/api/atlas/v2/clusters`, options); return data; } - async getCluster(groupId: string, clusterName: string) { - const { data } = await this.client.GET(`/api/atlas/v2/groups/{groupId}/clusters/{clusterName}`, { - params: { - path: { - groupId, - clusterName, - }, - }, - }); + async getCluster(options: FetchOptions) { + const { data } = await this.client.GET(`/api/atlas/v2/groups/{groupId}/clusters/{clusterName}`, options); return data; } - async createCluster(groupId: string, cluster: ClusterDescription20240805) { - const { data } = await this.client.POST("/api/atlas/v2/groups/{groupId}/clusters", { - params: { - path: { - groupId, - }, - }, - body: cluster, - }); + async createCluster(options: FetchOptions) { + const { data } = await this.client.POST("/api/atlas/v2/groups/{groupId}/clusters", options); return data; } - async createDatabaseUser(groupId: string, user: CloudDatabaseUser) { - const { data } = await this.client.POST("/api/atlas/v2/groups/{groupId}/databaseUsers", { - params: { - path: { - groupId, - }, - }, - body: user, - }); + async createDatabaseUser(options: FetchOptions) { + const { data } = await this.client.POST("/api/atlas/v2/groups/{groupId}/databaseUsers", options); return data; } - async listDatabaseUsers(groupId: string) { - const { data } = await this.client.GET(`/api/atlas/v2/groups/{groupId}/databaseUsers`, { - params: { - path: { - groupId, - }, - }, - }); + async listDatabaseUsers(options: FetchOptions) { + const { data } = await this.client.GET(`/api/atlas/v2/groups/{groupId}/databaseUsers`, options); return data; } } diff --git a/src/tools/atlas/createAccessList.ts b/src/tools/atlas/createAccessList.ts index b56a98ac..fad14a16 100644 --- a/src/tools/atlas/createAccessList.ts +++ b/src/tools/atlas/createAccessList.ts @@ -44,7 +44,14 @@ export class CreateAccessListTool extends AtlasToolBase { const inputs = [...ipInputs, ...cidrInputs]; - await this.apiClient.createProjectIpAccessList(projectId, ...inputs); + await this.apiClient.createProjectIpAccessList({ + params: { + path: { + groupId: projectId, + }, + }, + body: inputs, + }); return { content: [ diff --git a/src/tools/atlas/createDBUser.ts b/src/tools/atlas/createDBUser.ts index c3b186ca..cf61dcb3 100644 --- a/src/tools/atlas/createDBUser.ts +++ b/src/tools/atlas/createDBUser.ts @@ -53,7 +53,14 @@ export class CreateDBUserTool extends AtlasToolBase { : undefined, } as CloudDatabaseUser; - await this.apiClient!.createDatabaseUser(projectId, input); + await this.apiClient!.createDatabaseUser({ + params: { + path: { + groupId: projectId, + }, + }, + body: input + }); return { content: [{ type: "text", text: `User "${username}" created sucessfully.` }], diff --git a/src/tools/atlas/createFreeCluster.ts b/src/tools/atlas/createFreeCluster.ts index 34e207da..6a903f48 100644 --- a/src/tools/atlas/createFreeCluster.ts +++ b/src/tools/atlas/createFreeCluster.ts @@ -38,7 +38,14 @@ export class CreateFreeClusterTool extends AtlasToolBase { terminationProtectionEnabled: false, } as unknown as ClusterDescription20240805; - await this.apiClient.createCluster(projectId, input); + await this.apiClient.createCluster({ + params: { + path: { + groupId: projectId, + }, + }, + body: input, + }); return { content: [{ type: "text", text: `Cluster "${name}" has been created in region "${region}".` }], diff --git a/src/tools/atlas/inspectAccessList.ts b/src/tools/atlas/inspectAccessList.ts index 9df8711a..08468c9a 100644 --- a/src/tools/atlas/inspectAccessList.ts +++ b/src/tools/atlas/inspectAccessList.ts @@ -13,7 +13,13 @@ export class InspectAccessListTool extends AtlasToolBase { protected async execute({ projectId }: ToolArgs): Promise { await this.ensureAuthenticated(); - const accessList = await this.apiClient.listProjectIpAccessLists(projectId); + const accessList = await this.apiClient.listProjectIpAccessLists({ + params: { + path: { + groupId: projectId, + }, + }, + }); if (!accessList?.results?.length) { throw new Error("No access list entries found."); @@ -26,12 +32,11 @@ export class InspectAccessListTool extends AtlasToolBase { text: `IP ADDRESS | CIDR | COMMENT ------|------|------ -` + - (accessList.results || []) - .map((entry) => { - return `${entry.ipAddress} | ${entry.cidrBlock} | ${entry.comment}`; - }) - .join("\n"), +${(accessList.results || []) + .map((entry) => { + return `${entry.ipAddress} | ${entry.cidrBlock} | ${entry.comment}`; + }) + .join("\n")}`, }, ], }; diff --git a/src/tools/atlas/inspectCluster.ts b/src/tools/atlas/inspectCluster.ts index afce8d0c..c53f850f 100644 --- a/src/tools/atlas/inspectCluster.ts +++ b/src/tools/atlas/inspectCluster.ts @@ -15,7 +15,14 @@ export class InspectClusterTool extends AtlasToolBase { protected async execute({ projectId, clusterName }: ToolArgs): Promise { await this.ensureAuthenticated(); - const cluster = await this.apiClient.getCluster(projectId, clusterName); + const cluster = await this.apiClient.getCluster({ + params: { + path: { + groupId: projectId, + clusterName, + }, + }, + }); return this.formatOutput(cluster); } diff --git a/src/tools/atlas/listClusters.ts b/src/tools/atlas/listClusters.ts index df6356c6..eda4d420 100644 --- a/src/tools/atlas/listClusters.ts +++ b/src/tools/atlas/listClusters.ts @@ -19,13 +19,25 @@ export class ListClustersTool extends AtlasToolBase { return this.formatAllClustersTable(data); } else { - const project = await this.apiClient.getProject(projectId); + const project = await this.apiClient.getProject({ + params: { + path: { + groupId: projectId, + }, + }, + }); if (!project?.id) { throw new Error(`Project with ID "${projectId}" not found.`); } - const data = await this.apiClient.listClusters(project.id || ""); + const data = await this.apiClient.listClusters({ + params: { + path: { + groupId: project.id || "", + }, + }, + }); return this.formatClustersTable(project, data); } diff --git a/src/tools/atlas/listDBUsers.ts b/src/tools/atlas/listDBUsers.ts index 5ec11bc0..d49d981b 100644 --- a/src/tools/atlas/listDBUsers.ts +++ b/src/tools/atlas/listDBUsers.ts @@ -14,7 +14,13 @@ export class ListDBUsersTool extends AtlasToolBase { protected async execute({ projectId }: ToolArgs): Promise { await this.ensureAuthenticated(); - const data = await this.apiClient!.listDatabaseUsers(projectId); + const data = await this.apiClient!.listDatabaseUsers({ + params: { + path: { + groupId: projectId, + }, + }, + }); if (!data?.results?.length) { throw new Error("No database users found."); From 99074f8508350b2f142446e9da6204dc6b9b79ac Mon Sep 17 00:00:00 2001 From: Filipe C Menezes Date: Thu, 10 Apr 2025 15:08:43 +0100 Subject: [PATCH 04/12] fix: response text --- src/common/atlas/apiClient.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/common/atlas/apiClient.ts b/src/common/atlas/apiClient.ts index 33136703..133a166c 100644 --- a/src/common/atlas/apiClient.ts +++ b/src/common/atlas/apiClient.ts @@ -62,7 +62,13 @@ export class ApiClient { private errorMiddleware = (): Middleware => ({ async onResponse({ response }) { if (!response.ok) { - throw new ApiClientError(`Error calling Atlas API: ${await response.text()}`, response); + try { + const text = await response.text(); + throw new ApiClientError(`Error calling Atlas API: [${response.statusText}] ${text}`, response); + } + catch { + throw new ApiClientError(`Error calling Atlas API: ${response.statusText}`, response); + } } }, }); From c4a1f75512ccda1a1be47be13c22482145221418 Mon Sep 17 00:00:00 2001 From: Filipe C Menezes Date: Thu, 10 Apr 2025 15:09:10 +0100 Subject: [PATCH 05/12] fix: styles --- src/common/atlas/apiClient.ts | 3 +-- src/tools/atlas/createDBUser.ts | 2 +- src/tools/atlas/inspectAccessList.ts | 3 +-- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/common/atlas/apiClient.ts b/src/common/atlas/apiClient.ts index 133a166c..ad48c0a2 100644 --- a/src/common/atlas/apiClient.ts +++ b/src/common/atlas/apiClient.ts @@ -65,8 +65,7 @@ export class ApiClient { try { const text = await response.text(); throw new ApiClientError(`Error calling Atlas API: [${response.statusText}] ${text}`, response); - } - catch { + } catch { throw new ApiClientError(`Error calling Atlas API: ${response.statusText}`, response); } } diff --git a/src/tools/atlas/createDBUser.ts b/src/tools/atlas/createDBUser.ts index cf61dcb3..d2a3b5d6 100644 --- a/src/tools/atlas/createDBUser.ts +++ b/src/tools/atlas/createDBUser.ts @@ -59,7 +59,7 @@ export class CreateDBUserTool extends AtlasToolBase { groupId: projectId, }, }, - body: input + body: input, }); return { diff --git a/src/tools/atlas/inspectAccessList.ts b/src/tools/atlas/inspectAccessList.ts index 08468c9a..6e0c2f07 100644 --- a/src/tools/atlas/inspectAccessList.ts +++ b/src/tools/atlas/inspectAccessList.ts @@ -29,8 +29,7 @@ export class InspectAccessListTool extends AtlasToolBase { content: [ { type: "text", - text: - `IP ADDRESS | CIDR | COMMENT + text: `IP ADDRESS | CIDR | COMMENT ------|------|------ ${(accessList.results || []) .map((entry) => { From ad7a657edb3d801e03a1dfb1da3face4ba9838ed Mon Sep 17 00:00:00 2001 From: Filipe C Menezes Date: Thu, 10 Apr 2025 15:09:49 +0100 Subject: [PATCH 06/12] fix: text --- src/common/atlas/apiClient.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/common/atlas/apiClient.ts b/src/common/atlas/apiClient.ts index ad48c0a2..b0d00ddf 100644 --- a/src/common/atlas/apiClient.ts +++ b/src/common/atlas/apiClient.ts @@ -64,9 +64,9 @@ export class ApiClient { if (!response.ok) { try { const text = await response.text(); - throw new ApiClientError(`Error calling Atlas API: [${response.statusText}] ${text}`, response); + throw new ApiClientError(`Error calling Atlas API: [${response.status} ${response.statusText}] ${text}`, response); } catch { - throw new ApiClientError(`Error calling Atlas API: ${response.statusText}`, response); + throw new ApiClientError(`Error calling Atlas API: ${response.status} ${response.statusText}`, response); } } }, From c2f449f6031b7aeef48c422b8c04fff5354aa1fc Mon Sep 17 00:00:00 2001 From: Filipe C Menezes Date: Thu, 10 Apr 2025 15:14:36 +0100 Subject: [PATCH 07/12] fix: projects --- src/tools/atlas/listProjects.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/tools/atlas/listProjects.ts b/src/tools/atlas/listProjects.ts index 48915fe9..6b4b7d4a 100644 --- a/src/tools/atlas/listProjects.ts +++ b/src/tools/atlas/listProjects.ts @@ -11,15 +11,14 @@ export class ListProjectsTool extends AtlasToolBase { const data = await this.apiClient!.listProjects(); - if (data?.results?.length) { + if (!data?.results?.length) { throw new Error("No projects found in your MongoDB Atlas account."); } // Format projects as a table const rows = data! .results!.map((project) => { - const created = project.created as unknown as { $date: string }; - const createdAt = created ? new Date(created.$date).toLocaleString() : "N/A"; + const createdAt = project.created ? new Date(project.created).toLocaleString() : "N/A"; return `${project.name} | ${project.id} | ${createdAt}`; }) .join("\n"); From e1f7fb9555475ca2b74125f61383c106f17b161a Mon Sep 17 00:00:00 2001 From: Filipe C Menezes Date: Thu, 10 Apr 2025 15:15:31 +0100 Subject: [PATCH 08/12] fix: styles --- src/common/atlas/apiClient.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/common/atlas/apiClient.ts b/src/common/atlas/apiClient.ts index b0d00ddf..070537ba 100644 --- a/src/common/atlas/apiClient.ts +++ b/src/common/atlas/apiClient.ts @@ -64,9 +64,15 @@ export class ApiClient { if (!response.ok) { try { const text = await response.text(); - throw new ApiClientError(`Error calling Atlas API: [${response.status} ${response.statusText}] ${text}`, response); + throw new ApiClientError( + `Error calling Atlas API: [${response.status} ${response.statusText}] ${text}`, + response + ); } catch { - throw new ApiClientError(`Error calling Atlas API: ${response.status} ${response.statusText}`, response); + throw new ApiClientError( + `Error calling Atlas API: ${response.status} ${response.statusText}`, + response + ); } } }, From d75f3c618916423c4a16dcc69d59a6018ead3c1b Mon Sep 17 00:00:00 2001 From: Filipe C Menezes Date: Thu, 10 Apr 2025 16:27:50 +0100 Subject: [PATCH 09/12] chore: add fromResponse --- src/common/atlas/apiClient.ts | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/common/atlas/apiClient.ts b/src/common/atlas/apiClient.ts index 070537ba..4d33bede 100644 --- a/src/common/atlas/apiClient.ts +++ b/src/common/atlas/apiClient.ts @@ -31,6 +31,15 @@ export class ApiClientError extends Error { this.name = "ApiClientError"; this.response = response; } + + static async fromResponse(response: Response): Promise { + try { + const text = await response.text(); + return new ApiClientError(`Error calling Atlas API: [${response.status} ${response.statusText}] ${text}`, response); + } catch { + return new ApiClientError(`Error calling Atlas API: ${response.status} ${response.statusText}`, response); + } + } } export interface ApiClientOptions { @@ -62,18 +71,7 @@ export class ApiClient { private errorMiddleware = (): Middleware => ({ async onResponse({ response }) { if (!response.ok) { - try { - const text = await response.text(); - throw new ApiClientError( - `Error calling Atlas API: [${response.status} ${response.statusText}] ${text}`, - response - ); - } catch { - throw new ApiClientError( - `Error calling Atlas API: ${response.status} ${response.statusText}`, - response - ); - } + throw await ApiClientError.fromResponse(response); } }, }); From 878408ed9c31cfce456822dc3c6feb9f0e6893ed Mon Sep 17 00:00:00 2001 From: Filipe C Menezes Date: Thu, 10 Apr 2025 16:30:35 +0100 Subject: [PATCH 10/12] fix: styles --- src/common/atlas/apiClient.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/common/atlas/apiClient.ts b/src/common/atlas/apiClient.ts index 4d33bede..7d3734ff 100644 --- a/src/common/atlas/apiClient.ts +++ b/src/common/atlas/apiClient.ts @@ -35,7 +35,10 @@ export class ApiClientError extends Error { static async fromResponse(response: Response): Promise { try { const text = await response.text(); - return new ApiClientError(`Error calling Atlas API: [${response.status} ${response.statusText}] ${text}`, response); + return new ApiClientError( + `Error calling Atlas API: [${response.status} ${response.statusText}] ${text}`, + response + ); } catch { return new ApiClientError(`Error calling Atlas API: ${response.status} ${response.statusText}`, response); } From fb6889fd59f78a7aa4723348b91f90304b323b25 Mon Sep 17 00:00:00 2001 From: Filipe C Menezes Date: Thu, 10 Apr 2025 16:36:57 +0100 Subject: [PATCH 11/12] fix: error handling --- src/common/atlas/apiClient.ts | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/common/atlas/apiClient.ts b/src/common/atlas/apiClient.ts index 7d3734ff..531186a7 100644 --- a/src/common/atlas/apiClient.ts +++ b/src/common/atlas/apiClient.ts @@ -32,15 +32,16 @@ export class ApiClientError extends Error { this.response = response; } - static async fromResponse(response: Response): Promise { + static async fromResponse(response: Response, message?: string): Promise { + message ||= `error calling Atlas API`; try { const text = await response.text(); return new ApiClientError( - `Error calling Atlas API: [${response.status} ${response.statusText}] ${text}`, + `${message}: [${response.status} ${response.statusText}] ${text}`, response ); } catch { - return new ApiClientError(`Error calling Atlas API: ${response.status} ${response.statusText}`, response); + return new ApiClientError(`${message}: ${response.status} ${response.statusText}`, response); } } } @@ -116,7 +117,7 @@ export class ApiClient { }); if (!response.ok) { - throw new ApiClientError(`Failed to initiate authentication: ${response.statusText}`, response); + throw await ApiClientError.fromResponse(response, `failed to initiate authentication`); } return (await response.json()) as OauthDeviceCode; @@ -147,14 +148,12 @@ export class ApiClient { try { const errorResponse = await response.json(); if (errorResponse.errorCode === "DEVICE_AUTHORIZATION_PENDING") { - throw new ApiClientError("Authentication pending. Try again later.", response); - } else if (errorResponse.error === "expired_token") { - throw new ApiClientError("Device code expired. Please restart the authentication process.", response); + throw await ApiClientError.fromResponse(response, "Authentication pending. Try again later."); } else { - throw new ApiClientError("Device code expired. Please restart the authentication process.", response); + throw await ApiClientError.fromResponse(response, "Device code expired. Please restart the authentication process."); } } catch { - throw new ApiClientError("Failed to retrieve token. Please check your device code.", response); + throw await ApiClientError.fromResponse(response, "Failed to retrieve token. Please check your device code."); } } @@ -176,7 +175,7 @@ export class ApiClient { }); if (!response.ok) { - throw new ApiClientError(`Failed to refresh token: ${response.statusText}`, response); + throw await ApiClientError.fromResponse(response, "Failed to refresh token"); } const data = await response.json(); @@ -210,7 +209,7 @@ export class ApiClient { }); if (!response.ok) { - throw new ApiClientError(`Failed to revoke token: ${response.statusText}`, response); + throw await ApiClientError.fromResponse(response); } if (!token && this.token) { From 17f2b7062fcf52a6548b8b02148acce3f96a351e Mon Sep 17 00:00:00 2001 From: Filipe C Menezes Date: Thu, 10 Apr 2025 16:37:18 +0100 Subject: [PATCH 12/12] fix: styles --- src/common/atlas/apiClient.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/common/atlas/apiClient.ts b/src/common/atlas/apiClient.ts index 531186a7..e87e7048 100644 --- a/src/common/atlas/apiClient.ts +++ b/src/common/atlas/apiClient.ts @@ -36,10 +36,7 @@ export class ApiClientError extends Error { message ||= `error calling Atlas API`; try { const text = await response.text(); - return new ApiClientError( - `${message}: [${response.status} ${response.statusText}] ${text}`, - response - ); + return new ApiClientError(`${message}: [${response.status} ${response.statusText}] ${text}`, response); } catch { return new ApiClientError(`${message}: ${response.status} ${response.statusText}`, response); } @@ -150,10 +147,16 @@ export class ApiClient { if (errorResponse.errorCode === "DEVICE_AUTHORIZATION_PENDING") { throw await ApiClientError.fromResponse(response, "Authentication pending. Try again later."); } else { - throw await ApiClientError.fromResponse(response, "Device code expired. Please restart the authentication process."); + throw await ApiClientError.fromResponse( + response, + "Device code expired. Please restart the authentication process." + ); } } catch { - throw await ApiClientError.fromResponse(response, "Failed to retrieve token. Please check your device code."); + throw await ApiClientError.fromResponse( + response, + "Failed to retrieve token. Please check your device code." + ); } }