UNPKG

microsoft-todo-mcp-server

Version:

Microsoft Todo MCP service for Claude and Cursor. Fork of @jhirono/todomcp

426 lines (423 loc) 17.1 kB
// src/auth-server.ts import dotenv from "dotenv"; import express from "express"; import fs from "fs"; import { join, dirname } from "path"; import { ConfidentialClientApplication, LogLevel } from "@azure/msal-node"; import { fileURLToPath } from "url"; dotenv.config(); console.log("Environment loaded"); console.log("CLIENT_ID:", process.env.CLIENT_ID ? "Present (hidden)" : "Missing"); console.log("CLIENT_SECRET:", process.env.CLIENT_SECRET ? "Present (hidden)" : "Missing"); console.log( "TENANT_ID:", process.env.TENANT_ID ? process.env.TENANT_ID : 'Not specified, using "organizations" (multi-tenant)' ); console.log("REDIRECT_URI:", process.env.REDIRECT_URI || `http://localhost:3000/callback`); var __filename = fileURLToPath(import.meta.url); var __dirname = dirname(__filename); var app = express(); var port = 3e3; var TOKEN_FILE_PATH = join(process.cwd(), "tokens.json"); var tenantId = process.env.TENANT_ID || "organizations"; if (tenantId === "common") { console.log("Authentication type: Both organization and personal accounts (common)"); } else if (tenantId === "organizations") { console.log("Authentication type: Organizations only (multi-tenant)"); } else if (tenantId === "consumers") { console.log("Authentication type: Personal accounts only"); console.log( "WARNING: Microsoft To Do API has limitations for personal accounts (MailboxNotEnabledForRESTAPI error may occur)" ); } else { console.log(`Authentication type: Single tenant (${tenantId})`); } var msalConfig = { auth: { clientId: process.env.CLIENT_ID, authority: `https://login.microsoftonline.com/${tenantId}`, clientSecret: process.env.CLIENT_SECRET }, system: { loggerOptions: { loggerCallback(loglevel, message, containsPii) { console.log(`MSAL Log: ${message}`); }, piiLoggingEnabled: true, logLevel: LogLevel.Verbose } }, cache: { cachePlugin: { beforeCacheAccess: async (cacheContext) => { console.log("Cache access requested:", cacheContext); }, afterCacheAccess: async (cacheContext) => { console.log("Cache access completed:", cacheContext); } } } }; console.log("MSAL config created"); var scopes = [ "offline_access", // Put offline_access first to ensure it's not dropped "openid", // Add openid scope "profile", // Add profile scope "Tasks.Read", "Tasks.Read.Shared", "Tasks.ReadWrite", "Tasks.ReadWrite.Shared", "User.Read" ]; var cca = new ConfidentialClientApplication(msalConfig); console.log("MSAL application created"); app.get("/test", (req, res) => { res.send("Auth server is running correctly"); }); async function refreshAccessToken() { try { const tokenCache = cca.getTokenCache(); const accounts = await tokenCache.getAllAccounts(); if (accounts.length === 0) { console.log("No accounts found in the token cache"); return { success: false, error: "No accounts found in token cache" }; } const account = accounts[0]; console.log("Found account in token cache:", { username: account.username, localAccountId: account.localAccountId, tenantId: account.tenantId }); const silentRequest = { account, scopes, forceRefresh: true }; console.log("Attempting to acquire token silently..."); const response = await cca.acquireTokenSilent(silentRequest); console.log("Token refreshed successfully"); return { success: true, response, accessToken: response.accessToken, expiresAt: Date.now() + (response.expiresIn || 3600) * 1e3 - 5 * 60 * 1e3 }; } catch (error) { console.error("Error refreshing token silently:", error); return { success: false, error }; } } app.get("/refresh", async (req, res) => { try { const result = await refreshAccessToken(); if (result.success) { const tokenData = { accessToken: result.accessToken, expiresAt: result.expiresAt, tokenType: result.response.tokenType, scopes: result.response.scopes }; fs.writeFileSync(TOKEN_FILE_PATH, JSON.stringify(tokenData, null, 2), "utf8"); res.json({ success: true, message: "Token refreshed successfully", expiresAt: new Date(result.expiresAt).toISOString() }); } else { console.log("Silent token refresh failed, redirecting to login"); res.json({ success: false, message: "Token refresh failed, please login again", redirectUrl: "/" }); } } catch (error) { console.error("Error in refresh route:", error); res.status(500).send(`Error refreshing token: ${error.message}`); } }); app.get("/silentLogin", async (req, res) => { try { console.log("Silent login endpoint accessed"); const clientCredentialRequest = { scopes: ["https://graph.microsoft.com/.default"], skipCache: true // Force request to go to the server }; console.log("Attempting client credentials flow with scopes:", clientCredentialRequest.scopes); const response = await cca.acquireTokenByClientCredential(clientCredentialRequest); if (!response) { throw new Error("No response from client credentials flow"); } console.log("Client credentials response received", { hasAccessToken: !!response.accessToken, tokenType: response.tokenType, expiresOn: response.expiresOn, scopes: response.scopes }); const tokenCache = cca.getTokenCache(); const serializedCache = await tokenCache.serialize(); const cacheJson = JSON.parse(serializedCache); console.log("Token cache after client credentials flow:", { hasRefreshTokens: !!cacheJson.RefreshTokens, hasRefreshToken: !!cacheJson.RefreshToken, cacheKeys: Object.keys(cacheJson) }); let refreshTokenFound = false; for (const key in cacheJson) { if (key.toLowerCase().includes("refresh")) { refreshTokenFound = true; console.log(`Found potential refresh token section: ${key}`); } } if (!refreshTokenFound) { console.log("No refresh token sections found in cache after client credentials flow"); } res.json({ success: true, message: "Client credentials flow completed", accessTokenPresent: !!response.accessToken, expiresOn: response.expiresOn }); } catch (error) { console.error("Error in silent login:", error); res.status(500).send(`Error in silent login: ${error.message}`); } }); app.get("/", (req, res) => { console.log("Root route accessed, generating auth URL..."); let authInfo = ` <html> <head> <title>Microsoft To Do MCP Authentication</title> <style> body { font-family: Arial, sans-serif; margin: 40px; line-height: 1.6; } .container { max-width: 800px; margin: 0 auto; } .warning { background-color: #fff3cd; border: 1px solid #ffeeba; padding: 15px; border-radius: 4px; margin-bottom: 20px; } .primary-button { background-color: #0078d4; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; } </style> </head> <body> <div class="container"> <h1>Microsoft To Do MCP Authentication</h1> `; if (tenantId === "consumers" || tenantId === "common") { authInfo += ` <div class="warning"> <h3>\u26A0\uFE0F Important Note for Personal Microsoft Accounts</h3> <p>The Microsoft Graph API has limitations for personal Microsoft accounts (outlook.com, hotmail.com, live.com, etc.). The To Do API is primarily designed for Microsoft 365 business accounts, not personal accounts.</p> <p>If you use a personal Microsoft account, you may encounter a <strong>"MailboxNotEnabledForRESTAPI"</strong> error. This is a Microsoft service limitation, not an issue with this application's code or authentication setup.</p> </div> `; } authInfo += ` <p>Click the button below to authenticate with Microsoft and grant access to your To Do tasks.</p> <button class="primary-button" onclick="window.location.href='/auth'">Sign in with Microsoft</button> </div> </body> </html> `; res.send(authInfo); }); app.get("/auth", (req, res) => { console.log("Auth route accessed, generating auth URL..."); const authCodeUrlParameters = { scopes, redirectUri: process.env.REDIRECT_URI || `http://localhost:${port}/callback`, prompt: "consent", // Use only consent to force refresh token responseMode: "query" }; console.log("Auth parameters:", { scopes, redirectUri: process.env.REDIRECT_URI || `http://localhost:${port}/callback`, prompt: "consent", responseMode: "query" }); cca.getAuthCodeUrl(authCodeUrlParameters).then((response) => { console.log("Auth URL generated, redirecting to:", response.substring(0, 80) + "..."); res.redirect(response); }).catch((error) => { console.error("Error getting auth code URL:", error); res.status(500).send(`Error generating authentication URL: ${JSON.stringify(error)}`); }); }); app.get("/callback", async (req, res) => { console.log("Callback route accessed"); console.log("Query parameters:", { code: req.query.code ? "Present (hidden)" : "Missing", state: req.query.state ? "Present" : "Missing", error: req.query.error || "None", error_description: req.query.error_description || "None" }); const tokenRequest = { code: req.query.code, scopes, redirectUri: process.env.REDIRECT_URI || `http://localhost:${port}/callback` }; console.log("Token request parameters:", { scopes, redirectUri: process.env.REDIRECT_URI || `http://localhost:${port}/callback` }); try { const response = await cca.acquireTokenByCode(tokenRequest); console.log("Token response structure:", { keys: Object.keys(response), hasAccessToken: !!response.accessToken, hasRefreshToken: !!response.refreshToken, hasIdToken: !!response.idToken, tokenType: response.tokenType, expiresIn: response.expiresIn, expiresOn: response.expiresOn, scopes: response.scopes, account: response.account ? { username: response.account.username, tenantId: response.account.tenantId, localAccountId: response.account.localAccountId } : null }); const tokenCache = cca.getTokenCache(); const serializedCache = await tokenCache.serialize(); const cacheJson = JSON.parse(serializedCache); console.log("Full token cache structure keys:", Object.keys(cacheJson)); if (cacheJson.RefreshToken) { console.log("RefreshToken keys in cache:", Object.keys(cacheJson.RefreshToken)); } else if (cacheJson.RefreshTokens) { console.log("RefreshTokens keys in cache:", Object.keys(cacheJson.RefreshTokens)); } let refreshToken = null; if (cacheJson.RefreshTokens && Object.keys(cacheJson.RefreshTokens).length > 0) { const refreshTokenKeys = Object.keys(cacheJson.RefreshTokens); refreshToken = cacheJson.RefreshTokens[refreshTokenKeys[0]].secret; console.log("Refresh token found using RefreshTokens collection"); } else if (cacheJson.RefreshToken && Object.keys(cacheJson.RefreshToken).length > 0) { const refreshTokenKeys = Object.keys(cacheJson.RefreshToken); refreshToken = cacheJson.RefreshToken[refreshTokenKeys[0]].secret; console.log("Refresh token found using RefreshToken collection"); } else { for (const cacheSection in cacheJson) { if (cacheSection.toLowerCase().includes("refresh") && typeof cacheJson[cacheSection] === "object") { for (const key in cacheJson[cacheSection]) { if (cacheJson[cacheSection][key] && cacheJson[cacheSection][key].secret) { refreshToken = cacheJson[cacheSection][key].secret; console.log(`Refresh token found in ${cacheSection}.${key}`); break; } } if (refreshToken) break; } } } if (!refreshToken) { console.log("Could not find refresh token in token cache"); } const expiresInSeconds = response.expiresIn || 3600; const expiresAt = Date.now() + expiresInSeconds * 1e3 - 5 * 60 * 1e3; console.log("Token expiration details:", { expiresInSeconds, expiresAt: new Date(expiresAt).toLocaleString(), currentTime: (/* @__PURE__ */ new Date()).toLocaleString() }); const tokenData = { accessToken: response.accessToken, refreshToken: refreshToken || "", expiresAt, tokenType: response.tokenType, scopes: response.scopes, // Add client credentials for automatic refresh clientId: process.env.CLIENT_ID, clientSecret: process.env.CLIENT_SECRET, tenantId }; fs.writeFileSync(TOKEN_FILE_PATH, JSON.stringify(tokenData, null, 2), "utf8"); console.log("Authentication successful! Token saved to:", TOKEN_FILE_PATH); console.log("Refresh token obtained:", refreshToken ? "Yes" : "No"); const accessTokenDisplay = response.accessToken ? `${response.accessToken.substring(0, 15)}...${response.accessToken.substring(response.accessToken.length - 5)}` : "Not provided"; const refreshTokenDisplay = refreshToken ? `${refreshToken.substring(0, 10)}...${refreshToken.substring(refreshToken.length - 5)}` : "Not provided"; const isPersonalAccount = response.account && (response.account.username.includes("@outlook.com") || response.account.username.includes("@hotmail.com") || response.account.username.includes("@live.com") || response.account.username.includes("@msn.com")); let warningMessage = ""; if (isPersonalAccount) { warningMessage = ` <div class="warning"> <h3>\u26A0\uFE0F Important Note for Personal Microsoft Accounts</h3> <p>You are signed in with a personal Microsoft account (${response.account.username}).</p> <p>The Microsoft Graph API has limitations for personal Microsoft accounts. The To Do API is primarily designed for Microsoft 365 business accounts, not personal accounts.</p> <p>You may encounter a <strong>"MailboxNotEnabledForRESTAPI"</strong> error when trying to access To Do tasks. This is a Microsoft service limitation, not an issue with this application's code or authentication setup.</p> </div> `; } res.send(` <html> <head> <title>Authentication Successful</title> <style> body { font-family: Arial, sans-serif; margin: 40px; line-height: 1.6; } .container { max-width: 800px; margin: 0 auto; } .success { background-color: #d4edda; border: 1px solid #c3e6cb; padding: 15px; border-radius: 4px; margin-bottom: 20px; } .warning { background-color: #fff3cd; border: 1px solid #ffeeba; padding: 15px; border-radius: 4px; margin-bottom: 20px; } .token-details { background-color: #f8f9fa; padding: 15px; border-radius: 4px; margin-top: 20px; } .debug-info { margin-top: 30px; border-top: 1px solid #dee2e6; padding-top: 20px; } </style> </head> <body> <div class="container"> <div class="success"> <h1>Authentication Successful!</h1> <p>You can now close this window and use the Microsoft Todo MCP service.</p> </div> ${warningMessage} <div class="token-details"> <h3>Token Details:</h3> <ul> <li>Access Token: ${accessTokenDisplay}</li> <li>Refresh Token: ${refreshTokenDisplay}</li> <li>Token Type: ${response.tokenType || "Not provided"}</li> <li>Scopes: ${response.scopes ? response.scopes.join(", ") : "Not provided"}</li> <li>Expires: ${new Date(expiresAt).toLocaleString()}</li> </ul> </div> <div class="debug-info"> <h3>Debug Information:</h3> <pre>${JSON.stringify( { hasRefreshToken: !!refreshToken, tokenType: response.tokenType, scopes: response.scopes, cacheHasRefreshTokens: cacheJson.RefreshTokens && Object.keys(cacheJson.RefreshTokens).length > 0 }, null, 2 )}</pre> </div> </div> </body> </html> `); } catch (error) { console.error("Token acquisition error:", { errorCode: error.errorCode, errorMessage: error.errorMessage, subError: error.subError, correlationId: error.correlationId, stack: error.stack }); res.status(500).send(`Error acquiring token: ${JSON.stringify(error)}`); } }); app.listen(port, () => { console.log(`Auth server running at http://localhost:${port}`); console.log("Open your browser and navigate to the URL above to authenticate."); console.log("Or try http://localhost:3000/test to verify the server is running."); }); //# sourceMappingURL=auth-server.js.map