UNPKG

@aashari/mcp-server-aws-sso

Version:

Node.js/TypeScript MCP server for AWS Single Sign-On (SSO). Enables AI systems (LLMs) with tools to initiate SSO login (device auth flow), list accounts/roles, and securely execute AWS CLI commands using temporary credentials. Streamlines AI interaction w

543 lines (542 loc) 20.9 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.saveCredentials = void 0; exports.getCachedSsoToken = getCachedSsoToken; exports.saveSsoToken = saveSsoToken; exports.getCachedCredentials = getCachedCredentials; exports.saveCachedCredentials = saveCachedCredentials; exports.cacheDeviceAuthorizationInfo = cacheDeviceAuthorizationInfo; exports.getCachedDeviceAuthorizationInfo = getCachedDeviceAuthorizationInfo; exports.clearDeviceAuthorizationInfo = clearDeviceAuthorizationInfo; exports.clearAllCachedData = clearAllCachedData; exports.getCachedAccounts = getCachedAccounts; exports.saveAccounts = saveAccounts; exports.getCachedAccountRoles = getCachedAccountRoles; exports.saveAccountRoles = saveAccountRoles; exports.clearSsoToken = clearSsoToken; exports.getAccountRolesFromCache = getAccountRolesFromCache; exports.saveMcpAwsSsoCache = saveMcpAwsSsoCache; exports.saveAccountRolesToCache = saveAccountRolesToCache; const fs = __importStar(require("fs/promises")); const fsSync = __importStar(require("fs")); const path = __importStar(require("path")); const os = __importStar(require("os")); const logger_util_js_1 = require("./logger.util.js"); const constants_util_js_1 = require("./constants.util.js"); // Define the cache directory for MCP server const HOME_DIR = os.homedir(); const CACHE_DIR = path.join(HOME_DIR, '.mcp-server', constants_util_js_1.CLI_NAME); const TOKEN_FILE = path.join(CACHE_DIR, 'token.json'); // Constants for the MCP cache location const MCP_DATA_DIR = path.join(HOME_DIR, '.mcp', 'data'); const MCP_AWS_SSO_CACHE_FILE = path.join(MCP_DATA_DIR, `${constants_util_js_1.CLI_NAME}.json`); /** * Ensure the cache directory exists */ async function ensureCacheDir() { const methodLogger = logger_util_js_1.Logger.forContext('utils/aws.sso.cache.util.ts', 'ensureCacheDir'); methodLogger.debug('Ensuring cache directory exists'); try { // Make sure the cache directory exists if (!fsSync.existsSync(CACHE_DIR)) { methodLogger.debug(`Cache directory ${CACHE_DIR} does not exist, creating...`); fsSync.mkdirSync(CACHE_DIR, { recursive: true }); methodLogger.debug(`Cache directory created: ${CACHE_DIR}`); } } catch (error) { methodLogger.error('Error ensuring cache directory exists', error); throw error; } } /** * Check if a file exists * @param filePath File path * @returns True if the file exists, false otherwise */ async function fileExists(filePath) { try { await fs.access(filePath, fsSync.constants.F_OK); return true; } catch { return false; } } /** * Get cached SSO token * @returns Cached SSO token or undefined if not found or expired */ async function getCachedSsoToken() { const methodLogger = logger_util_js_1.Logger.forContext('utils/aws.sso.cache.util.ts', 'getCachedSsoToken'); methodLogger.debug('Getting cached SSO token'); try { // Check if token file exists if (!(await fileExists(TOKEN_FILE))) { methodLogger.debug('No token file found'); return undefined; } // Read token from file const tokenContent = await fs.readFile(TOKEN_FILE, 'utf8'); const token = JSON.parse(tokenContent); // Check if token is expired const now = Math.floor(Date.now() / 1000); if (token.expiresAt <= now) { methodLogger.debug('Token is expired'); return undefined; } // Token is valid methodLogger.debug('Found valid token, expires in', { expiresIn: token.expiresAt - now, expiresAt: new Date(token.expiresAt * 1000).toISOString(), }); return token; } catch (error) { methodLogger.error('Error getting token from cache', error); return undefined; } } /** * Save SSO token to cache * @param token SSO token to save */ async function saveSsoToken(token) { const methodLogger = logger_util_js_1.Logger.forContext('utils/aws.sso.cache.util.ts', 'saveSsoToken'); methodLogger.debug('Saving SSO token to cache'); try { // Ensure cache directory exists await ensureCacheDir(); // Debug log - only log part of token for security methodLogger.debug('Token details before saving:', { accessTokenLength: token.accessToken?.length || 0, accessTokenFirst10Chars: token.accessToken?.substring(0, 10) || 'none', expiresAt: token.expiresAt, region: token.region, }); // Save token to file await fs.writeFile(TOKEN_FILE, JSON.stringify(token, null, 2), 'utf8'); methodLogger.debug('Token saved to cache'); } catch (error) { methodLogger.error('Error saving token to cache', error); throw error; } } /** * Get cached AWS credentials for account and role * @param accountId AWS account ID * @param roleName AWS role name * @returns AWS credentials or undefined if not found */ async function getCachedCredentials(accountId, roleName) { const methodLogger = logger_util_js_1.Logger.forContext('utils/aws.sso.cache.util.ts', 'getCachedCredentials'); methodLogger.debug('Getting cached credentials', { accountId, roleName }); try { // Generate credentials file path const key = `${accountId}_${roleName}`; const credentialsFile = path.join(CACHE_DIR, `${key}.json`); // Check if credentials file exists if (!(await fileExists(credentialsFile))) { methodLogger.debug('No credentials file found'); return undefined; } // Read credentials file const data = await fs.readFile(credentialsFile, 'utf8'); const credentials = JSON.parse(data); // Ensure expiration is a Date if (typeof credentials.expiration === 'string') { credentials.expiration = new Date(credentials.expiration); } methodLogger.debug('Retrieved credentials from cache', { accountId, roleName, expiration: credentials.expiration, }); return credentials; } catch (error) { methodLogger.error('Error getting cached credentials', error); return undefined; } } /** * Save AWS credentials to cache * @param accountId AWS account ID * @param roleName AWS role name * @param credentials AWS credentials to save */ async function saveCachedCredentials(accountId, roleName, credentials) { const methodLogger = logger_util_js_1.Logger.forContext('utils/aws.sso.cache.util.ts', 'saveCachedCredentials'); methodLogger.debug('Saving credentials to cache', { accountId, roleName }); try { // Ensure cache directory exists await ensureCacheDir(); // Generate credentials file path const key = `${accountId}_${roleName}`; const credentialsFile = path.join(CACHE_DIR, `${key}.json`); // Save credentials to file await fs.writeFile(credentialsFile, JSON.stringify(credentials, null, 2), 'utf8'); methodLogger.debug('Credentials saved to cache'); } catch (error) { methodLogger.error('Error saving credentials to cache', error); throw error; } } /** * Cache device authorization info * @param info Device authorization info to cache */ async function cacheDeviceAuthorizationInfo(info) { const methodLogger = logger_util_js_1.Logger.forContext('utils/aws.sso.cache.util.ts', 'cacheDeviceAuthorizationInfo'); methodLogger.debug('Caching device authorization info'); try { // Ensure cache directory exists await ensureCacheDir(); // Save device authorization info to file const deviceAuthFile = path.join(CACHE_DIR, 'device-auth.json'); await fs.writeFile(deviceAuthFile, JSON.stringify(info, null, 2), 'utf8'); methodLogger.debug('Device authorization info cached'); } catch (error) { methodLogger.error('Error caching device authorization info', error); throw error; } } /** * Get cached device authorization info * @returns Device authorization info from cache or undefined if not found */ async function getCachedDeviceAuthorizationInfo() { const methodLogger = logger_util_js_1.Logger.forContext('utils/aws.sso.cache.util.ts', 'getCachedDeviceAuthorizationInfo'); methodLogger.debug('Getting cached device authorization info'); try { const deviceAuthFile = path.join(CACHE_DIR, 'device-auth.json'); // Check if device auth file exists if (!(await fileExists(deviceAuthFile))) { methodLogger.debug('No device authorization info found in cache'); return undefined; } // Read device auth file const data = await fs.readFile(deviceAuthFile, 'utf8'); const deviceInfo = JSON.parse(data); methodLogger.debug('Retrieved device authorization info from cache'); return deviceInfo; } catch (error) { methodLogger.error('Error getting cached device authorization info', error); return undefined; } } /** * Clear device authorization info from cache * @returns Promise that resolves when the operation completes */ async function clearDeviceAuthorizationInfo() { const methodLogger = logger_util_js_1.Logger.forContext('utils/aws.sso.cache.util.ts', 'clearDeviceAuthorizationInfo'); methodLogger.debug('Clearing device authorization info'); try { const deviceAuthFile = path.join(CACHE_DIR, 'device-auth.json'); // Check if device auth file exists if (await fileExists(deviceAuthFile)) { // Delete device auth file await fs.unlink(deviceAuthFile); methodLogger.debug('Device authorization info cleared from cache'); } else { methodLogger.debug('No device authorization info found to clear'); } } catch (error) { methodLogger.error('Error clearing device authorization info', error); // Don't throw error to ensure other operations can continue } } /** * Clear all cached data (tokens, credentials, etc.) * @returns Promise that resolves when the operation completes */ async function clearAllCachedData() { const methodLogger = logger_util_js_1.Logger.forContext('utils/aws.sso.cache.util.ts', 'clearAllCachedData'); methodLogger.debug('Clearing all cached data'); try { // Ensure cache directory exists await ensureCacheDir(); // Get all files in cache directory const files = await fs.readdir(CACHE_DIR); // Delete each file for (const file of files) { const filePath = path.join(CACHE_DIR, file); await fs.unlink(filePath); } methodLogger.debug('All cached data cleared successfully'); } catch (error) { methodLogger.error('Error clearing all cached data', error); // Don't throw error to ensure other operations can continue } } /** * Get cached AWS accounts * @returns List of AWS accounts or empty array if none found */ async function getCachedAccounts() { const methodLogger = logger_util_js_1.Logger.forContext('utils/aws.sso.cache.util.ts', 'getCachedAccounts'); methodLogger.debug('Getting cached AWS accounts'); try { // Generate accounts file path const accountsFile = path.join(CACHE_DIR, 'accounts.json'); // Check if accounts file exists if (!(await fileExists(accountsFile))) { methodLogger.debug('No accounts file found'); return []; } // Read accounts file const data = await fs.readFile(accountsFile, 'utf8'); const accounts = JSON.parse(data); methodLogger.debug(`Retrieved ${accounts.length} accounts from cache`); return accounts; } catch (error) { methodLogger.error('Error getting cached accounts', error); return []; } } /** * Save AWS accounts to cache * @param accounts List of AWS accounts to save */ async function saveAccounts(accounts) { const methodLogger = logger_util_js_1.Logger.forContext('utils/aws.sso.cache.util.ts', 'saveAccounts'); methodLogger.debug(`Saving ${accounts.length} accounts to cache`); try { // Ensure cache directory exists await ensureCacheDir(); // Save accounts to file const accountsFile = path.join(CACHE_DIR, 'accounts.json'); await fs.writeFile(accountsFile, JSON.stringify(accounts, null, 2), 'utf8'); methodLogger.debug('Accounts saved to cache'); } catch (error) { methodLogger.error('Error saving accounts to cache', error); throw error; } } /** * Get cached roles for an AWS account * @param accountId AWS account ID * @returns List of roles or empty array if none found */ async function getCachedAccountRoles(accountId) { const methodLogger = logger_util_js_1.Logger.forContext('utils/aws.sso.cache.util.ts', 'getCachedAccountRoles'); methodLogger.debug('Getting cached account roles', { accountId }); try { // Generate roles file path const rolesFile = path.join(CACHE_DIR, `roles_${accountId}.json`); // Check if roles file exists if (!(await fileExists(rolesFile))) { methodLogger.debug('No roles file found for account', { accountId, }); return []; } // Read roles file const data = await fs.readFile(rolesFile, 'utf8'); const roles = JSON.parse(data); methodLogger.debug(`Retrieved ${roles.length} roles for account`, { accountId, }); return roles; } catch (error) { methodLogger.error('Error getting cached account roles', error); return []; } } /** * Save roles for an AWS account to cache * @param account AWS account * @param roles List of roles to save */ async function saveAccountRoles(account, roles) { const methodLogger = logger_util_js_1.Logger.forContext('utils/aws.sso.cache.util.ts', 'saveAccountRoles'); methodLogger.debug(`Saving ${roles.length} roles for account`, { accountId: account.accountId, }); try { // Ensure cache directory exists await ensureCacheDir(); // Save roles to file const rolesFile = path.join(CACHE_DIR, `roles_${account.accountId}.json`); await fs.writeFile(rolesFile, JSON.stringify(roles, null, 2), 'utf8'); methodLogger.debug('Account roles saved to cache'); } catch (error) { methodLogger.error('Error saving account roles to cache', error); throw error; } } /** * Alias for saveCachedCredentials to maintain backward compatibility */ exports.saveCredentials = saveCachedCredentials; /** * Clear the cached SSO token */ async function clearSsoToken() { const methodLogger = logger_util_js_1.Logger.forContext('utils/aws.sso.cache.util.ts', 'clearSsoToken'); methodLogger.debug('Clearing cached SSO token'); try { // Check if token file exists if (await fileExists(TOKEN_FILE)) { // Delete the token file await fs.unlink(TOKEN_FILE); methodLogger.debug('SSO token cleared from cache'); } else { methodLogger.debug('No token file found to clear'); } } catch (error) { methodLogger.error('Error clearing SSO token', error); throw error; } } /** * Read the MCP AWS SSO cache file * @returns Cache data object or empty object if not found */ async function readMcpAwsSsoCache() { const logger = logger_util_js_1.Logger.forContext('utils/aws.sso.cache.util.ts', 'readMcpAwsSsoCache'); logger.debug('Reading MCP AWS SSO cache file from', { path: MCP_AWS_SSO_CACHE_FILE, }); try { // Check if the cache file exists if (await fileExists(MCP_AWS_SSO_CACHE_FILE)) { const content = await fs.readFile(MCP_AWS_SSO_CACHE_FILE, 'utf8'); const data = JSON.parse(content); logger.debug('Successfully read MCP AWS SSO cache file'); return data; } // If file doesn't exist, return empty object logger.debug('MCP AWS SSO cache file not found, returning empty object'); return {}; } catch (error) { logger.error('Error reading MCP AWS SSO cache file', error); return {}; } } /** * Gets account roles from the cache file * @returns Array of account roles data */ async function getAccountRolesFromCache() { const logger = logger_util_js_1.Logger.forContext('utils/aws.sso.cache.util.ts', 'getAccountRolesFromCache'); logger.debug('Getting account roles from cache'); try { const cacheData = await readMcpAwsSsoCache(); if (!cacheData.accountRoles) { logger.debug('No account roles found in cache'); return []; } logger.debug(`Found ${cacheData.accountRoles.length} accounts in cache`); return cacheData.accountRoles; } catch (error) { logger.error('Error getting account roles from cache', error); return []; } } /** * Save data to the MCP AWS SSO cache file * @param data The data to save */ async function saveMcpAwsSsoCache(data) { const logger = logger_util_js_1.Logger.forContext('utils/aws.sso.cache.util.ts', 'saveMcpAwsSsoCache'); logger.debug('Saving to MCP AWS SSO cache file'); try { // Make sure the MCP data directory exists if (!fsSync.existsSync(MCP_DATA_DIR)) { logger.debug(`Creating MCP data directory: ${MCP_DATA_DIR}`); fsSync.mkdirSync(MCP_DATA_DIR, { recursive: true }); } // Write to the cache file await fs.writeFile(MCP_AWS_SSO_CACHE_FILE, JSON.stringify(data, null, 2), 'utf8'); logger.debug('Successfully saved MCP AWS SSO cache file'); } catch (error) { logger.error('Error saving MCP AWS SSO cache file', error); throw error; } } /** * Save account roles to the MCP cache file * @param accountsWithRoles Array of accounts with roles */ async function saveAccountRolesToCache(accountsWithRoles) { const logger = logger_util_js_1.Logger.forContext('utils/aws.sso.cache.util.ts', 'saveAccountRolesToCache'); logger.debug(`Saving ${accountsWithRoles.length} accounts with roles to MCP cache`); try { // Get existing cache data const existingData = await readMcpAwsSsoCache(); // Format the data correctly const accountRolesData = accountsWithRoles.map((account) => ({ account: { accountId: account.accountId, accountName: account.accountName, emailAddress: account.emailAddress || '', }, roles: account.roles, })); // Update the cache data const updatedData = { ...existingData, accountRoles: accountRolesData, // Update the timestamp lastAuth: Date.now(), }; // Save the updated data await saveMcpAwsSsoCache(updatedData); logger.debug('Successfully saved account roles to MCP cache'); } catch (error) { logger.error('Error saving account roles to MCP cache', error); // Don't throw the error to avoid breaking the flow } }