Skip to content

Commit a85a108

Browse files
blvafmenezes
authored andcommitted
move tools to separate file and use enable logging
1 parent 92d8824 commit a85a108

File tree

8 files changed

+849
-212
lines changed

8 files changed

+849
-212
lines changed

dist/atlas-api.js

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { getToken } from "./auth.js";
2+
import { log } from "./index.js";
3+
// Fetch polyfill for Node.js
4+
const fetchDynamic = async () => (await import("node-fetch")).default;
5+
// Base URL for Atlas API
6+
const BASE_URL = "https://cloud.mongodb.com/api/atlas/v2";
7+
// Get project ID from environment variables or use a default value
8+
export const getProjectId = () => {
9+
return process.env.ATLAS_PROJECT_ID || "";
10+
};
11+
/**
12+
* Call the Atlas API with proper authentication
13+
* @param endpoint - API endpoint (e.g., "/groups")
14+
* @param method - HTTP method (default: 'GET')
15+
* @param body - Optional request body
16+
* @returns Promise with the API response data
17+
*/
18+
export async function callAtlasAPI(endpoint, method = 'GET', body) {
19+
const tokenData = await getToken();
20+
if (!tokenData || !tokenData.access_token) {
21+
throw new Error("Not authenticated. Please run the auth tool first.");
22+
}
23+
const token = tokenData.access_token;
24+
const url = `${BASE_URL}${endpoint}`;
25+
try {
26+
log("info", `Calling Atlas API: ${method} ${url}`);
27+
const fetch = await fetchDynamic();
28+
const response = await fetch(url, {
29+
method,
30+
headers: {
31+
'Authorization': `Bearer ${token}`,
32+
'Content-Type': 'application/json',
33+
'Accept': 'application/json'
34+
},
35+
body: body ? JSON.stringify(body) : undefined,
36+
});
37+
if (!response.ok) {
38+
const errorText = await response.text();
39+
log("error", `Atlas API Error (${response.status}): ${errorText}`);
40+
throw new Error(`Atlas API request failed: ${response.status} ${response.statusText} - ${errorText}`);
41+
}
42+
return await response.json();
43+
}
44+
catch (error) {
45+
log("error", `Error calling Atlas API: ${error}`);
46+
throw error;
47+
}
48+
}
49+
/**
50+
* Get all projects for the authenticated user
51+
*/
52+
export async function getProjects() {
53+
const response = await callAtlasAPI('/groups');
54+
return response;
55+
}
56+
/**
57+
* Get a specific project by ID
58+
*/
59+
export async function getProject(projectId) {
60+
return await callAtlasAPI(`/groups/${projectId}`);
61+
}
62+
/**
63+
* Get clusters for a specific project
64+
*/
65+
export async function getProjectClusters(projectId) {
66+
return await callAtlasAPI(`/groups/${projectId}/clusters`);
67+
}
68+
/**
69+
* Get all clusters across all accessible projects
70+
*/
71+
export async function getAllClusters() {
72+
const projectsResponse = await getProjects();
73+
const projects = projectsResponse.results || [];
74+
if (projects.length === 0) {
75+
return { results: [] };
76+
}
77+
const allClusters = [];
78+
for (const project of projects) {
79+
try {
80+
const clustersResponse = await getProjectClusters(project.id);
81+
// Enrich cluster data with project information
82+
const projectClusters = (clustersResponse.results || []).map((cluster) => ({
83+
...cluster,
84+
projectId: project.id,
85+
projectName: project.name
86+
}));
87+
allClusters.push(...projectClusters);
88+
}
89+
catch (error) {
90+
log("error", `Error fetching clusters for project ${project.id}: ${error}`);
91+
// Continue with other projects even if one fails
92+
}
93+
}
94+
return { results: allClusters };
95+
}
96+
/**
97+
* Format clusters into a readable table format
98+
*/
99+
export function formatClustersTable(clusters) {
100+
if (clusters.length === 0) {
101+
return "No clusters found.";
102+
}
103+
const header = `Project | Cluster Name | State | MongoDB Version | Region | Connection String
104+
----------------|----------------|----------------|----------------|----------------|----------------`;
105+
const rows = clusters.map(cluster => {
106+
const region = cluster.providerSettings?.regionName || 'N/A';
107+
const connectionString = cluster.connectionStrings?.standard || 'N/A';
108+
const mongoDBVersion = cluster.mongoDBVersion || 'N/A';
109+
const projectName = cluster.projectName || 'Unknown Project';
110+
return `${projectName} | ${cluster.name} | ${cluster.stateName} | ${mongoDBVersion} | ${region} | ${connectionString}`;
111+
}).join("\n");
112+
return `${header}\n${rows}`;
113+
}

dist/auth.js

Lines changed: 59 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import fs from "fs";
22
import path from "path";
33
import { fileURLToPath } from "url";
4+
import { globalState, log } from "./index.js";
45
// Replace __dirname with import.meta.url
56
const __filename = fileURLToPath(import.meta.url);
67
const __dirname = path.dirname(__filename);
@@ -10,33 +11,33 @@ const wait = (milliseconds) => {
1011
};
1112
const fetchDynamic = async () => (await import("node-fetch")).default;
1213
const TOKEN_FILE = path.resolve(__dirname, "token.json");
13-
// Update authState to use the AuthState interface
1414
export const authState = {
1515
deviceCode: "",
1616
verificationUri: "",
1717
userCode: "",
1818
clientId: process.env.CLIENT_ID || "0oabtxactgS3gHIR0297",
1919
};
20-
// Update functions to use authState and globalState
21-
import { globalState } from "./index.js";
2220
export async function authenticate() {
23-
console.log("Starting authentication process...");
21+
log("info", "Starting authentication process...");
2422
const authUrl = "https://cloud.mongodb.com/api/private/unauth/account/device/authorize";
25-
console.log("Client ID:", authState.clientId);
23+
log("info", `Client ID: ${authState.clientId}`);
2624
const deviceCodeResponse = await (await fetchDynamic())(authUrl, {
2725
method: "POST",
2826
headers: {
2927
"Content-Type": "application/x-www-form-urlencoded",
28+
"Accept": "application/json",
29+
"User-Agent": `AtlasMCP/${process.env.VERSION} (${process.platform}; ${process.arch}; ${process.env.HOSTNAME || "unknown"})`,
3030
},
3131
body: new URLSearchParams({
3232
client_id: authState.clientId,
33-
scope: "openid",
33+
scope: "openid profile offline_access",
34+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
3435
}).toString(),
3536
});
3637
const responseText = await deviceCodeResponse.text();
37-
console.log("Device Code Response Body:", responseText);
38+
log("info", `Device Code Response Body: ${responseText}`);
3839
if (!deviceCodeResponse.ok) {
39-
console.error("Failed to initiate authentication:", deviceCodeResponse.statusText);
40+
log("error", `Failed to initiate authentication: ${deviceCodeResponse.statusText}`);
4041
throw new Error(`Failed to initiate authentication: ${deviceCodeResponse.statusText}`);
4142
}
4243
const deviceCodeData = JSON.parse(responseText);
@@ -49,7 +50,7 @@ export async function authenticate() {
4950
};
5051
}
5152
export async function pollToken() {
52-
console.log("Starting token polling process...");
53+
log("info", "Starting token polling process...");
5354
if (!authState.deviceCode) {
5455
throw new Error("Device code not found. Please initiate authentication first.");
5556
}
@@ -70,7 +71,7 @@ export async function pollToken() {
7071
}).toString(),
7172
});
7273
const responseText = await OAuthToken.text();
73-
console.log("Token Response Body:", responseText);
74+
log("info", `Token Response Body: ${responseText}`);
7475
if (OAuthToken.ok) {
7576
const tokenData = JSON.parse(responseText);
7677
globalState.auth = true;
@@ -79,9 +80,9 @@ export async function pollToken() {
7980
}
8081
else {
8182
const errorResponse = JSON.parse(responseText);
82-
console.error("Token polling error:", errorResponse.error);
83+
log("error", `Token polling error: ${errorResponse.error}`);
8384
if (errorResponse.errorCode === "DEVICE_AUTHORIZATION_PENDING") {
84-
console.log("Device authorization is pending. Please try again later.");
85+
log("info", "Device authorization is pending. Please try again later.");
8586
continue;
8687
}
8788
else if (errorResponse.error === "expired_token") {
@@ -96,28 +97,27 @@ export async function pollToken() {
9697
}
9798
export function saveToken(token) {
9899
fs.writeFileSync(TOKEN_FILE, JSON.stringify({ token }));
99-
console.log("Token saved to file.");
100+
log("info", "Token saved to file.");
100101
}
101-
export function loadToken() {
102+
export function getToken() {
103+
if (!authState.token) {
104+
loadToken();
105+
}
106+
return authState.token;
107+
}
108+
function loadToken() {
102109
if (fs.existsSync(TOKEN_FILE)) {
103110
const data = JSON.parse(fs.readFileSync(TOKEN_FILE, "utf-8"));
104-
authState.token = data;
111+
authState.token = data.token;
105112
globalState.auth = true;
106-
console.log("Token loaded from file.");
113+
log("info", "Token loaded from file.");
114+
return data;
107115
}
116+
return undefined;
108117
}
109-
// Update getAuthStateData to return a typed object
110-
export function getAuthStateData() {
111-
return {
112-
deviceCode: authState.deviceCode,
113-
verificationUri: authState.verificationUri,
114-
userCode: authState.userCode,
115-
clientId: authState.clientId,
116-
};
117-
}
118-
// Extend isAuthenticated to validate and refresh the token
118+
// Check if token exists, if it's valid and refreshes it if necessary
119119
export async function isAuthenticated() {
120-
console.log("Checking authentication status...");
120+
log("info", "Checking authentication status...");
121121
if (globalState.auth) {
122122
return true;
123123
}
@@ -130,7 +130,7 @@ export async function isAuthenticated() {
130130
}
131131
// Validate the existing token
132132
try {
133-
const isValid = await validateToken(authState.token.access_token);
133+
const isValid = await validateToken(authState.token);
134134
if (isValid) {
135135
return true;
136136
}
@@ -144,44 +144,67 @@ export async function isAuthenticated() {
144144
}
145145
}
146146
catch (error) {
147-
console.error("Error during token validation or refresh:", error);
147+
log("error", `Error during token validation or refresh: ${error}`);
148148
}
149149
globalState.auth = false;
150150
return false;
151151
}
152-
// Helper function to validate the token
153-
async function validateToken(token) {
152+
async function validateToken(tokenData) {
154153
try {
155-
const tokenData = JSON.parse(fs.readFileSync(TOKEN_FILE, "utf-8"));
156154
const expiryDate = new Date(tokenData.expiry);
157155
return expiryDate > new Date(); // Token is valid if expiry is in the future
158156
}
159157
catch (error) {
160-
console.error("Error validating token:", error);
158+
log("error", `Error validating token: ${error}`);
161159
return false;
162160
}
163161
}
164-
// Update the code to cast 'data' to OAuthToken
165162
async function refreshToken(token) {
166163
try {
167164
const response = await (await fetchDynamic())("https://cloud.mongodb.com/api/private/unauth/account/device/token", {
168165
method: "POST",
169166
headers: {
170167
"Content-Type": "application/x-www-form-urlencoded",
168+
"Accept": "application/json",
169+
"User-Agent": `AtlasMCP/${process.env.VERSION} (${process.platform}; ${process.arch}; ${process.env.HOSTNAME || "unknown"})`,
171170
},
172171
body: new URLSearchParams({
173172
client_id: authState.clientId,
174173
refresh_token: token,
175174
grant_type: "refresh_token",
175+
scope: "openid profile offline_access",
176176
}).toString(),
177177
});
178178
if (response.ok) {
179-
const data = (await response.json()); // Explicit cast here
179+
const data = (await response.json());
180180
return data;
181181
}
182182
}
183183
catch (error) {
184-
console.error("Error refreshing token:", error);
184+
log("error", `Error refreshing token: ${error}`);
185185
}
186186
return null;
187187
}
188+
async function revokeToken(token) {
189+
try {
190+
const response = await (await fetchDynamic())("https://cloud.mongodb.com/api/private/unauth/account/device/revoke", {
191+
method: "POST",
192+
headers: {
193+
"Content-Type": "application/x-www-form-urlencoded",
194+
"Accept": "application/json",
195+
"User-Agent": `AtlasMCP/${process.env.VERSION} (${process.platform}; ${process.arch}; ${process.env.HOSTNAME || "unknown"})`,
196+
},
197+
body: new URLSearchParams({
198+
client_id: authState.clientId,
199+
token,
200+
token_type_hint: "refresh_token",
201+
}).toString(),
202+
});
203+
if (!response.ok) {
204+
log("error", `Failed to revoke token: ${response.statusText}`);
205+
}
206+
}
207+
catch (error) {
208+
log("error", `Error revoking token: ${error}`);
209+
}
210+
}

0 commit comments

Comments
 (0)