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
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(), '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();