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

234 lines (233 loc) 9.24 kB
import fs from 'fs'; import path from 'path'; import os from 'os'; import { logger } from './api.js'; // Function to find the actual config directory function findConfigDir() { const possiblePaths = [ process.env.MSTEAMS_CONFIG_DIR, // Environment override (highest priority) path.join(os.homedir(), '.msteams-mcp'), // Default path '/home/siya/.msteams-mcp', // Fallback for server deployments path.join('/home', process.env.USER || 'siya', '.msteams-mcp') // Dynamic user fallback ].filter(Boolean); // Remove undefined values for (const configPath of possiblePaths) { if (fs.existsSync(configPath)) { logger.log(`Found config directory at: ${configPath}`); return configPath; } } // If no existing directory found, use the default const defaultPath = path.join(os.homedir(), '.msteams-mcp'); logger.log(`No existing config directory found, using default: ${defaultPath}`); return defaultPath; } const CONFIG_DIR = findConfigDir(); const TOKENS_FILE = path.join(CONFIG_DIR, 'token.json'); const CREDENTIALS_FILE = path.join(CONFIG_DIR, 'credentials.json'); /** * Secure credential storage utility for Microsoft Teams MCP Server */ export class CredentialStore { ensureConfigDir() { if (!fs.existsSync(CONFIG_DIR)) { try { fs.mkdirSync(CONFIG_DIR, { recursive: true }); logger.log(`Created config directory: ${CONFIG_DIR}`); // Set restrictive permissions on the config directory fs.chmodSync(CONFIG_DIR, '700'); // Owner read/write/execute only logger.log(`Set restrictive permissions on config directory`); } catch (error) { logger.error(`Failed to create or set permissions on config directory: ${error}`); throw new Error(`Failed to create config directory: ${error}`); } } else { logger.log(`Config directory already exists: ${CONFIG_DIR}`); } } /** * Store credentials securely */ async storeCredentials(credentials) { this.ensureConfigDir(); try { const credentialsData = JSON.stringify(credentials, null, 2); fs.writeFileSync(CREDENTIALS_FILE, credentialsData, { mode: 0o600 }); // Owner read/write only logger.log(`Credentials stored successfully to: ${CREDENTIALS_FILE}`); // Verify file was created if (fs.existsSync(CREDENTIALS_FILE)) { const stats = fs.statSync(CREDENTIALS_FILE); logger.log(`Credentials file size: ${stats.size} bytes`); } else { logger.error('Credentials file was not created despite successful write'); } } catch (error) { logger.error('Failed to store credentials:', error); logger.error(`Attempted to write to: ${CREDENTIALS_FILE}`); logger.error(`Config directory exists: ${fs.existsSync(CONFIG_DIR)}`); throw new Error('Failed to store credentials'); } } /** * Retrieve stored credentials */ async getCredentials() { try { if (!fs.existsSync(CREDENTIALS_FILE)) { return null; } const credentialsData = fs.readFileSync(CREDENTIALS_FILE, 'utf8'); const credentials = JSON.parse(credentialsData); logger.log('Credentials retrieved successfully'); return credentials; } catch (error) { logger.error('Failed to retrieve credentials:', error); return null; } } /** * Store authentication tokens securely */ async storeTokens(tokens) { this.ensureConfigDir(); try { const tokensData = JSON.stringify(tokens, null, 2); fs.writeFileSync(TOKENS_FILE, tokensData, { mode: 0o600 }); // Owner read/write only logger.log(`Tokens stored successfully to: ${TOKENS_FILE}`); // Verify file was created if (fs.existsSync(TOKENS_FILE)) { const stats = fs.statSync(TOKENS_FILE); logger.log(`Token file size: ${stats.size} bytes`); } else { logger.error('Token file was not created despite successful write'); } } catch (error) { logger.error('Failed to store tokens:', error); logger.error(`Attempted to write to: ${TOKENS_FILE}`); logger.error(`Config directory exists: ${fs.existsSync(CONFIG_DIR)}`); throw new Error('Failed to store tokens'); } } /** * Retrieve stored tokens */ async getTokens() { try { logger.log(`Looking for token file at: ${TOKENS_FILE}`); logger.log(`Home directory: ${os.homedir()}`); logger.log(`Config directory: ${CONFIG_DIR}`); if (!fs.existsSync(TOKENS_FILE)) { logger.log('Token file does not exist'); // Check if config directory exists if (!fs.existsSync(CONFIG_DIR)) { logger.error(`Config directory does not exist: ${CONFIG_DIR}`); } else { logger.log(`Config directory exists, listing contents:`); try { const files = fs.readdirSync(CONFIG_DIR); logger.log(`Files in config dir: ${files.join(', ')}`); } catch (listError) { logger.error(`Failed to list config directory: ${listError}`); } } return null; } const tokensData = fs.readFileSync(TOKENS_FILE, 'utf8'); if (!tokensData.trim()) { logger.warn('Token file is empty'); return null; } const tokens = JSON.parse(tokensData); // Validate token structure if (!tokens.accessToken) { logger.warn('Invalid token structure: missing accessToken'); return null; } // Log token status for debugging const currentTime = Date.now(); const isExpired = tokens.expiresOn && currentTime >= tokens.expiresOn; logger.log(`Token file exists, size: ${tokensData.length} chars`); logger.log(`Token expiry: ${tokens.expiresOn ? new Date(tokens.expiresOn).toISOString() : 'unknown'}`); logger.log(`Token expired: ${isExpired}`); logger.log(`Refresh token available: ${!!tokens.refreshToken}`); logger.log(`Account info available: ${!!tokens.account}`); // Return tokens even if expired - let caller decide what to do logger.log('Tokens retrieved successfully'); return tokens; } catch (error) { logger.error('Failed to retrieve tokens:', error); logger.error(`Token file path: ${TOKENS_FILE}`); logger.error(`Token file exists: ${fs.existsSync(TOKENS_FILE)}`); if (fs.existsSync(TOKENS_FILE)) { try { const stats = fs.statSync(TOKENS_FILE); logger.error(`Token file size: ${stats.size} bytes`); logger.error(`Token file modified: ${stats.mtime}`); } catch (statsError) { logger.error('Failed to get token file stats:', statsError); } } return null; } } /** * Clear all stored credentials and tokens */ async clearAll() { try { if (fs.existsSync(CREDENTIALS_FILE)) { fs.unlinkSync(CREDENTIALS_FILE); logger.log('Credentials cleared'); } if (fs.existsSync(TOKENS_FILE)) { fs.unlinkSync(TOKENS_FILE); logger.log('Tokens cleared'); } // Also clear MSAL cache const msalCacheFile = path.join(CONFIG_DIR, 'msal-cache.json'); if (fs.existsSync(msalCacheFile)) { fs.unlinkSync(msalCacheFile); logger.log('MSAL cache cleared'); } } catch (error) { logger.error('Failed to clear stored data:', error); throw new Error('Failed to clear stored data'); } } /** * Check if credentials are stored */ async hasCredentials() { return fs.existsSync(CREDENTIALS_FILE); } /** * Check if tokens are stored and valid */ async hasValidTokens() { const tokens = await this.getTokens(); return tokens !== null; } /** * Get storage information for debugging */ getStorageInfo() { return { credentialsPath: CREDENTIALS_FILE, tokensPath: TOKENS_FILE, configDir: CONFIG_DIR }; } } // Export singleton instance export const credentialStore = new CredentialStore();