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
JavaScript
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();