UNPKG

@chinchillaenterprises/mcp-amplify

Version:

AWS Amplify MCP server with intelligent deployment automation, specialized logging suite, and recursive resource discovery

1,277 lines 111 kB
#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { ListToolsRequestSchema, CallToolRequestSchema, McpError, ErrorCode, } from "@modelcontextprotocol/sdk/types.js"; // AWS SDK Clients import { AmplifyClient, ListAppsCommand, ListBranchesCommand, ListJobsCommand, GetJobCommand, GetAppCommand } from "@aws-sdk/client-amplify"; import { DynamoDBClient, ListTablesCommand, DescribeTableCommand } from "@aws-sdk/client-dynamodb"; import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb"; import { LambdaClient, ListFunctionsCommand, GetFunctionCommand } from "@aws-sdk/client-lambda"; import { CloudWatchClient } from "@aws-sdk/client-cloudwatch"; import { CloudWatchLogsClient, DescribeLogGroupsCommand, FilterLogEventsCommand } from "@aws-sdk/client-cloudwatch-logs"; import { CloudFormationClient } from "@aws-sdk/client-cloudformation"; import { AppSyncClient, ListGraphqlApisCommand } from "@aws-sdk/client-appsync"; import { SecretsManagerClient } from "@aws-sdk/client-secrets-manager"; import { SSMClient } from "@aws-sdk/client-ssm"; import { CognitoIdentityProviderClient, ListUserPoolsCommand, DescribeUserPoolCommand } from "@aws-sdk/client-cognito-identity-provider"; import { S3Client, ListBucketsCommand, GetBucketTaggingCommand } from "@aws-sdk/client-s3"; import { Octokit } from "@octokit/rest"; import { exec } from "child_process"; import { promisify } from "util"; import { existsSync } from "fs"; import { join } from "path"; import crypto from "crypto"; import { promises as fs } from "fs"; import path from "path"; import os from "os"; const execAsync = promisify(exec); // ===================================================================================== // CREDENTIAL MANAGER CLASS // ===================================================================================== class CredentialManager { SERVICE_NAME = 'mcp-amplify'; CONFIG_DIR = path.join(os.homedir(), '.mcp-amplify'); CONFIG_FILE = path.join(this.CONFIG_DIR, 'accounts.json'); ENCRYPTION_KEY_FILE = path.join(this.CONFIG_DIR, '.key'); keytar = null; isKeytarAvailable = false; initPromise; encryptionKey = null; constructor() { this.initPromise = this.initialize(); } async initialize() { // Initialize keytar try { this.keytar = await import('keytar'); this.isKeytarAvailable = true; console.error('[CredentialManager] Keytar initialized successfully'); } catch (error) { console.error('[CredentialManager] Keytar not available, using file-based storage:', error); this.isKeytarAvailable = false; } // Ensure config directory exists try { await fs.mkdir(this.CONFIG_DIR, { recursive: true }); await fs.chmod(this.CONFIG_DIR, 0o700); // Readable only by user } catch (error) { console.error('[CredentialManager] Failed to create config directory:', error); } await this.initializeEncryptionKey(); } async initializeEncryptionKey() { try { this.encryptionKey = await fs.readFile(this.ENCRYPTION_KEY_FILE); } catch (error) { // Generate new key if doesn't exist this.encryptionKey = crypto.randomBytes(32); try { await fs.writeFile(this.ENCRYPTION_KEY_FILE, this.encryptionKey); await fs.chmod(this.ENCRYPTION_KEY_FILE, 0o600); // Read/write for owner only } catch (writeError) { console.error('[CredentialManager] Failed to save encryption key:', writeError); } } } async ensureInitialized() { await this.initPromise; } encrypt(text) { if (!this.encryptionKey) return text; const iv = crypto.randomBytes(16); const cipher = crypto.createCipheriv('aes-256-cbc', this.encryptionKey, iv); let encrypted = cipher.update(text, 'utf8', 'hex'); encrypted += cipher.final('hex'); return iv.toString('hex') + ':' + encrypted; } decrypt(encryptedText) { if (!this.encryptionKey || !encryptedText.includes(':')) return encryptedText; try { const [ivHex, encrypted] = encryptedText.split(':'); const iv = Buffer.from(ivHex, 'hex'); const decipher = crypto.createDecipheriv('aes-256-cbc', this.encryptionKey, iv); let decrypted = decipher.update(encrypted, 'hex', 'utf8'); decrypted += decipher.final('utf8'); return decrypted; } catch (error) { console.error('[CredentialManager] Failed to decrypt:', error); return encryptedText; } } async saveAccount(account) { await this.ensureInitialized(); const persistedAccount = { ...account, addedAt: new Date().toISOString(), lastUsed: new Date().toISOString() }; // Try keytar first if (this.isKeytarAvailable && this.keytar) { try { await this.keytar.setPassword(this.SERVICE_NAME, `aws-${account.id}`, JSON.stringify(persistedAccount)); console.error(`[CredentialManager] Account ${account.id} saved to keychain`); return; // Success with keytar } catch (error) { console.error('[CredentialManager] Keytar save failed, falling back to file:', error); } } // Fallback to file-based storage await this.saveToFile(persistedAccount); } async saveToFile(account) { try { const accounts = await this.loadFromFile(); // Encrypt sensitive data const encryptedAccount = { ...account, accessKeyId: this.encrypt(account.accessKeyId), secretAccessKey: this.encrypt(account.secretAccessKey), sessionToken: account.sessionToken ? this.encrypt(account.sessionToken) : undefined, githubToken: account.githubToken ? this.encrypt(account.githubToken) : undefined }; // Update or add account accounts[account.id] = encryptedAccount; // Save back to file await fs.writeFile(this.CONFIG_FILE, JSON.stringify(accounts, null, 2), 'utf8'); await fs.chmod(this.CONFIG_FILE, 0o600); // Read/write for owner only console.error(`[CredentialManager] Account ${account.id} saved to file`); } catch (error) { console.error('[CredentialManager] Failed to save account to file:', error); throw error; } } async loadFromFile() { try { const data = await fs.readFile(this.CONFIG_FILE, 'utf8'); return JSON.parse(data); } catch (error) { return {}; // File doesn't exist or is invalid } } async updateAccount(account) { await this.ensureInitialized(); // Try keytar first if (this.isKeytarAvailable && this.keytar) { try { const existing = await this.keytar.getPassword(this.SERVICE_NAME, `aws-${account.id}`); if (existing) { const persistedAccount = JSON.parse(existing); const updatedAccount = { ...account, addedAt: persistedAccount.addedAt, lastUsed: new Date().toISOString() }; await this.keytar.setPassword(this.SERVICE_NAME, `aws-${account.id}`, JSON.stringify(updatedAccount)); console.error(`[CredentialManager] Account ${account.id} updated in keychain`); return; } } catch (error) { console.error('[CredentialManager] Keytar update failed, falling back to file:', error); } } // Fallback to file update await this.saveAccount(account); } async loadAccount(accountId) { await this.ensureInitialized(); // Try keytar first if (this.isKeytarAvailable && this.keytar) { try { const stored = await this.keytar.getPassword(this.SERVICE_NAME, `aws-${accountId}`); if (stored) { return JSON.parse(stored); } } catch (error) { console.error('[CredentialManager] Failed to load account from keychain:', error); } } // Fallback to file try { const accounts = await this.loadFromFile(); const encryptedAccount = accounts[accountId]; if (encryptedAccount) { // Decrypt sensitive data const account = { ...encryptedAccount, accessKeyId: this.decrypt(encryptedAccount.accessKeyId), secretAccessKey: this.decrypt(encryptedAccount.secretAccessKey), sessionToken: encryptedAccount.sessionToken ? this.decrypt(encryptedAccount.sessionToken) : undefined, githubToken: encryptedAccount.githubToken ? this.decrypt(encryptedAccount.githubToken) : undefined }; return account; } } catch (error) { console.error('[CredentialManager] Failed to load account from file:', error); } return null; } async loadAllAccounts() { await this.ensureInitialized(); const accounts = []; // Try keytar first if (this.isKeytarAvailable && this.keytar) { try { const credentials = await this.keytar.findCredentials(this.SERVICE_NAME); for (const cred of credentials) { if (cred.account.startsWith('aws-')) { try { const account = JSON.parse(cred.password); accounts.push(account); } catch (error) { console.error(`[CredentialManager] Failed to parse keytar account ${cred.account}:`, error); } } } if (accounts.length > 0) { console.error(`[CredentialManager] Loaded ${accounts.length} accounts from keychain`); return accounts.sort((a, b) => a.name.localeCompare(b.name)); } } catch (error) { console.error('[CredentialManager] Failed to load from keychain:', error); } } // Fallback to file-based storage try { const fileAccounts = await this.loadFromFile(); for (const [id, encryptedAccount] of Object.entries(fileAccounts)) { try { // Decrypt sensitive data const account = { ...encryptedAccount, accessKeyId: this.decrypt(encryptedAccount.accessKeyId), secretAccessKey: this.decrypt(encryptedAccount.secretAccessKey), sessionToken: encryptedAccount.sessionToken ? this.decrypt(encryptedAccount.sessionToken) : undefined, githubToken: encryptedAccount.githubToken ? this.decrypt(encryptedAccount.githubToken) : undefined }; accounts.push(account); } catch (error) { console.error(`[CredentialManager] Failed to process file account ${id}:`, error); } } console.error(`[CredentialManager] Loaded ${accounts.length} accounts from file`); } catch (error) { console.error('[CredentialManager] Failed to load from file:', error); } return accounts.sort((a, b) => a.name.localeCompare(b.name)); } async removeAccount(accountId) { await this.ensureInitialized(); // Remove from keytar if (this.isKeytarAvailable && this.keytar) { try { await this.keytar.deletePassword(this.SERVICE_NAME, `aws-${accountId}`); console.error(`[CredentialManager] Account ${accountId} removed from keychain`); } catch (error) { console.error('[CredentialManager] Failed to remove account from keychain:', error); } } // Remove from file try { const accounts = await this.loadFromFile(); if (accounts[accountId]) { delete accounts[accountId]; await fs.writeFile(this.CONFIG_FILE, JSON.stringify(accounts, null, 2), 'utf8'); console.error(`[CredentialManager] Account ${accountId} removed from file`); } } catch (error) { console.error('[CredentialManager] Failed to remove account from file:', error); } } } // ===================================================================================== // AWS CLIENT FACTORY // ===================================================================================== function createAWSClients(account) { const config = { region: account.region, credentials: { accessKeyId: account.accessKeyId, secretAccessKey: account.secretAccessKey, ...(account.sessionToken && { sessionToken: account.sessionToken }) } }; const dynamodbClient = new DynamoDBClient(config); const dynamodbDoc = DynamoDBDocumentClient.from(dynamodbClient); const clients = { amplify: new AmplifyClient(config), dynamodb: dynamodbClient, dynamodbDoc, lambda: new LambdaClient(config), cloudwatch: new CloudWatchClient(config), cloudwatchLogs: new CloudWatchLogsClient(config), cloudformation: new CloudFormationClient(config), appsync: new AppSyncClient(config), secretsManager: new SecretsManagerClient(config), ssm: new SSMClient(config), cognitoIdp: new CognitoIdentityProviderClient(config), s3: new S3Client(config), }; // Add GitHub client if token is available if (account.githubToken) { clients.octokit = new Octokit({ auth: account.githubToken, ...(account.githubUsername && { username: account.githubUsername }) }); } return clients; } // ===================================================================================== // ENVIRONMENT MIGRATION // ===================================================================================== function migrateFromEnvironment() { const accessKeyId = process.env.AWS_ACCESS_KEY_ID; const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY; const region = process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION || 'us-east-1'; const sessionToken = process.env.AWS_SESSION_TOKEN; const githubToken = process.env.GITHUB_TOKEN; const githubUsername = process.env.GITHUB_USERNAME; if (accessKeyId && secretAccessKey) { return { id: 'migrated-env', name: 'Migrated from Environment', accessKeyId, secretAccessKey, ...(sessionToken && { sessionToken }), region, ...(githubToken && { githubToken }), ...(githubUsername && { githubUsername }), isDefault: true }; } return null; } // ===================================================================================== // GLOBAL STATE // ===================================================================================== const credentialManager = new CredentialManager(); const accountState = { accounts: new Map(), activeAccountId: null, clients: new Map() }; // ===================================================================================== // INITIALIZATION // ===================================================================================== async function initializeAccounts() { try { const savedAccounts = await credentialManager.loadAllAccounts(); for (const account of savedAccounts) { accountState.accounts.set(account.id, account); try { const clients = createAWSClients(account); accountState.clients.set(account.id, clients); if (account.isDefault && !accountState.activeAccountId) { accountState.activeAccountId = account.id; } } catch (error) { console.warn(`Failed to create clients for account ${account.id}:`, error); } } if (!accountState.activeAccountId && accountState.accounts.size > 0) { const firstAccount = accountState.accounts.values().next().value; if (firstAccount) { accountState.activeAccountId = firstAccount.id; } } if (accountState.accounts.size === 0) { const envAccount = migrateFromEnvironment(); if (envAccount) { console.log('Migrating AWS credentials from environment variables...'); await addAccount(envAccount); } } console.log(`Initialized ${accountState.accounts.size} AWS account(s)`); if (accountState.activeAccountId) { console.log(`Active account: ${accountState.activeAccountId}`); } } catch (error) { console.error('Failed to initialize accounts:', error); } } // ===================================================================================== // ACCOUNT MANAGEMENT FUNCTIONS // ===================================================================================== async function addAccount(account) { try { const clients = createAWSClients(account); accountState.accounts.set(account.id, account); accountState.clients.set(account.id, clients); if (!accountState.activeAccountId || account.isDefault) { accountState.activeAccountId = account.id; } await credentialManager.saveAccount(account); } catch (error) { accountState.accounts.delete(account.id); accountState.clients.delete(account.id); throw error; } } async function removeAccount(accountId) { accountState.accounts.delete(accountId); accountState.clients.delete(accountId); if (accountState.activeAccountId === accountId) { const remainingAccounts = Array.from(accountState.accounts.keys()); accountState.activeAccountId = remainingAccounts.length > 0 ? remainingAccounts[0] : null; } await credentialManager.removeAccount(accountId); } async function switchAccount(accountId) { if (!accountState.accounts.has(accountId)) { throw new Error(`Account ${accountId} not found`); } accountState.activeAccountId = accountId; const account = accountState.accounts.get(accountId); await credentialManager.updateAccount(account); } function getCurrentClients() { if (!accountState.activeAccountId) { throw new Error('No active AWS account. Please add an account first.'); } const clients = accountState.clients.get(accountState.activeAccountId); if (!clients) { throw new Error(`Clients not found for active account: ${accountState.activeAccountId}`); } return clients; } // ===================================================================================== // TOOL DEFINITIONS // ===================================================================================== const accountTools = [ { name: "list_accounts", description: "List all configured AWS accounts with their status", inputSchema: { type: "object", properties: {}, }, }, { name: "switch_account", description: "Switch to a different AWS account", inputSchema: { type: "object", properties: { account_id: { type: "string", description: "Account ID to switch to", }, }, required: ["account_id"], }, }, { name: "add_account", description: "Add a new AWS account configuration", inputSchema: { type: "object", properties: { name: { type: "string", description: "Human-friendly name for this account", }, access_key_id: { type: "string", description: "AWS Access Key ID", }, secret_access_key: { type: "string", description: "AWS Secret Access Key", }, session_token: { type: "string", description: "AWS Session Token (optional, for temporary credentials)", }, region: { type: "string", description: "AWS region (e.g., 'us-east-1')", }, profile: { type: "string", description: "AWS CLI profile name (optional)", }, github_token: { type: "string", description: "GitHub token for repository operations (optional)", }, github_username: { type: "string", description: "GitHub username (optional)", }, }, required: ["name", "access_key_id", "secret_access_key", "region"], }, }, { name: "remove_account", description: "Remove an AWS account configuration", inputSchema: { type: "object", properties: { account_id: { type: "string", description: "Account ID to remove", }, }, required: ["account_id"], }, }, { name: "get_active_account", description: "Get details of the currently active account", inputSchema: { type: "object", properties: {}, }, }, { name: "set_default_account", description: "Set an account as the default for new sessions", inputSchema: { type: "object", properties: { account_id: { type: "string", description: "Account ID to set as default", }, }, required: ["account_id"], }, }, { name: "update_account", description: "Update account credentials (e.g. for key rotation)", inputSchema: { type: "object", properties: { account_id: { type: "string", description: "Account ID to update", }, access_key_id: { type: "string", description: "New AWS Access Key ID", }, secret_access_key: { type: "string", description: "New AWS Secret Access Key", }, session_token: { type: "string", description: "New AWS Session Token", }, github_token: { type: "string", description: "New GitHub token", }, github_username: { type: "string", description: "New GitHub username", }, }, required: ["account_id"], }, }, ]; const amplifyTools = [ // App Discovery & Management (3 tools) { name: "amplify_list_apps", description: "List all Amplify apps with basic info", inputSchema: { type: "object", properties: { maxResults: { type: "number", description: "Maximum number of apps to return (default: 25, max: 100)" } } } }, { name: "amplify_get_app_info", description: "Get comprehensive app details (branches, domains, build status)", inputSchema: { type: "object", properties: { appId: { type: "string", description: "Amplify application ID to get info for" } }, required: ["appId"] } }, { name: "amplify_list_branches", description: "List branches with deployment status", inputSchema: { type: "object", properties: { appId: { type: "string", description: "Amplify app ID" }, maxResults: { type: "number", description: "Maximum number of branches to return (default: 50)" } }, required: ["appId"] } }, // Resource Discovery (1 tool) { name: "amplify_discover_resources", description: "Enhanced unified resource discovery with branch-specific capabilities", inputSchema: { type: "object", properties: { appId: { type: "string", description: "If provided, gets branch-specific resources" }, branchName: { type: "string", description: "Auto-detects if only one branch exists" }, resourceType: { type: "string", enum: ["lambda", "dynamodb", "cognito", "all"], description: "Type of resources to discover" } } } }, // Granular Logging & Debugging (3 tools) { name: "amplify_get_build_logs", description: "Build/deployment logs from Amplify Console", inputSchema: { type: "object", properties: { appId: { type: "string", description: "Amplify app ID" }, branchName: { type: "string", description: "Branch name (e.g., 'main', 'dev')" }, jobId: { type: "string", description: "Specific job ID (optional - defaults to latest build)" }, maxLines: { type: "number", description: "Maximum number of log lines to return (default: 1000)" } }, required: ["appId", "branchName"] } }, { name: "amplify_get_function_logs", description: "Enhanced Lambda CloudWatch logs with auto-discovery", inputSchema: { type: "object", properties: { appId: { type: "string", description: "Amplify app ID" }, functionName: { type: "string", description: "Function name (auto-discover if not provided)" }, timeWindow: { type: "string", enum: ["15m", "1h", "6h", "24h"], description: "Time window for logs" }, filterPattern: { type: "string", description: "CloudWatch filter pattern (e.g., 'ERROR', 'timeout')" }, logLevel: { type: "string", enum: ["ERROR", "WARN", "INFO", "DEBUG"], description: "Filter by log level" } }, required: ["appId"] } }, { name: "amplify_get_service_logs", description: "Other AWS service logs (Cognito, AppSync, DynamoDB)", inputSchema: { type: "object", properties: { appId: { type: "string", description: "Amplify app ID" }, service: { type: "string", enum: ["cognito", "appsync", "dynamodb"], description: "AWS service to get logs for" }, timeWindow: { type: "string", enum: ["15m", "1h", "6h", "24h"], description: "Time window for logs" }, filterPattern: { type: "string", description: "Log filter pattern" } }, required: ["appId", "service"] } }, // Essential Operations (1 tool) { name: "amplify_deploy_sandbox", description: "Deploy sandbox environment using 'npx ampx sandbox --once'", inputSchema: { type: "object", properties: { identifier: { type: "string", description: "Sandbox identifier" }, outputsFormat: { type: "string", enum: ["json", "dart", "ts"], description: "Format for outputs file (default: json)" }, profile: { type: "string", description: "AWS profile to use" } } } }, // Performance & Health Monitoring (1 tool) { name: "amplify_get_function_metrics", description: "Lambda performance insights and metrics", inputSchema: { type: "object", properties: { appId: { type: "string", description: "Amplify app ID" }, functionName: { type: "string", description: "Function name (auto-discover if not provided)" }, timeWindow: { type: "string", enum: ["1h", "6h", "24h"], description: "Time window for metrics" }, metrics: { type: "array", items: { type: "string", enum: ["errors", "duration", "invocations", "throttles", "memory"] }, description: "Metrics to retrieve" } }, required: ["appId"] } }, // Quick Debugging (1 tool) { name: "amplify_get_recent_errors", description: "Last 50 errors across all services for quick debugging", inputSchema: { type: "object", properties: { appId: { type: "string", description: "Amplify app ID" }, timeWindow: { type: "string", enum: ["15m", "1h", "6h"], description: "How far back to look for errors" }, services: { type: "array", items: { type: "string", enum: ["lambda", "cognito", "appsync", "dynamodb"] }, description: "Services to check for errors" } }, required: ["appId"] } }, // Environment Management (1 tool) { name: "amplify_get_environment_config", description: "All environment variables and stack outputs", inputSchema: { type: "object", properties: { appId: { type: "string", description: "Amplify app ID" }, branchName: { type: "string", description: "Branch name (auto-detect if not provided)" }, format: { type: "string", enum: ["json", "env", "summary"], description: "Output format" } }, required: ["appId"] } }, // API Testing (1 tool) { name: "amplify_test_api_endpoint", description: "Test GraphQL/REST endpoints without leaving CLI", inputSchema: { type: "object", properties: { appId: { type: "string", description: "Amplify app ID" }, query: { type: "string", description: "GraphQL query to execute" }, variables: { type: "object", description: "Query variables" }, authMode: { type: "string", enum: ["apiKey", "userPool", "iam"], description: "Authentication mode" } }, required: ["appId"] } }, // Deployment Status & Health (1 tool) { name: "amplify_get_deployment_status", description: "Real-time deployment and health status overview", inputSchema: { type: "object", properties: { appId: { type: "string", description: "Amplify app ID" }, branchName: { type: "string", description: "Branch name (auto-detect if not provided)" }, includeHealthChecks: { type: "boolean", description: "Include health checks for all services" } }, required: ["appId"] } } ]; // ===================================================================================== // ACCOUNT MANAGEMENT HANDLERS // ===================================================================================== async function handleListAccounts() { const accounts = Array.from(accountState.accounts.values()).map(account => ({ id: account.id, name: account.name, region: account.region, profile: account.profile || null, hasGithubToken: !!account.githubToken, githubUsername: account.githubUsername || null, isDefault: account.isDefault || false, isActive: account.id === accountState.activeAccountId })); return { accounts, activeAccountId: accountState.activeAccountId, totalAccounts: accounts.length }; } async function handleSwitchAccount(accountId) { await switchAccount(accountId); const account = accountState.accounts.get(accountId); return { success: true, activeAccount: { id: account.id, name: account.name, region: account.region }, message: `Switched to account: ${account.name}` }; } async function handleAddAccount(args) { const accountId = args.name.toLowerCase().replace(/[^a-z0-9]/g, '-'); if (accountState.accounts.has(accountId)) { throw new Error(`Account with ID '${accountId}' already exists`); } const account = { id: accountId, name: args.name, accessKeyId: args.access_key_id, secretAccessKey: args.secret_access_key, ...(args.session_token && { sessionToken: args.session_token }), region: args.region, ...(args.profile && { profile: args.profile }), ...(args.github_token && { githubToken: args.github_token }), ...(args.github_username && { githubUsername: args.github_username }), isDefault: accountState.accounts.size === 0 }; await addAccount(account); return { success: true, account: { id: account.id, name: account.name, region: account.region, isDefault: account.isDefault, isActive: accountState.activeAccountId === account.id }, message: `Added account: ${account.name}` }; } async function handleRemoveAccount(accountId) { if (!accountState.accounts.has(accountId)) { throw new Error(`Account '${accountId}' not found`); } const account = accountState.accounts.get(accountId); await removeAccount(accountId); return { success: true, removedAccount: account.name, activeAccountId: accountState.activeAccountId, message: `Removed account: ${account.name}` }; } async function handleGetActiveAccount() { if (!accountState.activeAccountId) { return { activeAccount: null, message: "No active account" }; } const account = accountState.accounts.get(accountState.activeAccountId); return { activeAccount: { id: account.id, name: account.name, region: account.region, profile: account.profile || null, hasGithubToken: !!account.githubToken, githubUsername: account.githubUsername || null, isDefault: account.isDefault || false } }; } async function handleSetDefaultAccount(accountId) { if (!accountState.accounts.has(accountId)) { throw new Error(`Account '${accountId}' not found`); } for (const [id, account] of accountState.accounts.entries()) { const updatedAccount = { ...account, isDefault: id === accountId }; accountState.accounts.set(id, updatedAccount); await credentialManager.updateAccount(updatedAccount); } const account = accountState.accounts.get(accountId); return { success: true, defaultAccount: account.name, message: `Set default account: ${account.name}` }; } async function handleUpdateAccount(args) { const { account_id, ...updates } = args; if (!accountState.accounts.has(account_id)) { throw new Error(`Account '${account_id}' not found`); } const account = accountState.accounts.get(account_id); const updatedAccount = { ...account, ...(updates.access_key_id && { accessKeyId: updates.access_key_id }), ...(updates.secret_access_key && { secretAccessKey: updates.secret_access_key }), ...(updates.session_token !== undefined && { sessionToken: updates.session_token }), ...(updates.github_token !== undefined && { githubToken: updates.github_token }), ...(updates.github_username !== undefined && { githubUsername: updates.github_username }) }; // Update clients if credentials changed if (updates.access_key_id || updates.secret_access_key || updates.session_token) { const newClients = createAWSClients(updatedAccount); accountState.clients.set(account_id, newClients); } accountState.accounts.set(account_id, updatedAccount); await credentialManager.updateAccount(updatedAccount); return { success: true, account: { id: updatedAccount.id, name: updatedAccount.name, region: updatedAccount.region }, message: `Updated account: ${updatedAccount.name}` }; } // ===================================================================================== // AMPLIFY QUICKSTART HANDLER // ===================================================================================== async function handleAmplifyQuickstartNextjs(args) { const { projectName, installDependencies = true, openInVscode = false } = args; // Validate project name if (!projectName || projectName.trim() === '') { throw new Error('Project name is required'); } // Check if directory already exists if (existsSync(projectName)) { throw new Error(`Directory '${projectName}' already exists`); } const results = { projectName, path: join(process.cwd(), projectName), steps: [], success: false, nextSteps: [] }; try { // Clone the template results.steps.push('Cloning Amplify Next.js template...'); await execAsync(`git clone https://github.com/aws-samples/amplify-next-template.git ${projectName}`); results.steps.push('✓ Template cloned successfully'); // Remove .git directory to start fresh results.steps.push('Removing template git history...'); await execAsync(`rm -rf ${projectName}/.git`); results.steps.push('✓ Git history removed'); // Install dependencies if requested if (installDependencies) { results.steps.push('Installing npm dependencies (this may take a few minutes)...'); await execAsync(`cd ${projectName} && npm install`); results.steps.push('✓ Dependencies installed'); } // Open in VS Code if requested if (openInVscode) { try { await execAsync(`code ${projectName}`); results.steps.push('✓ Opened in VS Code'); } catch (error) { results.steps.push('⚠ Could not open in VS Code (make sure VS Code command is installed)'); } } results.success = true; // Add next steps results.nextSteps = [ `cd ${projectName}`, installDependencies ? '' : 'npm install', 'npx ampx sandbox', 'npm run dev' ].filter(step => step !== ''); return results; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); throw new Error(`Failed to create project: ${errorMessage}`); } } // ===================================================================================== // AMPLIFY APP MANAGEMENT HANDLERS // ===================================================================================== async function handleAmplifyListApps(args) { const clients = getCurrentClients(); const { maxResults = 25 } = args; try { const command = new ListAppsCommand({ maxResults: Math.min(maxResults, 100) }); const response = await clients.amplify.send(command); const apps = response.apps?.map(app => ({ appId: app.appId, name: app.name, description: app.description, repository: app.repository, platform: app.platform, createTime: app.createTime, updateTime: app.updateTime, defaultDomain: app.defaultDomain, enableBranchAutoBuild: app.enableBranchAutoBuild, enableBranchAutoDeletion: app.enableBranchAutoDeletion })) || []; return { apps, totalApps: apps.length, nextToken: response.nextToken || null }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); throw new Error(`Failed to list Amplify apps: ${errorMessage}`); } } async function handleAmplifyListBranches(args) { const clients = getCurrentClients(); const { appId, maxResults = 50 } = args; if (!appId) { throw new Error('App ID is required'); } try { const command = new ListBranchesCommand({ appId, maxResults }); const response = await clients.amplify.send(command); const branches = response.branches?.map(branch => ({ branchName: branch.branchName, branchArn: branch.branchArn, description: branch.description, stage: branch.stage, displayName: branch.displayName, enableNotification: branch.enableNotification, createTime: branch.createTime, updateTime: branch.updateTime, enableAutoBuild: branch.enableAutoBuild, customDomains: branch.customDomains, framework: branch.framework, activeJobId: branch.activeJobId, totalNumberOfJobs: branch.totalNumberOfJobs, enablePullRequestPreview: branch.enablePullRequestPreview, ttl: branch.ttl })) || []; return { appId, branches, totalBranches: branches.length, nextToken: response.nextToken || null }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); throw new Error(`Failed to list branches: ${errorMessage}`); } } async function handleAmplifyGetBuildLogs(args) { const clients = getCurrentClients(); const { appId, branchName, jobId, maxLines = 1000 } = args; if (!appId || !branchName) { throw new Error('App ID and branch name are required'); } try { let targetJobId = jobId; // If no jobId provided, get the latest job if (!targetJobId) { const listJobsCommand = new ListJobsCommand({ appId, branchName, maxResults: 1 }); const jobsResponse = await clients.amplify.send(listJobsCommand); if (!jobsResponse.jobSummaries || jobsResponse.jobSummaries.length === 0) { return { appId, branchName, message: "No build jobs found for this branch" }; } targetJobId = jobsResponse.jobSummaries[0].jobId; } // Get the job details including logs const getJobCommand = new GetJobCommand({ appId, branchName, jobId: targetJobId }); const jobResponse = await clients.amplify.send(getJobCommand); const job = jobResponse.job; if (!job) { throw new Error('Job not found'); } // Get log entries from job steps const logs = []; if (job.steps) { for (const step of job.steps) { if (step.stepName) { logs.push(`\n=== ${step.stepName} ===`); } if (step.startTime) { logs.push(`Start: ${step.startTime}`); } if (step.status) { logs.push(`Status: ${step.status}`); } if (step.logUrl) { logs.push(`Log URL: ${step.logUrl}`); } if (step.artifactsUrl) { logs.push(`Artifacts: ${step.artifactsUrl}`); } if (step.endTime) { logs.push(`End: ${step.endTime}`); } } } return { appId, branchName, jobId: targetJobId, status: job.summary?.status, commitId: job.summary?.commitId, commitMessage: job.summary?.commitMessage, commitTime: job.summary?.commitTime, startTime: job.summary?.startTime, endTime: job.summary?.endTime, jobType: job.summary?.jobType, steps: job.steps?.map(step => ({ stepName: step.stepName, status: step.status, startTime: step.startTime, endTime: step.endTime, logUrl: step.logUrl, artifactsUrl: step.artifactsUrl, testArtifactsUrl: step.testArtifactsUrl, testConfigUrl: step.testConfigUrl, screenshots: step.screenshots })), logs: logs.slice(0, maxLines), totalLogLines: logs.length, message: logs.length === 0 ? "No detailed logs available. Check step URLs for full logs." : null }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); throw new Error(`Failed to get build logs: ${errorMessage}`); } } async function handleAmplifyGetAppInfo(args) { const clients = getCurrentClients(); const { appId } = args; if (!appId) { throw new Error('App ID is required'); } try { // Get app details const getAppCommand = new GetAppCommand({ appId }); const appResponse = await clients.amplify.send(getAppCommand); const app = appResponse.app; if (!app) { throw new Error(`App with ID '${appId}' not found`); } // Get all branches for this app const listBranchesCommand = new ListBranchesCommand({ appId }); const branchesResponse = await clients.amplify.send(listBranchesCommand); const branches = branchesResponse.branches || []; // Format app information const appInfo = { appId: app.appId, name: app.name, description: app.description, repository: app.repository, platform: app.platform, createTime: app.createTime, updateTime: app.updateTime, iamServiceRoleArn: app.iamServiceRoleArn, // Domain and hosting info defaultDomain: app.defaultDomain, enableBranchAutoBuild: app.enableBranchAutoBuild, enableBranchAutoDeletion: app.enableBranchAutoDeletion, enableBasicAuth: app.enableBasicAuth, // Build configuration buildSpec: app.buildSpec, customHeaders: app.customHeaders, enableAutoBranchCreation: app.enableAutoBranchCreation, autoBranchCreationConfig: app.autoBranchCreationConfig, autoBranchCreationPatterns: app.autoBranchCreationPatterns, // Environment variables (app-level) environmentVariables: app.environmentVariables || {}, // Branch information with resource naming patterns br