Skip to content

Commit 8f0754e

Browse files
blvafmenezes
authored andcommitted
chore: add OAuth support
1 parent 9a376ac commit 8f0754e

File tree

8 files changed

+616
-270
lines changed

8 files changed

+616
-270
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ node_modules
22
.vscode/mcp.json
33

44
# Environment variables
5-
.env
5+
.env
6+
dist/token.json

dist/auth.js

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import fs from "fs";
2+
import path from "path";
3+
import { fileURLToPath } from "url";
4+
// Replace __dirname with import.meta.url
5+
const __filename = fileURLToPath(import.meta.url);
6+
const __dirname = path.dirname(__filename);
7+
// Remove imports for missing utils and define wait and fetchDynamic locally
8+
const wait = (milliseconds) => {
9+
return new Promise(resolve => setTimeout(resolve, milliseconds));
10+
};
11+
const fetchDynamic = async () => (await import("node-fetch")).default;
12+
const TOKEN_FILE = path.resolve(__dirname, "token.json");
13+
// Update authState to use the AuthState interface
14+
export const authState = {
15+
deviceCode: "",
16+
verificationUri: "",
17+
userCode: "",
18+
clientId: process.env.CLIENT_ID || "0oabtxactgS3gHIR0297",
19+
};
20+
// Update functions to use authState and globalState
21+
import { globalState } from "./index.js";
22+
export async function authenticate() {
23+
console.log("Starting authentication process...");
24+
const authUrl = "https://cloud.mongodb.com/api/private/unauth/account/device/authorize";
25+
console.log("Client ID:", authState.clientId);
26+
const deviceCodeResponse = await (await fetchDynamic())(authUrl, {
27+
method: "POST",
28+
headers: {
29+
"Content-Type": "application/x-www-form-urlencoded",
30+
},
31+
body: new URLSearchParams({
32+
client_id: authState.clientId,
33+
scope: "openid",
34+
}).toString(),
35+
});
36+
const responseText = await deviceCodeResponse.text();
37+
console.log("Device Code Response Body:", responseText);
38+
if (!deviceCodeResponse.ok) {
39+
console.error("Failed to initiate authentication:", deviceCodeResponse.statusText);
40+
throw new Error(`Failed to initiate authentication: ${deviceCodeResponse.statusText}`);
41+
}
42+
const deviceCodeData = JSON.parse(responseText);
43+
authState.deviceCode = deviceCodeData.device_code;
44+
authState.verificationUri = deviceCodeData.verification_uri;
45+
authState.userCode = deviceCodeData.user_code;
46+
return {
47+
verificationUri: deviceCodeData.verification_uri,
48+
userCode: deviceCodeData.user_code,
49+
};
50+
}
51+
export async function pollToken() {
52+
console.log("Starting token polling process...");
53+
if (!authState.deviceCode) {
54+
throw new Error("Device code not found. Please initiate authentication first.");
55+
}
56+
const tokenEndpoint = "https://cloud.mongodb.com/api/private/unauth/account/device/token";
57+
const interval = 5 * 1000;
58+
const expiresAt = Date.now() + 2 * 60 * 1000;
59+
while (Date.now() < expiresAt) {
60+
await wait(interval);
61+
const OAuthToken = await (await fetchDynamic())(tokenEndpoint, {
62+
method: "POST",
63+
headers: {
64+
"Content-Type": "application/x-www-form-urlencoded",
65+
},
66+
body: new URLSearchParams({
67+
client_id: authState.clientId,
68+
device_code: authState.deviceCode,
69+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
70+
}).toString(),
71+
});
72+
const responseText = await OAuthToken.text();
73+
console.log("Token Response Body:", responseText);
74+
if (OAuthToken.ok) {
75+
const tokenData = JSON.parse(responseText);
76+
globalState.auth = true;
77+
saveToken(tokenData);
78+
return tokenData.access_token;
79+
}
80+
else {
81+
const errorResponse = JSON.parse(responseText);
82+
console.error("Token polling error:", errorResponse.error);
83+
if (errorResponse.errorCode === "DEVICE_AUTHORIZATION_PENDING") {
84+
console.log("Device authorization is pending. Please try again later.");
85+
continue;
86+
}
87+
else if (errorResponse.error === "expired_token") {
88+
throw new Error("Device code expired. Please restart the authentication process.");
89+
}
90+
else {
91+
throw new Error(`Failed to authenticate: ${errorResponse.error_description || "Unknown error"}`);
92+
}
93+
}
94+
}
95+
throw new Error("Authentication timed out. Please restart the process.");
96+
}
97+
export function saveToken(token) {
98+
fs.writeFileSync(TOKEN_FILE, JSON.stringify({ token }));
99+
console.log("Token saved to file.");
100+
}
101+
export function loadToken() {
102+
if (fs.existsSync(TOKEN_FILE)) {
103+
const data = JSON.parse(fs.readFileSync(TOKEN_FILE, "utf-8"));
104+
authState.token = data;
105+
globalState.auth = true;
106+
console.log("Token loaded from file.");
107+
}
108+
}
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
119+
export async function isAuthenticated() {
120+
console.log("Checking authentication status...");
121+
if (globalState.auth) {
122+
return true;
123+
}
124+
// Try to load token from file if not already loaded
125+
if (!authState.token) {
126+
loadToken();
127+
}
128+
if (!authState.token) {
129+
return false;
130+
}
131+
// Validate the existing token
132+
try {
133+
const isValid = await validateToken(authState.token.access_token);
134+
if (isValid) {
135+
return true;
136+
}
137+
// If the token is invalid, attempt to refresh it
138+
const refreshedToken = await refreshToken(authState.token.access_token);
139+
if (refreshedToken) {
140+
authState.token = refreshedToken;
141+
globalState.auth = true;
142+
saveToken(refreshedToken);
143+
return true;
144+
}
145+
}
146+
catch (error) {
147+
console.error("Error during token validation or refresh:", error);
148+
}
149+
globalState.auth = false;
150+
return false;
151+
}
152+
// Helper function to validate the token
153+
async function validateToken(token) {
154+
try {
155+
const tokenData = JSON.parse(fs.readFileSync(TOKEN_FILE, "utf-8"));
156+
const expiryDate = new Date(tokenData.expiry);
157+
return expiryDate > new Date(); // Token is valid if expiry is in the future
158+
}
159+
catch (error) {
160+
console.error("Error validating token:", error);
161+
return false;
162+
}
163+
}
164+
// Update the code to cast 'data' to OAuthToken
165+
async function refreshToken(token) {
166+
try {
167+
const response = await (await fetchDynamic())("https://cloud.mongodb.com/api/private/unauth/account/device/token", {
168+
method: "POST",
169+
headers: {
170+
"Content-Type": "application/x-www-form-urlencoded",
171+
},
172+
body: new URLSearchParams({
173+
client_id: authState.clientId,
174+
refresh_token: token,
175+
grant_type: "refresh_token",
176+
}).toString(),
177+
});
178+
if (response.ok) {
179+
const data = (await response.json()); // Explicit cast here
180+
return data;
181+
}
182+
}
183+
catch (error) {
184+
console.error("Error refreshing token:", error);
185+
}
186+
return null;
187+
}

dist/index.js

Lines changed: 44 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -1,152 +1,77 @@
11
#!/usr/bin/env node
22
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
33
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4-
import express from "express"; // Fixing type imports
4+
import express from "express";
5+
// Import the necessary modules
6+
import { loadToken, pollToken, authenticate, isAuthenticated } from "./auth.js";
57
import dotenv from "dotenv";
68
dotenv.config();
7-
// Replace require() with dynamic import for node-fetch
8-
const fetchDynamic = async () => (await import("node-fetch")).default;
99
function wait(milliseconds) {
1010
return new Promise(resolve => setTimeout(resolve, milliseconds));
1111
}
1212
const server = new McpServer({
1313
name: "MongoDB Atlas",
1414
version: "1.0.0"
1515
});
16-
// Move clientId to a state variable
17-
var state = {
18-
auth: false,
19-
token: "", // Added token property
20-
deviceCode: "",
21-
verificationUri: "",
22-
userCode: "",
23-
clientId: process.env.CLIENT_ID || "0oabtxactgS3gHIR0297", // Moved clientId to state
24-
};
2516
const app = express();
2617
let authCode = "";
2718
app.get("/callback", (req, res) => {
2819
authCode = req.query.code;
2920
res.send("Authentication successful! You can close this tab.");
3021
});
31-
// Update the device code request to align with the Atlas Go SDK
22+
// Update globalState to use the GlobalState interface
23+
export const globalState = {
24+
auth: false,
25+
};
26+
// Update imports to include globalState
27+
// Load token on server start
28+
loadToken();
29+
// Update references to state in the server tools
3230
server.tool("auth", "Authenticate to Atlas", async ({}) => {
33-
console.log("Starting authentication process...");
34-
const authUrl = "https://cloud.mongodb.com/api/private/unauth/account/device/authorize"; // Updated endpoint
35-
console.log("Client ID:", state.clientId);
36-
// Step 1: Request a device code
37-
const deviceCodeResponse = await (await fetchDynamic())(authUrl, {
38-
method: "POST",
39-
headers: {
40-
"Content-Type": "application/x-www-form-urlencoded",
41-
},
42-
body: new URLSearchParams({
43-
client_id: state.clientId, // Use state.clientId
44-
scope: "openid",
45-
}).toString(),
46-
});
47-
const responseText = await deviceCodeResponse.text(); // Capture the full response body
48-
console.log("Device Code Response Body:", responseText);
49-
if (!deviceCodeResponse.ok) {
50-
console.error("Failed to initiate authentication:", deviceCodeResponse.statusText);
31+
const authResult = await isAuthenticated();
32+
if (authResult) {
33+
console.log("Already authenticated!");
5134
return {
52-
content: [{ type: "text", text: `Failed to initiate authentication: ${deviceCodeResponse.statusText}` }],
35+
content: [{ type: "text", text: "You are already authenticated!" }],
5336
};
5437
}
55-
const deviceCodeData = JSON.parse(responseText); // Parse the response body
56-
console.log(`Please authenticate by visiting the following URL: ${deviceCodeData.verification_uri}`);
57-
console.log(`Enter the code: ${deviceCodeData.user_code}`);
58-
// Store the device code data for further use
59-
state.deviceCode = deviceCodeData.device_code;
60-
state.verificationUri = deviceCodeData.verification_uri;
61-
state.userCode = deviceCodeData.user_code;
62-
return {
63-
content: [
64-
{ type: "text", text: `Please authenticate by visiting ${deviceCodeData.verification_uri} and entering the code ${deviceCodeData.user_code}` },
65-
],
66-
};
67-
});
68-
// Add PollToken functionality to the auth tool
69-
server.tool("poll-token", "Poll for Access Token", async ({}) => {
70-
console.log("Starting token polling process...");
71-
if (!state.deviceCode) {
72-
console.error("Device code not found. Please initiate authentication first.");
73-
return {
74-
content: [{ type: "text", text: "Device code not found. Please initiate authentication first." }],
38+
try {
39+
// Step 1: Generate the device code
40+
const { verificationUri, userCode } = await authenticate();
41+
// Inform the user to authenticate
42+
const initialResponse = {
43+
content: [
44+
{ type: "text", text: `Please authenticate by visiting ${verificationUri} and entering the code ${userCode}` },
45+
{ type: "text", text: "Polling for token..." }
46+
], // Explicitly typed to match the expected structure
7547
};
48+
// Start polling for the token asynchronously
49+
pollToken().then(_ => {
50+
globalState.auth = true;
51+
console.log("Authentication successful!");
52+
}).catch(error => {
53+
console.error("Token polling failed:", error);
54+
});
55+
return initialResponse;
7656
}
77-
const tokenEndpoint = "https://cloud.mongodb.com/api/private/unauth/account/device/token";
78-
const interval = 5 * 1000; // Default polling interval in milliseconds
79-
const expiresAt = Date.now() + 15 * 60 * 1000; // Assume 15 minutes expiration for the device code
80-
while (Date.now() < expiresAt) {
81-
await wait(interval);
82-
try {
83-
const tokenResponse = await (await fetchDynamic())(tokenEndpoint, {
84-
method: "POST",
85-
headers: {
86-
"Content-Type": "application/x-www-form-urlencoded",
87-
},
88-
body: new URLSearchParams({
89-
client_id: state.clientId, // Use state.clientId
90-
device_code: state.deviceCode,
91-
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
92-
}).toString(),
93-
});
94-
const responseText = await tokenResponse.text();
95-
console.log("Token Response Body:", responseText);
96-
if (tokenResponse.ok) {
97-
const tokenData = JSON.parse(responseText);
98-
console.log("Authentication successful. Token received:", tokenData.access_token);
99-
// Store the token
100-
state.auth = true;
101-
state.token = tokenData.access_token;
102-
return {
103-
content: [{ type: "text", text: "Authentication successful! You are now logged in." }],
104-
};
105-
}
106-
else {
107-
console.error("Token Response Error:", responseText);
108-
const errorResponse = JSON.parse(responseText);
109-
if (errorResponse.error === "authorization_pending") {
110-
console.log("Authorization pending. Retrying...");
111-
continue;
112-
}
113-
else if (errorResponse.error === "expired_token") {
114-
console.error("Device code expired. Please restart the authentication process.");
115-
return {
116-
content: [{ type: "text", text: "Device code expired. Please restart the authentication process." }],
117-
};
118-
}
119-
else {
120-
console.error("Failed to authenticate:", errorResponse.error_description || "Unknown error");
121-
return {
122-
content: [{ type: "text", text: `Failed to authenticate: ${errorResponse.error_description || "Unknown error"}` }],
123-
};
124-
}
125-
}
57+
catch (error) {
58+
if (error instanceof Error) {
59+
console.error("Authentication error:", error);
60+
return {
61+
content: [{ type: "text", text: `Authentication failed: ${error.message}` }],
62+
};
12663
}
127-
catch (error) {
128-
if (error instanceof Error) {
129-
console.error("Unexpected error during token polling:", error);
130-
return {
131-
content: [{ type: "text", text: `Unexpected error during token polling: ${error.message}` }],
132-
};
133-
}
134-
else {
135-
console.error("Unexpected non-Error object during token polling:", error);
136-
return {
137-
content: [{ type: "text", text: "Unexpected error during token polling." }],
138-
};
139-
}
64+
else {
65+
console.error("Unknown authentication error:", error);
66+
return {
67+
content: [{ type: "text", text: "Authentication failed due to an unknown error." }],
68+
};
14069
}
14170
}
142-
console.error("Authentication timed out. Please restart the process.");
143-
return {
144-
content: [{ type: "text", text: "Authentication timed out. Please restart the process." }],
145-
};
14671
});
14772
server.tool("list-clusters", "Lists clusters", async ({}) => {
14873
await wait(1000);
149-
if (!state.auth) {
74+
if (!globalState.auth) {
15075
return {
15176
content: [{ type: "text", text: "Not authenticated" }],
15277
};

0 commit comments

Comments
 (0)