UNPKG

msteams-mcp-server

Version:

Microsoft Teams MCP Server - Complete Teams integration for Claude Desktop and MCP clients with secure OAuth2 authentication and comprehensive team management

397 lines (353 loc) 14.2 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(), 'msteams-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('Microsoft Teams 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 login = false; let logout = false; let verifyLogin = false; let checkPermissions = false; let adminConsentHelp = false; let azureSetup = false; let clientId = null; let tenantId = null; let clientSecret = null; let userId = null; let redirectUri = null; // Detect if we're running under an MCP context (Claude/SIYA/ChatGPT/etc.) const isMcpContext = !process.stdin.isTTY || process.env.npm_execpath?.includes('npx') || 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 === '--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 === '--check-permissions') { checkPermissions = true; writeDebug('Check permissions mode enabled'); } else if (arg === '--admin-consent-help') { adminConsentHelp = true; writeDebug('Admin consent help mode enabled'); } else if (arg === '--azure-setup') { azureSetup = true; writeDebug('Azure setup mode enabled'); } else if (arg === '--client-id') { if (i + 1 < args.length) { clientId = args[i + 1]; i++; // Skip the next argument since it's the value writeDebug(`Client ID set via argument: ${clientId}`); } 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 + 1]; i++; // Skip the next argument since it's the value writeDebug(`Tenant ID set via argument: ${tenantId}`); } 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 + 1]; i++; // Skip the next argument since it's the value writeDebug(`Client Secret set via argument: [REDACTED]`); } else { console.error('Error: --client-secret requires a value'); process.exit(1); } } else if (arg === '--user-id') { if (i + 1 < args.length) { userId = args[i + 1]; i++; // Skip the next argument since it's the value writeDebug(`User ID set via argument: ${userId}`); } else { console.error('Error: --user-id requires a value'); process.exit(1); } } else if (arg === '--redirect-uri') { if (i + 1 < args.length) { redirectUri = args[i + 1]; i++; // Skip the next argument since it's the value writeDebug(`Redirect URI set via argument: ${redirectUri}`); } else { console.error('Error: --redirect-uri requires a value'); process.exit(1); } } else if (arg === '--help' || arg === '-h') { console.log(` Microsoft Teams MCP Server - Microsoft Teams Integration for Claude/SIYA Desktop Usage: npx msteams-mcp-server [options] Options: --setup-auth Set up Microsoft Teams API credentials --reset-auth Clear stored authentication tokens --login Login to Microsoft Teams --logout Logout from Microsoft Teams --verify-login Verify login to Microsoft Teams --check-permissions Check current permission level and available scopes --admin-consent-help Get help with admin consent requirements --azure-setup Get step-by-step Azure app registration guide --client-id <id> Azure app client ID (alternative to TEAMS_CLIENT_ID env var) --tenant-id <id> Azure tenant ID (alternative to TEAMS_TENANT_ID env var) --client-secret <secret> Azure app client secret (alternative to TEAMS_CLIENT_SECRET env var) --redirect-uri <uri> OAuth redirect URI (alternative to TEAMS_REDIRECT_URI env var) --user-id <id> User identifier for OAuth state parameter (alternative to USER_ID env var) --debug Enable debug output --non-interactive, -n Run in non-interactive mode (no prompt) --help, -h Show this help message Setup: 1. Run: npx msteams-mcp-server --login 2. Follow the OAuth redirect authentication flow 3. Run: npx msteams-mcp-server to start the server Authentication Setup: The server supports multiple methods for providing Microsoft Teams API credentials: 1. OAuth Redirect Flow (Recommended - No Setup Required): - Run: npx msteams-mcp-server --login - Visit the provided OAuth URL in your browser - Sign in with your Microsoft account - Uses built-in Microsoft Graph Command Line Tools app 2. Environment Variables: - TEAMS_CLIENT_ID: Your Azure app client ID - TEAMS_CLIENT_SECRET: Your Azure app client secret (optional) - TEAMS_TENANT_ID: Your Azure tenant ID (or "common") - TEAMS_REDIRECT_URI: OAuth redirect URI (optional) 3. Interactive Setup: - Run: npx msteams-mcp-server --setup-auth - Saves credentials to ~/.msteams-mcp/credentials.json Azure App Registration (Optional): For advanced scenarios, you can create your own Azure app: 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: - Team.ReadBasic.All - TeamMember.ReadWrite.All - ChannelMessage.Send - Chat.ReadWrite - ChatMessage.Send - User.Read Examples: npx msteams-mcp-server --login # Authenticate with OAuth redirect npx msteams-mcp-server # Start the server npx msteams-mcp-server --reset-auth # Clear auth tokens npx msteams-mcp-server --verify-login # Check authentication status SIYA Desktop Configuration: Add this to your SIYA configuration: { "mcpServers": { "teams": { "command": "npx", "args": ["msteams-mcp-server"] } } } Available Tools (12 Total): - authenticate: Login/logout and status management - manage_teams: List, search, create, and manage teams - manage_channels: List, search, and create channels - send_message: Send messages to team channels - manage_messages: Get and search channel messages - send_direct_message: Send direct/private messages to users - get_direct_messages: Retrieve direct message conversations - manage_reactions: Add/remove emoji reactions to messages - manage_replies: Reply to messages and get threaded conversations - manage_files: Upload, download, and list files in teams and channels - manage_group_chats: Create group chats, manage members, and chat settings - manage_meetings: Create online meetings, manage calendar events - manage_presence: Get and set user presence/status information - manage_members: Add and remove team members - search_users: Find users in your organization For detailed usage examples and troubleshooting, see the README.md file. `); 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}`); // Build command arguments const serverArgs = []; if (debug) serverArgs.push('--debug'); if (nonInteractive) serverArgs.push('--non-interactive'); if (setupAuth) serverArgs.push('--setup-auth'); if (resetAuth) serverArgs.push('--reset-auth'); if (login) serverArgs.push('--login'); if (logout) serverArgs.push('--logout'); if (verifyLogin) serverArgs.push('--verify-login'); if (checkPermissions) serverArgs.push('--check-permissions'); if (adminConsentHelp) serverArgs.push('--admin-consent-help'); if (azureSetup) serverArgs.push('--azure-setup'); if (clientId) serverArgs.push('--client-id', clientId); if (tenantId) serverArgs.push('--tenant-id', tenantId); if (clientSecret) serverArgs.push('--client-secret', clientSecret); if (redirectUri) serverArgs.push('--redirect-uri', redirectUri); if (userId) serverArgs.push('--user-id', userId); writeDebug(`Server arguments: ${serverArgs.join(' ')}`); if (isMcpContext && !login && !logout && !verifyLogin && !resetAuth && !setupAuth && !checkPermissions && !adminConsentHelp && !azureSetup) { // Direct execution for MCP contexts (Claude, SIYA, etc.) writeDebug('Running in MCP context - direct execution'); // Set environment variables for direct execution since we can't pass CLI args if (clientId) { process.env.TEAMS_CLIENT_ID = clientId; writeDebug(`Set TEAMS_CLIENT_ID for direct execution: ${clientId}`); } if (tenantId) { process.env.TEAMS_TENANT_ID = tenantId; writeDebug(`Set TEAMS_TENANT_ID for direct execution: ${tenantId}`); } if (clientSecret) { process.env.TEAMS_CLIENT_SECRET = clientSecret; writeDebug(`Set TEAMS_CLIENT_SECRET for direct execution: [REDACTED]`); } if (redirectUri) { process.env.TEAMS_REDIRECT_URI = redirectUri; writeDebug(`Set TEAMS_REDIRECT_URI for direct execution: ${redirectUri}`); } if (userId) { process.env.USER_ID = userId; writeDebug(`Set USER_ID for direct execution: ${userId}`); } // Import and run the server directly to avoid stdio issues import(serverPath).catch(err => { writeDebug(`Error importing server: ${err.message}`); console.error('Failed to start server:', err.message); console.error('Try running: npm run build'); process.exit(1); }); } else { // Spawn process for interactive mode or CLI commands writeDebug('Running in interactive mode - spawning process'); const serverProcess = spawn(process.execPath, [serverPath, ...serverArgs], { stdio: debug ? 'inherit' : ['ignore', 'inherit', 'inherit'] }); serverProcess.on('error', (err) => { writeDebug(`Server process error: ${err.message}`); console.error('Failed to start server:', err.message); process.exit(1); }); serverProcess.on('close', (code, signal) => { writeDebug(`Server process closed with code ${code} and signal ${signal}`); if (code !== 0 && code !== null) { console.error(`Server exited with code ${code}`); process.exit(code); } }); // Handle termination signals ['SIGTERM', 'SIGINT'].forEach(signal => { process.on(signal, () => { writeDebug(`Received ${signal}, terminating server process`); serverProcess.kill(signal); }); }); } } // Handle uncaught exceptions process.on('uncaughtException', (err) => { writeDebug(`Uncaught exception: ${err.message}`); console.error('Uncaught exception:', err.message); process.exit(1); }); process.on('unhandledRejection', (reason, promise) => { writeDebug(`Unhandled rejection: ${reason}`); console.error('Unhandled rejection:', reason); process.exit(1); }); // Start the server startServer();