@chinchillaenterprises/mcp-amplify
Version:
AWS Amplify MCP server with intelligent deployment automation, specialized logging suite, and recursive resource discovery
1,277 lines • 111 kB
JavaScript
#!/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