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