Microsoft Todo MCP Service
by jhirono
Verified
- todoMCP
- src
// Authentication server for Microsoft Todo MCP service
import dotenv from 'dotenv';
import express from 'express';
import { writeFileSync } from 'fs';
import { join } from 'path';
import { ConfidentialClientApplication } from '@azure/msal-node';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
// Initialize environment variables
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 ? 'Present (hidden)' : 'Missing');
console.log('REDIRECT_URI:', process.env.REDIRECT_URI || `http://localhost:3000/callback`);
// Get current file directory in ESM
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const app = express();
const port = 3000;
const TOKEN_FILE_PATH = join(process.cwd(), 'tokens.json');
// MSAL configuration for delegated permissions
const msalConfig = {
auth: {
clientId: process.env.CLIENT_ID,
authority: `https://login.microsoftonline.com/${process.env.TENANT_ID}`,
clientSecret: process.env.CLIENT_SECRET,
},
system: {
loggerOptions: {
loggerCallback(loglevel, message, containsPii) {
console.log(`MSAL Log: ${message}`);
},
piiLoggingEnabled: true
}
},
cache: {
cachePlugin: {
beforeCacheAccess: async (cacheContext) => {
console.log('Cache access requested:', cacheContext);
return null;
},
afterCacheAccess: async (cacheContext) => {
console.log('Cache access completed:', cacheContext);
return null;
}
}
}
};
console.log('MSAL config created');
// Task-related permission scopes
const 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'
];
// Create MSAL application
const cca = new ConfidentialClientApplication(msalConfig);
console.log('MSAL application created');
// Setup a test route to check if server is working
app.get('/test', (req, res) => {
res.send('Auth server is running correctly');
});
// Setup the auth flow
app.get('/', (req, res) => {
console.log('Root route accessed, generating auth URL...');
const authCodeUrlParameters = {
scopes: 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: 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)}`);
});
});
// Handle the callback from Microsoft login
app.get('/callback', (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: scopes,
redirectUri: process.env.REDIRECT_URI || `http://localhost:${port}/callback`,
};
console.log('Token request parameters:', {
scopes: scopes,
redirectUri: process.env.REDIRECT_URI || `http://localhost:${port}/callback`,
});
cca.acquireTokenByCode(tokenRequest)
.then((response) => {
try {
// Log full response structure (without sensitive values)
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
});
// Calculate token expiration (make sure it's never null)
const expiresInSeconds = response.expiresIn || 3600;
const expiresAt = Date.now() + (expiresInSeconds * 1000) - (5 * 60 * 1000);
console.log('Token expiration details:', {
expiresInSeconds,
expiresAt: new Date(expiresAt).toLocaleString(),
currentTime: new Date().toLocaleString()
});
// Store tokens
const tokenData = {
accessToken: response.accessToken,
refreshToken: response.refreshToken || '',
expiresAt: expiresAt,
tokenType: response.tokenType,
scopes: response.scopes
};
writeFileSync(TOKEN_FILE_PATH, JSON.stringify(tokenData, null, 2), 'utf8');
console.log('Authentication successful! Token saved to:', TOKEN_FILE_PATH);
// Format token display with safety checks
const accessTokenDisplay = response.accessToken ?
`${response.accessToken.substring(0, 15)}...${response.accessToken.substring(response.accessToken.length - 5)}` :
'Not provided';
const refreshTokenDisplay = response.refreshToken ?
`${response.refreshToken.substring(0, 10)}...${response.refreshToken.substring(response.refreshToken.length - 5)}` :
'Not provided';
res.send(`
<h1>Authentication Successful!</h1>
<p>You can now close this window and use the Microsoft Todo MCP service.</p>
<p>Token details:</p>
<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>
<p>Debug Information:</p>
<pre>${JSON.stringify({
hasRefreshToken: !!response.refreshToken,
tokenType: response.tokenType,
scopes: response.scopes
}, null, 2)}</pre>
`);
} catch (error) {
console.error('Error saving token:', error);
res.status(500).send(`Error saving token: ${error.message}`);
}
})
.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)}`);
});
});
// Start the server
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.');
});