microsoft-todo-mcp-server
Version:
Microsoft Todo MCP service for Claude and Cursor. Fork of @jhirono/todomcp
426 lines (423 loc) • 17.1 kB
JavaScript
// 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