UNPKG

@chinchillaenterprises/mcp-amplify

Version:

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

461 lines 20.8 kB
import { InvokeCommand, ListFunctionsCommand, GetFunctionCommand } from "@aws-sdk/client-lambda"; import { GetMetricStatisticsCommand, ListMetricsCommand } from "@aws-sdk/client-cloudwatch"; import { DescribeLogStreamsCommand, GetLogEventsCommand } from "@aws-sdk/client-cloudwatch-logs"; // Athena client removed - not needed for this implementation import { getCurrentClients } from './account-handlers.js'; export async function handleAmplifyFunctionInvoke(args) { const { functionName, payload, invocationType = 'RequestResponse' } = args; if (!functionName) { throw new Error('functionName is required'); } try { const clients = getCurrentClients(); // Try to find the actual Lambda function name let actualFunctionName = functionName; // If it looks like a logical name, try to find the physical resource if (!functionName.includes('-') || functionName.length < 20) { // List functions and search for a match const listCommand = new ListFunctionsCommand({ MaxItems: 100 }); const listResponse = await clients.lambda.send(listCommand); const matchingFunction = listResponse.Functions?.find((fn) => fn.FunctionName?.toLowerCase().includes(functionName.toLowerCase())); if (matchingFunction) { actualFunctionName = matchingFunction.FunctionName; console.error(`[Function Invoke] Resolved ${functionName} to ${actualFunctionName}`); } else { // Try exact match try { const getCommand = new GetFunctionCommand({ FunctionName: functionName }); await clients.lambda.send(getCommand); actualFunctionName = functionName; } catch (error) { throw new Error(`Function ${functionName} not found. Available functions: ${listResponse.Functions?.map((f) => f.FunctionName).join(', ') || 'none'}`); } } } // Prepare the payload const payloadBytes = payload ? new TextEncoder().encode(JSON.stringify(payload)) : new TextEncoder().encode('{}'); // Invoke the function const invokeCommand = new InvokeCommand({ FunctionName: actualFunctionName, InvocationType: invocationType, Payload: payloadBytes }); const response = await clients.lambda.send(invokeCommand); // Decode the response let responsePayload = null; if (response.Payload) { const payloadText = new TextDecoder().decode(response.Payload); try { responsePayload = JSON.parse(payloadText); } catch (e) { responsePayload = payloadText; } } // Handle errors if (response.FunctionError) { return { success: false, functionName: actualFunctionName, error: response.FunctionError, errorMessage: responsePayload, statusCode: response.StatusCode, logResult: response.LogResult ? Buffer.from(response.LogResult, 'base64').toString('utf-8') : undefined }; } return { success: true, functionName: actualFunctionName, statusCode: response.StatusCode, executedVersion: response.ExecutedVersion, response: responsePayload, logResult: response.LogResult ? Buffer.from(response.LogResult, 'base64').toString('utf-8').split('\n').slice(-10).join('\n') : undefined, billingInfo: { billedDurationMs: responsePayload?.billedDuration, memoryUsedMB: responsePayload?.maxMemoryUsed } }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); throw new Error(`Failed to invoke function: ${errorMessage}`); } } export async function handleAmplifyFunctionGetLogs(args) { const { functionName, startTime = '10m ago', endTime, limit = 100, filterPattern, format = 'summary' } = args; if (!functionName) { throw new Error('functionName is required'); } try { const clients = getCurrentClients(); // Resolve the actual function name let actualFunctionName = functionName; let logGroupName = `/aws/lambda/${functionName}`; // If it looks like a logical name, try to find the physical resource if (!functionName.includes('-') || functionName.length < 20) { const listCommand = new ListFunctionsCommand({ MaxItems: 100 }); const listResponse = await clients.lambda.send(listCommand); const matchingFunction = listResponse.Functions?.find((fn) => fn.FunctionName?.toLowerCase().includes(functionName.toLowerCase())); if (matchingFunction) { actualFunctionName = matchingFunction.FunctionName; logGroupName = `/aws/lambda/${actualFunctionName}`; } } // Parse time parameters const now = new Date(); let startTimeMs; let endTimeMs = endTime ? new Date(endTime).getTime() : now.getTime(); if (startTime.includes('ago')) { const match = startTime.match(/(\d+)([mhd])\s*ago/); if (match) { const value = parseInt(match[1]); const unit = match[2]; const ms = unit === 'm' ? value * 60 * 1000 : unit === 'h' ? value * 60 * 60 * 1000 : unit === 'd' ? value * 24 * 60 * 60 * 1000 : 0; startTimeMs = now.getTime() - ms; } else { startTimeMs = new Date(startTime).getTime(); } } else { startTimeMs = new Date(startTime).getTime(); } // Get log streams const streamsCommand = new DescribeLogStreamsCommand({ logGroupName, orderBy: 'LastEventTime', descending: true, limit: 10 }); let streams; try { const streamsResponse = await clients.cloudwatchLogs.send(streamsCommand); streams = streamsResponse.logStreams || []; } catch (error) { return { success: false, error: `Log group ${logGroupName} not found. Function may not have been invoked yet.`, functionName: actualFunctionName }; } // Collect log events const allEvents = []; for (const stream of streams) { if (!stream.logStreamName) continue; try { const eventsCommand = new GetLogEventsCommand({ logGroupName, logStreamName: stream.logStreamName, startTime: startTimeMs, endTime: endTimeMs, limit: Math.min(limit, 1000) }); const eventsResponse = await clients.cloudwatchLogs.send(eventsCommand); for (const event of eventsResponse.events || []) { if (!filterPattern || event.message?.includes(filterPattern)) { allEvents.push({ timestamp: event.timestamp, message: event.message, streamName: stream.logStreamName }); } } } catch (error) { // Skip errors for individual streams } } // Sort by timestamp allEvents.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0)); // Format based on requested format if (format === 'raw') { return { functionName: actualFunctionName, logGroup: logGroupName, timeRange: { start: new Date(startTimeMs).toISOString(), end: new Date(endTimeMs).toISOString() }, events: allEvents.slice(0, limit).map(e => ({ timestamp: new Date(e.timestamp).toISOString(), message: e.message })), totalEvents: allEvents.length }; } else { // Summary format - extract key information const errors = allEvents.filter(e => e.message?.includes('ERROR') || e.message?.includes('Error') || e.message?.includes('Exception')); const reports = allEvents.filter(e => e.message?.includes('REPORT RequestId:')); // Extract metrics from REPORT lines const metrics = reports.map(r => { const match = r.message.match(/Duration: ([\d.]+) ms.*Billed Duration: (\d+) ms.*Memory Size: (\d+) MB.*Max Memory Used: (\d+) MB/); if (match) { return { duration: parseFloat(match[1]), billedDuration: parseInt(match[2]), memorySize: parseInt(match[3]), maxMemoryUsed: parseInt(match[4]) }; } return null; }).filter(Boolean); const avgDuration = metrics.length > 0 ? metrics.reduce((sum, m) => sum + m.duration, 0) / metrics.length : 0; const avgMemoryUsed = metrics.length > 0 ? metrics.reduce((sum, m) => sum + m.maxMemoryUsed, 0) / metrics.length : 0; return { functionName: actualFunctionName, logGroup: logGroupName, timeRange: { start: new Date(startTimeMs).toISOString(), end: new Date(endTimeMs).toISOString() }, summary: { totalInvocations: reports.length, errors: errors.length, errorRate: reports.length > 0 ? (errors.length / reports.length * 100).toFixed(2) + '%' : '0%', avgDuration: avgDuration.toFixed(2) + ' ms', avgMemoryUsed: avgMemoryUsed.toFixed(0) + ' MB', recentErrors: errors.slice(0, 5).map(e => ({ timestamp: new Date(e.timestamp).toISOString(), error: e.message?.substring(0, 200) })) }, totalEvents: allEvents.length, hint: "Use format='raw' to see all log entries" }; } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); throw new Error(`Failed to get function logs: ${errorMessage}`); } } export async function handleAmplifyFunctionGetMetrics(args) { const { functionName, metrics = ['invocations', 'errors', 'duration'], startTime = '1h ago', endTime, period = 300, statistic = 'Average' } = args; if (!functionName) { throw new Error('functionName is required'); } try { const clients = getCurrentClients(); // Resolve the actual function name let actualFunctionName = functionName; if (!functionName.includes('-') || functionName.length < 20) { const listCommand = new ListFunctionsCommand({ MaxItems: 100 }); const listResponse = await clients.lambda.send(listCommand); const matchingFunction = listResponse.Functions?.find((fn) => fn.FunctionName?.toLowerCase().includes(functionName.toLowerCase())); if (matchingFunction) { actualFunctionName = matchingFunction.FunctionName; } } // Parse time parameters const now = new Date(); let startTimeDate; let endTimeDate = endTime ? new Date(endTime) : now; if (startTime.includes('ago')) { const match = startTime.match(/(\d+)([mhd])\s*ago/); if (match) { const value = parseInt(match[1]); const unit = match[2]; const ms = unit === 'm' ? value * 60 * 1000 : unit === 'h' ? value * 60 * 60 * 1000 : unit === 'd' ? value * 24 * 60 * 60 * 1000 : 0; startTimeDate = new Date(now.getTime() - ms); } else { startTimeDate = new Date(startTime); } } else { startTimeDate = new Date(startTime); } // Map metric names to CloudWatch metric names const metricMap = { invocations: { name: 'Invocations', namespace: 'AWS/Lambda', unit: 'Count' }, errors: { name: 'Errors', namespace: 'AWS/Lambda', unit: 'Count' }, duration: { name: 'Duration', namespace: 'AWS/Lambda', unit: 'Milliseconds' }, concurrent: { name: 'ConcurrentExecutions', namespace: 'AWS/Lambda', unit: 'Count' }, throttles: { name: 'Throttles', namespace: 'AWS/Lambda', unit: 'Count' } }; const results = {}; for (const metric of metrics) { const metricInfo = metricMap[metric]; if (!metricInfo) continue; const getMetricsCommand = new GetMetricStatisticsCommand({ Namespace: metricInfo.namespace, MetricName: metricInfo.name, Dimensions: [ { Name: 'FunctionName', Value: actualFunctionName } ], StartTime: startTimeDate, EndTime: endTimeDate, Period: period, Statistics: [statistic] }); const response = await clients.cloudwatch.send(getMetricsCommand); results[metric] = { datapoints: response.Datapoints?.sort((a, b) => new Date(a.Timestamp).getTime() - new Date(b.Timestamp).getTime()).map((dp) => ({ timestamp: dp.Timestamp, value: dp[statistic], unit: dp.Unit })) || [], label: metricInfo.name, unit: metricInfo.unit }; } // Calculate some useful insights const invocationData = results.invocations?.datapoints || []; const errorData = results.errors?.datapoints || []; const durationData = results.duration?.datapoints || []; const totalInvocations = invocationData.reduce((sum, dp) => sum + (dp.value || 0), 0); const totalErrors = errorData.reduce((sum, dp) => sum + (dp.value || 0), 0); const avgDuration = durationData.length > 0 ? durationData.reduce((sum, dp) => sum + (dp.value || 0), 0) / durationData.length : 0; return { functionName: actualFunctionName, timeRange: { start: startTimeDate.toISOString(), end: endTimeDate.toISOString() }, period: `${period} seconds`, statistic, metrics: results, insights: { totalInvocations: Math.round(totalInvocations), totalErrors: Math.round(totalErrors), errorRate: totalInvocations > 0 ? ((totalErrors / totalInvocations) * 100).toFixed(2) + '%' : '0%', averageDuration: avgDuration.toFixed(2) + ' ms', trend: invocationData.length >= 2 ? (invocationData[invocationData.length - 1].value > invocationData[0].value ? 'increasing' : 'decreasing') : 'stable' } }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); throw new Error(`Failed to get function metrics: ${errorMessage}`); } } export async function handleAmplifyHealthCheck(args) { const { components = ['auth', 'api', 'database', 'functions'] } = args; try { const clients = getCurrentClients(); const healthStatus = { timestamp: new Date().toISOString(), overall: 'healthy', components: {} }; // Check Lambda functions if (components.includes('functions')) { try { const listCommand = new ListFunctionsCommand({ MaxItems: 10 }); const response = await clients.lambda.send(listCommand); const functionCount = response.Functions?.length || 0; healthStatus.components.functions = { status: functionCount > 0 ? 'healthy' : 'no functions', count: functionCount, message: functionCount > 0 ? `${functionCount} Lambda functions found` : 'No Lambda functions deployed' }; } catch (error) { healthStatus.components.functions = { status: 'error', message: 'Failed to check Lambda functions', error: error instanceof Error ? error.message : String(error) }; healthStatus.overall = 'degraded'; } } // Check other components based on CloudWatch metrics if (components.includes('api')) { try { // Check for AppSync APIs const listMetricsCommand = new ListMetricsCommand({ Namespace: 'AWS/AppSync', MetricName: 'GraphQLError' }); const metricsResponse = await clients.cloudwatch.send(listMetricsCommand); const hasApis = (metricsResponse.Metrics?.length || 0) > 0; healthStatus.components.api = { status: 'healthy', hasAppSync: hasApis, message: hasApis ? 'AppSync API metrics found' : 'No AppSync API metrics found' }; } catch (error) { healthStatus.components.api = { status: 'unknown', message: 'Unable to check API status' }; } } // Add recommendations healthStatus.recommendations = []; if (healthStatus.components.functions?.count === 0) { healthStatus.recommendations.push('Deploy Lambda functions using "npx ampx sandbox --once" to enable backend functionality'); } if (healthStatus.overall === 'degraded') { healthStatus.recommendations.push('Some components are experiencing issues. Check individual component status for details.'); } return healthStatus; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); throw new Error(`Failed to perform health check: ${errorMessage}`); } } export async function handleAmplifyTraceRequest(args) { const { requestId, startFrom = 'api', maxDuration = 30 } = args; if (!requestId) { throw new Error('requestId is required'); } try { const clients = getCurrentClients(); const traces = []; const endTime = Date.now(); const startTime = endTime - (maxDuration * 1000); // Search for the request ID in different log groups const logGroupPatterns = { api: '/aws/appsync/apis/', lambda: '/aws/lambda/', frontend: '/aws/amplify/' }; // This is a simplified trace - in practice, you'd use X-Ray or similar return { requestId, startFrom, message: 'Request tracing requires AWS X-Ray integration or custom correlation IDs', recommendation: 'Consider implementing distributed tracing with AWS X-Ray for full request tracking', alternatives: [ 'Use amplify_get_cloudwatch_logs with filterPattern to search for the requestId', 'Use amplify_log_insights_query to correlate logs across services', 'Implement custom correlation IDs in your application' ] }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); throw new Error(`Failed to trace request: ${errorMessage}`); } } //# sourceMappingURL=monitoring-handlers.js.map