UNPKG

ms365-mcp-server

Version:

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

261 lines (226 loc) 8.48 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'; // 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 === '--help' || arg === '-h') { console.log(` MS365 MCP Server - Microsoft 365 Integration for Claude/SIYA Desktop Usage: npx ms365-mcp-server [options] Options: --setup-auth Set up MS365 API credentials --reset-auth Clear stored authentication tokens --multi-user Enable multi-user authentication mode --login Login to MS365 --logout Logout from MS365 --verify-login Verify login to MS365 --server-url URL Set the server URL (default: http://localhost:55000) --debug Enable debug output --non-interactive, -n Run in non-interactive mode (no prompt) --help, -h Show this help message Setup: 1. Run: npx ms365-mcp-server --setup-auth 2. Follow the instructions to set up Azure App Registration 3. Run: npx ms365-mcp-server to start the server Authentication Setup: The server supports two methods for providing MS365 API credentials: 1. Environment Variables (Recommended): - MS365_CLIENT_ID: Your Azure app client ID - MS365_CLIENT_SECRET: Your Azure app client secret - MS365_TENANT_ID: Your Azure tenant ID (or "common") - MS365_REDIRECT_URI: OAuth redirect URI (optional) - SERVER_URL: Server URL for attachments (optional) 2. Credentials File: - Run --setup-auth for interactive setup - Saves credentials to ~/.ms365-mcp/credentials.json Azure App Registration: 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:44001/oauth2callback 5. Grant required API permissions for Microsoft Graph: - Mail.ReadWrite - Mail.Send - MailboxSettings.Read - Contacts.Read - User.Read Note: Device code flow (--login) doesn't require Azure app registration! Examples: npx ms365-mcp-server --setup-auth # Set up authentication npx ms365-mcp-server # Start the server npx ms365-mcp-server --multi-user # Start in multi-user mode npx ms365-mcp-server --reset-auth # Clear auth tokens npx ms365-mcp-server --server-url https://your-domain.com # Set custom server URL `); 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(' ')}`); // Start the server process const serverProcess = spawn(process.execPath, [serverPath, ...serverArgs], { stdio: 'inherit', env: { ...process.env, // Ensure environment variables are passed through NODE_PATH: process.env.NODE_PATH || '', SERVER_URL: serverUrl } }); // 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();