outlook-mcp/auth/tools.js
Seton Carmichael e70840552d feat(outlook-mcp): shared mailbox targeting, discovery, and scopes (v1.1.0)
Add optional mailbox (UPN/SMTP) routing on email/folder/thread tools via
users/{upn}/... and X-AnchorMailbox. New list-mailboxes probes primary,
OUTLOOK_SHARED_MAILBOXES seeds, cache, and candidates. Send supports
mailbox-rooted sendMail and onBehalfOf. MSAL requests Mail.*.Shared;
check-auth-status reports token scp gaps. Docs, env example, tests.
2026-08-24 08:41:05 -04:00

145 lines
5.5 KiB
JavaScript

/**
* Authentication-related tools for the Outlook MCP server
*/
const config = require('../config');
const tokenManager = require('./token-manager');
const { analyzeTokenScopes, expectedSharedScopes } = require('../utils/mailbox');
async function handleAbout() {
const sharedLine = config.ENABLE_SHARED_MAILBOXES
? 'Shared mailbox support: enabled (optional mailbox param + list-mailboxes). Requires Mail.*.Shared scopes and re-auth after upgrade.'
: 'Shared mailbox support: disabled (OUTLOOK_ENABLE_SHARED_MAILBOXES=false).';
return {
content: [{
type: "text",
text: `Outlook Assistant MCP Server v${config.SERVER_VERSION}\n\nProvides access to Microsoft Outlook email, calendar, folders, and rules through Microsoft Graph API.\n${sharedLine}`
}]
};
}
/**
* Initiates device code flow via MSAL. Returns the code and URL to the user
* immediately; MSAL polls internally until the user completes sign-in.
*/
async function handleAuthenticate(args) {
if (config.USE_TEST_MODE) {
tokenManager.createTestTokens();
return {
content: [{ type: "text", text: 'Successfully authenticated with Microsoft Graph API (test mode)' }]
};
}
let flowResult;
try {
flowResult = await tokenManager.initiateDeviceCodeFlow();
} catch (err) {
console.error('[authenticate] Failed to initiate device code flow:', err.message);
return {
content: [{ type: "text", text: `Failed to start authentication: ${err.message}` }]
};
}
const { deviceCodeInfo, tokenPromise } = flowResult;
const { userCode, verificationUri, expiresIn } = deviceCodeInfo;
const minutesRemaining = Math.floor(expiresIn / 60);
// MSAL polls internally — just log completion when it resolves
tokenPromise
.then(result => {
if (result) console.error('[authenticate] Sign-in completed — MSAL cached tokens.');
})
.catch(err => {
console.error(`[authenticate] Device code flow ended: ${err.message}`);
});
const scopeHint = config.ENABLE_SHARED_MAILBOXES
? `\nThis login requests shared-mailbox scopes: ${expectedSharedScopes().join(', ')}.\nAfter signing in, call check-auth-status and confirm those scopes are present.`
: '';
return {
content: [{
type: "text",
text: [
`To sign in to Microsoft, please:`,
``,
` 1. Open: ${verificationUri}`,
` 2. Enter code: ${userCode}`,
``,
`You have ${minutesRemaining} minutes to complete sign-in.`,
`After signing in, call check-auth-status to confirm.`,
scopeHint
].filter(Boolean).join('\n')
}]
};
}
async function handleCheckAuthStatus() {
console.error(`[CHECK-AUTH-STATUS] Checking for valid token...`);
try {
const token = await tokenManager.getAccessToken();
if (!token) {
console.error('[CHECK-AUTH-STATUS] No valid token found');
return { content: [{ type: "text", text: "Not authenticated" }] };
}
console.error('[CHECK-AUTH-STATUS] Valid token acquired');
const lines = ['Authenticated and ready'];
if (config.ENABLE_SHARED_MAILBOXES) {
const analysis = analyzeTokenScopes(token);
if (analysis.missing.length === 0) {
lines.push('Shared mailbox scopes: OK (' + analysis.present.join(', ') + ')');
} else if (analysis.present.length === 0) {
lines.push('Shared mailbox scopes: MISSING (' + analysis.missing.join(', ') + ')');
lines.push('Re-run authenticate (device code) after adding these delegated permissions in Entra and granting admin consent.');
} else {
lines.push('Shared mailbox scopes: PARTIAL');
lines.push(' present: ' + analysis.present.join(', '));
lines.push(' missing: ' + analysis.missing.join(', '));
lines.push('Re-run authenticate so the token picks up the missing scopes.');
}
if (analysis.scp) {
lines.push('Token scp: ' + analysis.scp);
}
} else {
lines.push('Shared mailbox feature disabled (OUTLOOK_ENABLE_SHARED_MAILBOXES=false).');
}
return { content: [{ type: "text", text: lines.join('\n') }] };
} catch (e) {
console.error('[CHECK-AUTH-STATUS] Error:', e.message);
return { content: [{ type: "text", text: "Not authenticated" }] };
}
}
const authTools = [
{
name: "about",
description: "Returns information about this Outlook Assistant server",
inputSchema: { type: "object", properties: {}, required: [] },
handler: handleAbout
},
{
name: "authenticate",
description: "Authenticate with Microsoft Graph API using device code flow. Returns a short code and URL. IMPORTANT: Show the user the code and URL — they must visit the URL on any device and enter the code to complete sign-in. After they sign in, call check-auth-status to confirm. After upgrading to shared-mailbox support, re-authenticate so Mail.*.Shared scopes appear on the token.",
inputSchema: {
type: "object",
properties: {
force: {
anyOf: [{ type: "boolean" }, { type: "string" }],
description: "Force re-authentication even if already authenticated"
}
},
required: []
},
handler: handleAuthenticate
},
{
name: "check-auth-status",
description: "Check the current authentication status with Microsoft Graph API, including whether shared-mailbox scopes are present on the access token",
inputSchema: { type: "object", properties: {}, required: [] },
handler: handleCheckAuthStatus
}
];
module.exports = { authTools, handleAbout, handleAuthenticate, handleCheckAuthStatus };