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