UNPKG

ms365-mcp-server

Version:

Microsoft 365 MCP Server for managing Microsoft 365 email through natural language interactions with full OAuth2 authentication support

311 lines (274 loc) 10.5 kB
#!/usr/bin/env node import path from 'path'; import { fileURLToPath } from 'url'; import { spawn } from 'child_process'; import fs from 'fs'; import os from 'os'; // Debug info - write to a debug file const debugFile = path.join(os.tmpdir(), 'ms365-mcp-debug.log'); const writeDebug = (message) => { try { fs.appendFileSync(debugFile, `${new Date().toISOString()} - ${message}\n`); } catch (err) { // Silently fail if we can't write to the debug file } }; // Start debugging writeDebug('MS365 MCP CLI script started'); writeDebug(`Node version: ${process.version}`); writeDebug(`Platform: ${process.platform}`); writeDebug(`CLI Arguments: ${process.argv.join(' ')}`); writeDebug(`Is stdin a TTY: ${process.stdin.isTTY}`); writeDebug(`Is stdout a TTY: ${process.stdout.isTTY}`); writeDebug(`Process PID: ${process.pid}`); writeDebug(`Executable path: ${process.execPath}`); writeDebug(`Current directory: ${process.cwd()}`); // Print debug file location to stderr (not stdout) console.error(`Debug log: ${debugFile}`); const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const packageRoot = path.resolve(__dirname, '..'); writeDebug(`__filename: ${__filename}`); writeDebug(`__dirname: ${__dirname}`); writeDebug(`packageRoot: ${packageRoot}`); // Check if bin/cli.js is executable try { const stats = fs.statSync(__filename); const isExecutable = !!(stats.mode & fs.constants.S_IXUSR); writeDebug(`Is CLI executable: ${isExecutable}`); // Make it executable if it's not if (!isExecutable) { fs.chmodSync(__filename, '755'); writeDebug('Made CLI executable'); } } catch (err) { writeDebug(`Error checking/setting executable: ${err.message}`); } // Parse command line arguments const args = process.argv.slice(2); let debug = false; let nonInteractive = false; let setupAuth = false; let resetAuth = false; let multiUser = false; let login = false; let logout = false; let verifyLogin = false; let serverUrl = process.env.SERVER_URL || 'http://localhost:55000'; let clientId = process.env.OUTLOOK_CLIENT_ID || ''; let tenantId = process.env.OUTLOOK_TENANT_ID || ''; let clientSecret = process.env.OUTLOOK_CLIENT_SECRET || ''; let redirectUri = process.env.OUTLOOK_REDIRECT_URI || ''; // Detect if we're running under an MCP context (Claude/SIYA/ChatGPT/etc.) const isMcpContext = !process.stdin.isTTY || process.env.npm_execpath?.includes('npx') || process.env.CLAUDE_API_KEY || args.includes('--non-interactive') || args.includes('-n'); writeDebug(`Detected MCP context: ${isMcpContext}`); if (isMcpContext) { nonInteractive = true; writeDebug('Setting non-interactive mode due to MCP context detection'); } // Process command line arguments for (let i = 0; i < args.length; i++) { const arg = args[i]; if (arg === '--debug') { debug = true; writeDebug('Debug mode enabled'); } else if (arg === '--non-interactive' || arg === '-n') { nonInteractive = true; writeDebug('Non-interactive mode enabled via flag'); } else if (arg === '--setup-auth') { setupAuth = true; writeDebug('Setup auth mode enabled'); } else if (arg === '--reset-auth') { resetAuth = true; writeDebug('Reset auth mode enabled'); } else if (arg === '--multi-user') { multiUser = true; writeDebug('Multi-user mode enabled'); } else if (arg === '--login') { login = true; writeDebug('Login mode enabled'); } else if (arg === '--logout') { logout = true; writeDebug('Logout mode enabled'); } else if (arg === '--verify-login') { verifyLogin = true; writeDebug('Verify login mode enabled'); } else if (arg === '--server-url') { if (i + 1 < args.length) { serverUrl = args[++i]; writeDebug(`Server URL set to: ${serverUrl}`); } else { console.error('Error: --server-url requires a value'); process.exit(1); } } else if (arg === '--client-id') { if (i + 1 < args.length) { clientId = args[++i]; writeDebug(`Client ID set`); } else { console.error('Error: --client-id requires a value'); process.exit(1); } } else if (arg === '--tenant-id') { if (i + 1 < args.length) { tenantId = args[++i]; writeDebug(`Tenant ID set`); } else { console.error('Error: --tenant-id requires a value'); process.exit(1); } } else if (arg === '--client-secret') { if (i + 1 < args.length) { clientSecret = args[++i]; writeDebug(`Client secret set`); } else { console.error('Error: --client-secret requires a value'); process.exit(1); } } else if (arg === '--redirect-uri') { if (i + 1 < args.length) { redirectUri = args[++i]; writeDebug(`Redirect URI set to: ${redirectUri}`); } else { console.error('Error: --redirect-uri requires a value'); process.exit(1); } } else if (arg === '--help' || arg === '-h') { console.log(` MS365 MCP Server - Microsoft 365 Integration for Claude/SIYA Desktop Usage: npx ms365-mcp-server [options] Options: --client-id ID Azure App Client ID --tenant-id ID Azure Tenant ID (or "common") --client-secret SECRET Azure App Client Secret (optional) --redirect-uri URI OAuth redirect URI (optional) --login Login to MS365 (opens browser for OAuth) --logout Logout from MS365 --verify-login Verify login to MS365 --server-url URL Set the server URL (default: http://localhost:55000) --setup-auth Interactive credential setup --reset-auth Clear stored authentication tokens --multi-user Enable multi-user authentication mode --debug Enable debug output --non-interactive, -n Run in non-interactive mode (no prompt) --help, -h Show this help message Quick Start: 1. Run: npx ms365-mcp-server --login 2. Complete authentication in browser 3. Run: npx ms365-mcp-server to start the server Authentication (OAuth2 Redirect Flow): The server uses OAuth2 redirect flow with a local callback server. No Azure app registration required - uses built-in Microsoft credentials! 1. Run --login to open browser for authentication 2. Sign in with your Microsoft account 3. Authorization redirects back to local server (port 44005) 4. Tokens saved to ~/.outlook-mcp/ Environment Variables (Optional - for custom Azure app): - OUTLOOK_CLIENT_ID: Your Azure app client ID - OUTLOOK_TENANT_ID: Your Azure tenant ID (or "common") - OUTLOOK_CLIENT_SECRET: Your Azure app client secret (optional) - OUTLOOK_REDIRECT_URI: OAuth redirect URI (optional) - SERVER_URL: Server URL for attachments (optional) Custom Azure App Registration (Optional): 1. Go to https://portal.azure.com 2. Navigate to Azure Active Directory > App registrations 3. Click "New registration" 4. Set redirect URI to: http://localhost:44005/oauth2callback 5. Grant required API permissions for Microsoft Graph: - Mail.ReadWrite - Mail.Send - MailboxSettings.Read - Contacts.Read - User.Read Examples: npx ms365-mcp-server --login # Login (opens browser) npx ms365-mcp-server # Start the server npx ms365-mcp-server --verify-login # Check auth status npx ms365-mcp-server --logout # Clear auth tokens npx ms365-mcp-server --multi-user # Start in multi-user mode `); process.exit(0); } } function startServer() { const serverPath = path.join(packageRoot, 'dist', 'index.js'); // Check if the compiled server exists if (!fs.existsSync(serverPath)) { console.error('Server not found. Building...'); // Try to build the project const buildProcess = spawn('npm', ['run', 'build'], { cwd: packageRoot, stdio: 'inherit' }); buildProcess.on('close', (code) => { if (code === 0) { startServerWithPath(serverPath); } else { console.error('Build failed. Please run "npm run build" manually.'); process.exit(1); } }); } else { startServerWithPath(serverPath); } } function startServerWithPath(serverPath) { writeDebug(`Starting server with path: ${serverPath}`); // Prepare arguments for the server const serverArgs = []; if (setupAuth) serverArgs.push('--setup-auth'); if (resetAuth) serverArgs.push('--reset-auth'); if (multiUser) serverArgs.push('--multi-user'); if (login) serverArgs.push('--login'); if (logout) serverArgs.push('--logout'); if (verifyLogin) serverArgs.push('--verify-login'); if (debug) serverArgs.push('--debug'); if (nonInteractive) serverArgs.push('--non-interactive'); if (serverUrl) serverArgs.push('--server-url', serverUrl); writeDebug(`Server arguments: ${serverArgs.join(' ')}`); // Build environment with credentials const serverEnv = { ...process.env, NODE_PATH: process.env.NODE_PATH || '', SERVER_URL: serverUrl }; // Add credentials to environment if provided via CLI if (clientId) serverEnv.OUTLOOK_CLIENT_ID = clientId; if (tenantId) serverEnv.OUTLOOK_TENANT_ID = tenantId; if (clientSecret) serverEnv.OUTLOOK_CLIENT_SECRET = clientSecret; if (redirectUri) serverEnv.OUTLOOK_REDIRECT_URI = redirectUri; writeDebug(`Starting server with credentials: clientId=${clientId ? 'set' : 'not set'}, tenantId=${tenantId ? 'set' : 'not set'}, clientSecret=${clientSecret ? 'set' : 'not set'}`); // Start the server process const serverProcess = spawn(process.execPath, [serverPath, ...serverArgs], { stdio: 'inherit', env: serverEnv }); // Handle server process events serverProcess.on('error', (err) => { writeDebug(`Server process error: ${err.message}`); console.error('Failed to start MS365 MCP server:', err.message); process.exit(1); }); serverProcess.on('close', (code, signal) => { writeDebug(`Server process closed with code ${code} and signal ${signal}`); if (code !== 0) { console.error(`MS365 MCP server exited with code ${code}`); } process.exit(code || 0); }); // Handle SIGINT and SIGTERM process.on('SIGINT', () => { writeDebug('Received SIGINT, terminating server process'); serverProcess.kill('SIGINT'); }); process.on('SIGTERM', () => { writeDebug('Received SIGTERM, terminating server process'); serverProcess.kill('SIGTERM'); }); } // Start the server startServer();