@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
159 lines (158 loc) • 6.7 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.executeCommand = executeCommand;
const logger_util_js_1 = require("../utils/logger.util.js");
// Import promisify and exec statically
const node_util_1 = require("node:util");
const node_child_process_1 = require("node:child_process");
const vendor_aws_sso_accounts_service_js_1 = require("./vendor.aws.sso.accounts.service.js");
const logger = logger_util_js_1.Logger.forContext('services/vendor.aws.sso.exec.service.ts');
// Create promisified version once
const exec = (0, node_util_1.promisify)(node_child_process_1.exec);
/**
* Execute AWS CLI command with temporary credentials
*
* Gets temporary credentials for the specified account and role, then executes
* the AWS CLI command with those credentials in the environment.
*
* @param {string} accountId - AWS account ID to get credentials for
* @param {string} roleName - AWS role name to assume via SSO
* @param {string} commandString - AWS CLI command as a single string
* @param {string} [region] - Optional AWS region override
* @param {boolean} [forceRefreshCredentials] - Force refresh credentials even if cached
* @returns {Promise<CommandExecutionResult>} Command execution result with stdout, stderr, and exit code
* @throws {Error} If credentials cannot be obtained or command execution fails
*/
async function executeCommand(accountId, roleName, commandString, region, forceRefreshCredentials) {
const methodLogger = logger.forMethod('executeCommand');
methodLogger.debug('Executing AWS CLI command', {
accountId,
roleName,
command: commandString,
region,
forceRefresh: !!forceRefreshCredentials,
});
// Validate parameters
if (!accountId || !roleName) {
throw new Error('Account ID and role name are required');
}
if (!commandString) {
throw new Error('Command is required');
}
try {
// Get credentials for the account and role
const credentials = await (0, vendor_aws_sso_accounts_service_js_1.getAwsCredentials)({
accountId,
roleName,
region,
forceRefresh: forceRefreshCredentials,
});
methodLogger.debug('Obtained temporary credentials', {
accountId,
roleName,
expiration: credentials.expiration.toISOString(),
});
// Set up environment variables for the command
const processEnv = { ...process.env };
// Add AWS credentials to the environment
processEnv.AWS_ACCESS_KEY_ID = credentials.accessKeyId;
processEnv.AWS_SECRET_ACCESS_KEY = credentials.secretAccessKey;
processEnv.AWS_SESSION_TOKEN = credentials.sessionToken;
// Fix PATH to prioritize working AWS CLI binary
// Prepend /usr/local/aws-cli to PATH so the working binary is found first
const currentPath = processEnv.PATH || '';
processEnv.PATH = `/usr/local/aws-cli:${currentPath}`;
methodLogger.debug('Updated PATH for AWS CLI execution', {
awsCliPath: '/usr/local/aws-cli',
pathPrefix: processEnv.PATH.split(':').slice(0, 3),
});
// Set region if provided
if (region) {
processEnv.AWS_REGION = region;
processEnv.AWS_DEFAULT_REGION = region;
}
// Execute the command
const result = await executeChildProcess(commandString, processEnv);
methodLogger.debug('Command execution completed', {
exitCode: result.exitCode,
stdoutBytes: result.stdout.length,
stderrBytes: result.stderr.length,
});
// Check for credential validation errors
if (result.exitCode !== 0 && !forceRefreshCredentials) {
const credentialErrorPatterns = [
'InvalidClientTokenId',
'security token.*invalid',
'InvalidToken',
'expired',
'credential.*verification',
];
const hasCredentialError = credentialErrorPatterns.some((pattern) => new RegExp(pattern, 'i').test(result.stderr));
if (hasCredentialError) {
methodLogger.debug('Detected credential error, retrying with force refresh', {
error: result.stderr.substring(0, 100), // Only log first 100 chars
});
// Retry with forced credential refresh
return executeCommand(accountId, roleName, commandString, region, true);
}
}
return result;
}
catch (error) {
methodLogger.error('Failed to execute command', error);
throw error;
}
}
/**
* Execute child process with the given command and arguments
*
* Helper function to spawn a child process and collect its output.
*
* @param {string} commandString - Command string to execute via shell
* @param {NodeJS.ProcessEnv} env - Environment variables for the process
* @returns {Promise<CommandExecutionResult>} Command execution result
*/
async function executeChildProcess(commandString, env) {
const methodLogger = logger.forMethod('executeChildProcess');
methodLogger.debug('Executing child process via shell', {
command: commandString,
});
try {
// Execute using exec, which uses a shell
const { stdout, stderr } = await exec(commandString, { env });
methodLogger.debug('Shell command completed successfully', {
stdoutLength: stdout.length,
stderrLength: stderr.length,
});
return {
stdout,
stderr,
exitCode: 0, // exec throws on non-zero exit code
};
}
catch (error) {
// Handle errors from exec (includes non-zero exit codes)
methodLogger.error('Shell command execution failed', { error });
let exitCode = 1;
let stdout = '';
let stderr = '';
if (typeof error === 'object' && error !== null) {
// Cast to check for common exec error properties
const execError = error;
exitCode = typeof execError.code === 'number' ? execError.code : 1;
stdout =
typeof execError.stdout === 'string' ? execError.stdout : '';
stderr =
typeof execError.stderr === 'string' ? execError.stderr : '';
}
else if (error instanceof Error) {
// Fallback for generic Errors
stderr = error.message;
}
return {
stdout: String(stdout),
stderr: String(stderr),
exitCode: Number(exitCode) || 1,
};
}
}
;