|
1 | 1 | #!/usr/bin/env node |
2 | 2 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; |
3 | 3 | 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"; |
5 | 7 | import dotenv from "dotenv"; |
6 | 8 | dotenv.config(); |
7 | | -// Replace require() with dynamic import for node-fetch |
8 | | -const fetchDynamic = async () => (await import("node-fetch")).default; |
9 | 9 | function wait(milliseconds) { |
10 | 10 | return new Promise(resolve => setTimeout(resolve, milliseconds)); |
11 | 11 | } |
12 | 12 | const server = new McpServer({ |
13 | 13 | name: "MongoDB Atlas", |
14 | 14 | version: "1.0.0" |
15 | 15 | }); |
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 | | -}; |
25 | 16 | const app = express(); |
26 | 17 | let authCode = ""; |
27 | 18 | app.get("/callback", (req, res) => { |
28 | 19 | authCode = req.query.code; |
29 | 20 | res.send("Authentication successful! You can close this tab."); |
30 | 21 | }); |
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 |
32 | 30 | 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!"); |
51 | 34 | return { |
52 | | - content: [{ type: "text", text: `Failed to initiate authentication: ${deviceCodeResponse.statusText}` }], |
| 35 | + content: [{ type: "text", text: "You are already authenticated!" }], |
53 | 36 | }; |
54 | 37 | } |
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 |
75 | 47 | }; |
| 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; |
76 | 56 | } |
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 | + }; |
126 | 63 | } |
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 | + }; |
140 | 69 | } |
141 | 70 | } |
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 | | - }; |
146 | 71 | }); |
147 | 72 | server.tool("list-clusters", "Lists clusters", async ({}) => { |
148 | 73 | await wait(1000); |
149 | | - if (!state.auth) { |
| 74 | + if (!globalState.auth) { |
150 | 75 | return { |
151 | 76 | content: [{ type: "text", text: "Not authenticated" }], |
152 | 77 | }; |
|
0 commit comments