#!/usr/bin/env node
import "./load-env.js";
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
ListResourcesRequestSchema,
ReadResourceRequestSchema,
ListPromptsRequestSchema,
GetPromptRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import {
ListBucketsCommand,
GetBucketPolicyStatusCommand,
} from "@aws-sdk/client-s3";
import {
DescribeInstancesCommand,
DescribeSecurityGroupsCommand,
DescribeAddressesCommand,
DescribeVolumesCommand,
DescribeVpcsCommand,
DescribeSubnetsCommand,
DescribeRouteTablesCommand,
DescribeInternetGatewaysCommand,
DescribeNatGatewaysCommand,
} from "@aws-sdk/client-ec2";
import { GetCallerIdentityCommand } from "@aws-sdk/client-sts";
import {
ListUsersCommand,
ListAccessKeysCommand,
ListMFADevicesCommand,
} from "@aws-sdk/client-iam";
import { LookupEventsCommand } from "@aws-sdk/client-cloudtrail";
import { DescribeAlarmsCommand } from "@aws-sdk/client-cloudwatch";
import {
GetCostAndUsageCommand,
GetCostForecastCommand,
GetAnomaliesCommand,
GetSavingsPlansUtilizationCommand,
GetReservationUtilizationCommand,
} from "@aws-sdk/client-cost-explorer";
import {
ListFindingsCommand,
GetFindingsCommand,
ListDetectorsCommand,
} from "@aws-sdk/client-guardduty";
import {
GetLogEventsCommand,
DescribeLogStreamsCommand,
FilterLogEventsCommand,
} from "@aws-sdk/client-cloudwatch-logs";
import { DescribeEventsCommand } from "@aws-sdk/client-health";
import {
ListCertificatesCommand,
DescribeCertificateCommand,
} from "@aws-sdk/client-acm";
import { DescribeDBInstancesCommand } from "@aws-sdk/client-rds";
import { ListFunctionsCommand } from "@aws-sdk/client-lambda";
import { ListBackupJobsCommand } from "@aws-sdk/client-backup";
import { DescribeBudgetsCommand } from "@aws-sdk/client-budgets";
import {
DescribeLoadBalancersCommand,
DescribeTargetGroupsCommand,
DescribeTargetHealthCommand,
DescribeListenersCommand,
DescribeRulesCommand,
} from "@aws-sdk/client-elastic-load-balancing-v2";
import {
ListWebACLsCommand,
GetSampledRequestsCommand,
ListIPSetsCommand,
GetIPSetCommand,
} from "@aws-sdk/client-wafv2";
import { ListTopicsCommand } from "@aws-sdk/client-sns";
import {
ListHostedZonesCommand,
ListResourceRecordSetsCommand,
} from "@aws-sdk/client-route-53";
import { GetMetricStatisticsCommand } from "@aws-sdk/client-cloudwatch";
import {
ListClustersCommand,
ListServicesCommand,
DescribeClustersCommand,
DescribeServicesCommand,
} from "@aws-sdk/client-ecs";
import { ListClustersCommand as ListEksClustersCommand } from "@aws-sdk/client-eks";
import {
DescribeAutoScalingGroupsCommand,
DescribeScalingActivitiesCommand,
} from "@aws-sdk/client-auto-scaling";
import { ListDistributionsCommand } from "@aws-sdk/client-cloudfront";
import { ListSecretsCommand } from "@aws-sdk/client-secrets-manager";
import { DescribeParametersCommand } from "@aws-sdk/client-ssm";
import { ListStacksCommand } from "@aws-sdk/client-cloudformation";
import { ListTablesCommand } from "@aws-sdk/client-dynamodb";
import { DescribeTrustedAdvisorChecksCommand } from "@aws-sdk/client-support";
import checkIp from "ip-range-check";
import { classifyError } from "./lib/error-handler.js";
import { config } from "./lib/config.js";
import * as cache from "./lib/cache.js";
import { withRetry } from "./lib/retry.js";
import { logToolInvocation } from "./lib/audit.js";
import {
generatePolicyForTools,
TOOL_IAM_MAPPING,
} from "./lib/iam-policy-generator.js";
import { logger } from "./lib/logger.js";
import { checkRateLimit } from "./lib/rate-limiter.js";
import { notifyWebhook } from "./lib/webhook.js";
import { loadConfig } from "./lib/config-loader.js";
import {
s3Client,
ec2Client,
stsClient,
iamClient,
cloudTrailClient,
cloudWatchClient,
costExplorerClient,
guardDutyClient,
cloudWatchLogsClient,
healthClient,
acmClient,
backupClient,
budgetsClient,
elbv2Client,
wafv2Client,
snsClient,
route53Client,
ecsClient,
eksClient,
asgClient,
cloudFrontClient,
secretsManagerClient,
ssmClient,
cfnClient,
dynamoDbClient,
supportClient,
getEc2Client,
getRdsClient,
getLambdaClient,
} from "./clients.js";
const server = new Server(
{
name: "aws-mcp-server",
version: "1.1.0",
},
{
capabilities: {
tools: {},
resources: {},
prompts: {},
},
}
);
// Define Tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "get_aws_caller_identity",
description:
"Returns the AWS IAM caller identity (user/role) to verify credentials.",
inputSchema: {
type: "object",
properties: {},
},
},
{
name: "list_s3_buckets",
description: "Lists all S3 buckets in the AWS account.",
inputSchema: {
type: "object",
properties: {
max_results: {
type: "number",
description:
"Max buckets to return (default: 1000).",
},
check_public_access: {
type: "boolean",
description:
"If true, checks if buckets have public access enabled.",
},
},
},
},
{
name: "list_ec2_instances",
description:
"Lists EC2 instances in the current region, showing ID, type, state, and public IP.",
inputSchema: {
type: "object",
properties: {
region: {
type: "string",
description:
"Optional AWS region to list instances from (overrides default)",
},
tag_filter: {
type: "object",
properties: {
key: {
type: "string",
description: "Tag key (e.g., Environment)",
},
value: {
type: "string",
description: "Optional tag value",
},
},
description: "Filter by tag",
},
},
},
},
{
name: "list_iam_users",
description: "Lists IAM users in the AWS account.",
inputSchema: {
type: "object",
properties: {
max_results: {
type: "number",
description:
"Max users to return (default: 100, max: 1000).",
},
},
},
},
{
name: "list_recent_cloudtrail_events",
description:
"Lists recent CloudTrail events to track console access and changes.",
inputSchema: {
type: "object",
properties: {
limit: {
type: "number",
description:
"Number of events to return (default: 10).",
},
lookup_attribute_key: {
type: "string",
description:
"Attribute key to filter by (e.g., 'EventName', 'Username').",
},
lookup_attribute_value: {
type: "string",
description: "Value for the lookup attribute.",
},
},
},
},
{
name: "list_cloudwatch_alarms",
description:
"Lists CloudWatch alarms, optionally filtering by state.",
inputSchema: {
type: "object",
properties: {
state: {
type: "string",
enum: ["OK", "ALARM", "INSUFFICIENT_DATA"],
description: "Filter alarms by state.",
},
},
},
},
{
name: "get_recent_cost",
description:
"Retrieves daily AWS costs for the specified date range (default: last 7 days).",
inputSchema: {
type: "object",
properties: {
start_date: {
type: "string",
description: "Start date in YYYY-MM-DD format.",
},
end_date: {
type: "string",
description: "End date in YYYY-MM-DD format.",
},
},
},
},
{
name: "get_cost_by_service",
description:
"Retrieves AWS costs broken down by service for the specified date range.",
inputSchema: {
type: "object",
properties: {
start_date: {
type: "string",
description: "Start date in YYYY-MM-DD format.",
},
end_date: {
type: "string",
description: "End date in YYYY-MM-DD format.",
},
},
},
},
{
name: "get_cost_breakdown",
description:
"Detailed cost analysis. If service_name is provided, breaks down that service by Usage Type. Otherwise, breaks down by Service.",
inputSchema: {
type: "object",
properties: {
start_date: {
type: "string",
description:
"Start date in YYYY-MM-DD format (default: 14 days ago).",
},
end_date: {
type: "string",
description: "End date in YYYY-MM-DD format.",
},
service_name: {
type: "string",
description:
"Optional: Specific service to analyze (e.g., 'Amazon Elastic Compute Cloud - Compute').",
},
},
},
},
{
name: "get_cost_forecast",
description:
"Predicts future costs for a specified time range.",
inputSchema: {
type: "object",
properties: {
start_date: {
type: "string",
description: "Start date (YYYY-MM-DD).",
},
end_date: {
type: "string",
description: "End date (YYYY-MM-DD).",
},
granularity: {
type: "string",
enum: ["DAILY", "MONTHLY", "HOURLY"],
description: "Granularity (default: DAILY).",
},
prediction_interval_level: {
type: "number",
description:
"Prediction interval confidence (51-99, default: 80).",
},
},
required: ["start_date", "end_date"],
},
},
{
name: "get_budget_details",
description:
"Lists all AWS Budgets along with their status, limits, and current spend.",
inputSchema: {
type: "object",
properties: {
account_id: {
type: "string",
description:
"The AWS Account ID (required for Budgets).",
},
},
required: ["account_id"],
},
},
{
name: "get_cost_anomalies",
description:
"Retrieves cost anomalies detected by AWS Cost Anomaly Detection.",
inputSchema: {
type: "object",
properties: {
start_date: {
type: "string",
description: "Start date (YYYY-MM-DD).",
},
end_date: {
type: "string",
description: "End date (YYYY-MM-DD).",
},
},
required: ["start_date", "end_date"],
},
},
{
name: "get_savings_plans_utilization",
description: "Retrieves Savings Plans utilization percentages.",
inputSchema: {
type: "object",
properties: {
start_date: {
type: "string",
description: "Start date (YYYY-MM-DD).",
},
end_date: {
type: "string",
description: "End date (YYYY-MM-DD).",
},
},
required: ["start_date", "end_date"],
},
},
{
name: "get_reservation_utilization",
description:
"Retrieves Reserved Instance (RI) utilization percentages.",
inputSchema: {
type: "object",
properties: {
start_date: {
type: "string",
description: "Start date (YYYY-MM-DD).",
},
end_date: {
type: "string",
description: "End date (YYYY-MM-DD).",
},
},
required: ["start_date", "end_date"],
},
},
{
name: "get_instance_details",
description:
"Retrieves detailed information about a specific EC2 instance.",
inputSchema: {
type: "object",
properties: {
instance_id: {
type: "string",
description: "The ID of the EC2 instance.",
},
},
required: ["instance_id"],
},
},
{
name: "list_vpcs",
description:
"Lists all VPCs in the current or specified region.",
inputSchema: {
type: "object",
properties: {
region: {
type: "string",
description: "Optional AWS region.",
},
},
},
},
{
name: "list_subnets",
description:
"Lists subnets with availability zones and available IP counts.",
inputSchema: {
type: "object",
properties: {
vpc_id: {
type: "string",
description: "Optional: Filter by VPC ID.",
},
},
},
},
{
name: "list_route_tables",
description:
"Lists route tables with their routes and associations.",
inputSchema: {
type: "object",
properties: {
vpc_id: {
type: "string",
description: "Optional: Filter by VPC ID.",
},
},
},
},
{
name: "list_internet_gateways",
description: "Lists Internet Gateways and their attachments.",
inputSchema: {
type: "object",
properties: {},
},
},
{
name: "list_nat_gateways",
description:
"Lists NAT Gateways with their state and public IP.",
inputSchema: {
type: "object",
properties: {
vpc_id: {
type: "string",
description: "Optional: Filter by VPC ID.",
},
},
},
},
{
name: "list_security_groups",
description: "Lists all security groups.",
inputSchema: {
type: "object",
properties: {
vpc_id: {
type: "string",
description: "Optional: Filter by VPC ID.",
},
},
},
},
{
name: "list_users_without_mfa",
description: "Lists IAM users who do not have MFA enabled.",
inputSchema: {
type: "object",
properties: {},
},
},
{
name: "list_old_access_keys",
description:
"Lists access keys older than 90 days (or specified days).",
inputSchema: {
type: "object",
properties: {
days: {
type: "number",
description:
"Number of days threshold (default: 90).",
},
},
},
},
{
name: "list_expiring_certificates",
description:
"Lists ACM certificates expiring within the specified days.",
inputSchema: {
type: "object",
properties: {
days: {
type: "number",
description:
"Number of days threshold (default: 30).",
},
},
},
},
{
name: "list_rds_instances",
description:
"Lists RDS instances with engine versions and status.",
inputSchema: {
type: "object",
properties: {
region: {
type: "string",
description: "Optional AWS region.",
},
},
},
},
{
name: "list_lambda_functions",
description:
"Lists Lambda functions with runtimes and last modified dates.",
inputSchema: {
type: "object",
properties: {
region: {
type: "string",
description: "Optional AWS region.",
},
tag_filter: {
type: "object",
properties: {
key: { type: "string" },
value: { type: "string" },
},
description: "Filter by tag",
},
},
},
},
{
name: "list_backup_jobs",
description:
"Lists recent backup jobs, optionally filtering by state (default: FAILED).",
inputSchema: {
type: "object",
properties: {
state: {
type: "string",
description:
"Filter by job state (e.g., COMPLETED, FAILED, RUNNING). Default: FAILED.",
},
hours: {
type: "number",
description:
"Look back window in hours (default: 24).",
},
},
},
},
{
name: "list_open_security_groups",
description:
"Lists security groups that allow ingress from 0.0.0.0/0 on specified ports (default: 22, 3389).",
inputSchema: {
type: "object",
properties: {
ports: {
type: "array",
items: { type: "number" },
description:
"List of ports to check (default: [22, 3389]).",
},
},
},
},
{
name: "list_unused_ebs_volumes",
description:
"Lists EBS volumes that are available (not attached to any instance).",
inputSchema: {
type: "object",
properties: {},
},
},
{
name: "list_unassociated_eips",
description:
"Lists Elastic IPs that are not associated with any instance.",
inputSchema: {
type: "object",
properties: {},
},
},
{
name: "list_guardduty_findings",
description: "Lists recent high-severity GuardDuty findings.",
inputSchema: {
type: "object",
properties: {
severity: {
type: "number",
description: "Minimum severity level (default: 4).",
},
limit: {
type: "number",
description:
"Number of findings to return (default: 10).",
},
},
},
},
{
name: "get_recent_logs",
description:
"Retrieves recent log events from a CloudWatch Log Group.",
inputSchema: {
type: "object",
properties: {
log_group_name: {
type: "string",
description: "Name of the Log Group.",
},
limit: {
type: "number",
description:
"Number of log events to return (default: 20).",
},
},
required: ["log_group_name"],
},
},
{
name: "search_cloudwatch_logs",
description:
"Search CloudWatch logs using a filter pattern (e.g., 'ERROR', 'Exception').",
inputSchema: {
type: "object",
properties: {
log_group_name: {
type: "string",
description: "Name of the Log Group.",
},
filter_pattern: {
type: "string",
description:
"The filter pattern to use (e.g., 'ERROR', '{ $.latency > 100 }').",
},
limit: {
type: "number",
description:
"Number of events to return (default: 50).",
},
hours: {
type: "number",
description: "Time window in hours (default: 24).",
},
start_time: { type: "string" },
end_time: { type: "string" },
},
required: ["log_group_name", "filter_pattern"],
},
},
{
name: "list_cloudtrail_changes",
description:
"Lists write/mutation events (Create, Update, Delete) for a specific resource or service.",
inputSchema: {
type: "object",
properties: {
resource_id: {
type: "string",
description:
"Optional: The Resource ID or Name (e.g., sg-12345, my-bucket).",
},
lookup_key: {
type: "string",
enum: [
"ResourceName",
"ResourceType",
"EventName",
"Username",
],
description:
"The attribute to lookup by (default: ResourceName if resource_id provided).",
},
lookup_value: {
type: "string",
description:
"The value for the lookup key (required if resource_id is omitted).",
},
days: {
type: "number",
description:
"Lookback period in days (default: 7).",
},
},
},
},
{
name: "list_access_denied_events",
description:
"Lists recent Access Denied or Unauthorized events from CloudTrail.",
inputSchema: {
type: "object",
properties: {
limit: {
type: "number",
description:
"Number of events to return (default: 20).",
},
},
},
},
{
name: "get_service_health",
description:
"Lists recent open events from AWS Health Dashboard.",
inputSchema: {
type: "object",
properties: {},
},
},
{
name: "list_load_balancers",
description:
"Lists all Application and Network Load Balancers.",
inputSchema: {
type: "object",
properties: {},
},
},
{
name: "list_target_groups",
description: "Lists all Target Groups.",
inputSchema: {
type: "object",
properties: {
load_balancer_arn: {
type: "string",
description:
"Optional: Filter by Load Balancer ARN.",
},
},
},
},
{
name: "list_listener_rules",
description:
"Lists listeners and routing rules (host, path) for a specified Load Balancer.",
inputSchema: {
type: "object",
properties: {
load_balancer_arn: {
type: "string",
description: "The ARN of the Load Balancer.",
},
},
required: ["load_balancer_arn"],
},
},
{
name: "get_target_health",
description:
"Retrieves the health of targets in a specified Target Group.",
inputSchema: {
type: "object",
properties: {
target_group_arn: {
type: "string",
description: "The ARN of the Target Group.",
},
},
required: ["target_group_arn"],
},
},
{
name: "list_web_acls",
description: "Lists Web ACLs (Global/CloudFront or Regional).",
inputSchema: {
type: "object",
properties: {
scope: {
type: "string",
enum: ["CLOUDFRONT", "REGIONAL"],
description:
"The scope of the Web ACLs (default: REGIONAL).",
},
},
},
},
{
name: "get_waf_sampled_requests",
description: "Retrieves sampled requests from a Web ACL.",
inputSchema: {
type: "object",
properties: {
web_acl_arn: {
type: "string",
description: "The ARN of the Web ACL.",
},
rule_metric_name: {
type: "string",
description:
"The metric name of the rule to sample.",
},
scope: {
type: "string",
enum: ["CLOUDFRONT", "REGIONAL"],
description: "The scope (default: REGIONAL).",
},
time_window_seconds: {
type: "number",
description:
"Time window in seconds (e.g., 3600 for 1 hour).",
},
},
required: ["web_acl_arn", "rule_metric_name"],
},
},
{
name: "check_ip_in_waf",
description:
"Checks if an IP address exists in any WAF IP Set (Blocklists/Allowlists).",
inputSchema: {
type: "object",
properties: {
ip_address: {
type: "string",
description:
"The IP address to check (e.g., 192.168.1.1).",
},
},
required: ["ip_address"],
},
},
{
name: "get_metric_statistics",
description:
"Retrieves statistics for a specific CloudWatch metric.",
inputSchema: {
type: "object",
properties: {
namespace: {
type: "string",
description:
"The namespace of the metric (e.g., AWS/EC2).",
},
metric_name: {
type: "string",
description:
"The name of the metric (e.g., CPUUtilization).",
},
dimensions: {
type: "array",
items: {
type: "object",
properties: {
Name: { type: "string" },
Value: { type: "string" },
},
},
description:
"Array of dimensions (e.g., [{Name: 'InstanceId', Value: 'i-xxx'}]).",
},
start_time: {
type: "string",
description: "Start time (ISO string).",
},
end_time: {
type: "string",
description: "End time (ISO string).",
},
period: {
type: "number",
description:
"Granularity in seconds (default: 300).",
},
statistics: {
type: "array",
items: { type: "string" },
description:
"Statistics to retrieve (e.g., ['Average', 'Maximum']).",
},
},
required: ["namespace", "metric_name"],
},
},
{
name: "list_sns_topics",
description: "Lists all SNS topics.",
inputSchema: {
type: "object",
properties: {},
},
},
{
name: "list_record_sets",
description: "Lists DNS records for a given hosted zone.",
inputSchema: {
type: "object",
properties: {
hosted_zone_id: {
type: "string",
description: "The ID of the Hosted Zone.",
},
},
required: ["hosted_zone_id"],
},
},
{
name: "list_hosted_zones",
description: "Lists all Route53 Hosted Zones.",
inputSchema: {
type: "object",
properties: {},
},
},
{
name: "list_ecs_clusters",
description:
"Lists ECS clusters with their status and running task counts.",
inputSchema: {
type: "object",
properties: {
max_results: {
type: "number",
description: "Max clusters (default: 100).",
},
},
},
},
{
name: "list_ecs_services",
description: "Lists services in a specific ECS cluster.",
inputSchema: {
type: "object",
properties: {
cluster: {
type: "string",
description: "The name or ARN of the ECS cluster.",
},
},
required: ["cluster"],
},
},
{
name: "list_eks_clusters",
description: "Lists EKS clusters in the current region.",
inputSchema: { type: "object", properties: {} },
},
{
name: "list_auto_scaling_groups",
description:
"Lists Auto Scaling Groups with their capacity settings.",
inputSchema: { type: "object", properties: {} },
},
{
name: "list_scaling_activities",
description:
"Describes recent scaling activities for an Auto Scaling Group.",
inputSchema: {
type: "object",
properties: {
auto_scaling_group_name: {
type: "string",
description: "The name of the Auto Scaling Group.",
},
},
required: ["auto_scaling_group_name"],
},
},
{
name: "list_cloudfront_distributions",
description:
"Lists CloudFront distributions with their domain names and status.",
inputSchema: { type: "object", properties: {} },
},
{
name: "list_secrets",
description: "Lists Secrets Manager secrets (names only).",
inputSchema: { type: "object", properties: {} },
},
{
name: "list_ssm_parameters",
description: "Lists SSM Parameters (names only).",
inputSchema: { type: "object", properties: {} },
},
{
name: "list_cloudformation_stacks",
description: "Lists CloudFormation stacks and their status.",
inputSchema: {
type: "object",
properties: {
max_results: {
type: "number",
description: "Max stacks (default: 100).",
},
},
},
},
{
name: "list_dynamodb_tables",
description: "Lists DynamoDB tables.",
inputSchema: {
type: "object",
properties: {
max_results: {
type: "number",
description: "Max tables (default: 100).",
},
},
},
},
{
name: "list_trusted_advisor_checks",
description: "Lists Trusted Advisor checks available.",
inputSchema: { type: "object", properties: {} },
},
{
name: "aws_health_check",
description:
"Verifies AWS credentials and connectivity. Use this first to ensure the MCP server can reach AWS.",
inputSchema: { type: "object", properties: {} },
},
{
name: "get_iam_policy_for_tools",
description:
"Generates a least-privilege IAM policy JSON for the specified tools. Pass tool names to get the required permissions.",
inputSchema: {
type: "object",
properties: {
tool_names: {
type: "array",
items: { type: "string" },
description:
"List of tool names (e.g., ['list_ec2_instances','get_recent_cost']). Omit for all tools.",
},
},
},
},
{
name: "estimate_cost",
description:
"Rough cost estimate for a service (EC2, Lambda, RDS, S3) in a region. Uses published rates; actual costs may vary.",
inputSchema: {
type: "object",
properties: {
service: {
type: "string",
enum: ["ec2", "lambda", "rds", "s3"],
description: "Service to estimate.",
},
region: {
type: "string",
description: "AWS region (default: us-east-1).",
},
quantity: {
type: "number",
description:
"Quantity (e.g., hours for EC2, GB for S3).",
},
unit: {
type: "string",
description:
"Unit (e.g., t3.micro, 128 for Lambda MB).",
},
},
required: ["service"],
},
},
{
name: "scan_secrets_risks",
description:
"Lists Secrets Manager secrets that may need attention (e.g., no rotation, old last changed date, names containing sensitive keywords).",
inputSchema: {
type: "object",
properties: {
max_results: {
type: "number",
description: "Max secrets to scan (default: 50).",
},
},
},
},
],
};
});
// Handle Tool Calls
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
const mcpConfig = loadConfig();
const succeed = (result: { content: { type: "text"; text: string }[] }) => {
if (config.auditLog) logToolInvocation(name, true);
if (mcpConfig.webhookUrl) {
notifyWebhook(mcpConfig.webhookUrl, {
tool: name,
timestamp: new Date().toISOString(),
success: true,
});
}
logger.debug("Tool completed", { tool: name });
return result;
};
// Rate limiting
const limit =
mcpConfig.rateLimitPerMinute ??
parseInt(process.env.MCP_AWS_RATE_LIMIT || "0", 10);
if (limit > 0) {
const key = "global";
if (!checkRateLimit(key, limit)) {
return {
content: [
{
type: "text",
text: JSON.stringify({
error: "Rate limit exceeded. Please retry later.",
}),
},
],
isError: true,
};
}
}
// Dry-run: return mock data for testing without AWS
if (config.dryRun) {
const mock: Record<string, string> = {
get_aws_caller_identity: JSON.stringify(
{
UserId: "dry-run",
Account: "123456789012",
Arn: "arn:aws:iam::123456789012:user/dry-run",
},
null,
2
),
aws_health_check: JSON.stringify(
{ status: "ok", message: "Dry-run mode: no AWS calls made" },
null,
2
),
list_ec2_instances: "[]",
list_s3_buckets: "[]",
list_iam_users: "[]",
get_recent_cost: "[]",
};
const text = mock[name] ?? JSON.stringify({ dryRun: true, tool: name });
if (config.auditLog) logToolInvocation(name, true);
return { content: [{ type: "text", text }] };
}
try {
if (name === "get_aws_caller_identity") {
const cacheKey = "get_aws_caller_identity";
if (config.cacheTtlSeconds > 0) {
const cached = cache.get<string>(cacheKey);
if (cached)
return succeed({
content: [{ type: "text", text: cached }],
});
}
const command = new GetCallerIdentityCommand({});
const response = await withRetry(() => stsClient.send(command));
const text = JSON.stringify(
{
UserId: response.UserId,
Account: response.Account,
Arn: response.Arn,
},
null,
2
);
if (config.cacheTtlSeconds > 0)
cache.set(cacheKey, text, config.cacheTtlSeconds);
return succeed({ content: [{ type: "text", text }] });
}
if (name === "list_s3_buckets") {
const maxResults = Math.min(
Math.max(
((args as Record<string, unknown>)
?.max_results as number) ?? 1000,
1
),
1000
);
const command = new ListBucketsCommand({});
const response = await s3Client.send(command);
let buckets =
response.Buckets?.map((b) => ({
Name: b.Name,
CreationDate: b.CreationDate,
IsPublic: undefined as boolean | undefined,
})) || [];
if (args && (args as any).check_public_access) {
buckets = await Promise.all(
buckets.map(async (b) => {
try {
if (!b.Name) return b;
const policyCmd = new GetBucketPolicyStatusCommand({
Bucket: b.Name,
});
const policyResponse =
await s3Client.send(policyCmd);
return {
...b,
IsPublic:
policyResponse.PolicyStatus?.IsPublic ||
false,
};
} catch (_err) {
// If checks fail (e.g. AccessDenied or no policy context), assume not public or unknown
return { ...b, IsPublic: false };
}
})
);
}
buckets = buckets.slice(0, maxResults);
return succeed({
content: [
{
type: "text",
text: JSON.stringify(buckets, null, 2),
},
],
});
}
if (name === "list_ec2_instances") {
// Create a region-specific client if provided, otherwise use default
// Note: Re-instantiating client for every request isn't ideal for perf but fine for this scale
// To strictly support region override we'd need to recreate the client or default to the global one
// For simplicity here using the global one unless specific tool logic is needed.
// Actually, if args.region is passed, we should use a new client.
const region = (args as { region?: string })?.region;
const tagFilter = (
args as {
tag_filter?: { key: string; value?: string };
}
)?.tag_filter;
const client = getEc2Client(region);
const filters =
tagFilter && tagFilter.key
? [
tagFilter.value
? {
Name: `tag:${tagFilter.key}`,
Values: [tagFilter.value],
}
: {
Name: `tag-key`,
Values: [tagFilter.key],
},
]
: undefined;
const command = new DescribeInstancesCommand({
Filters: filters,
});
const response = await client.send(command);
const instances =
response.Reservations?.flatMap(
(r) =>
r.Instances?.map((i) => ({
InstanceId: i.InstanceId,
Name: i.Tags?.find((t) => t.Key === "Name")?.Value,
InstanceType: i.InstanceType,
State: i.State?.Name,
PublicIpAddress: i.PublicIpAddress,
PrivateIpAddress: i.PrivateIpAddress,
LaunchTime: i.LaunchTime,
Tags: i.Tags,
})) || []
) || [];
const content: any[] = [
{
type: "text",
text: JSON.stringify(instances, null, 2),
},
];
if (!region) {
const defaultRegion = process.env.AWS_REGION || "default";
content.push({
type: "text",
text: `\n(Using region: ${defaultRegion}. Pass 'region' argument to check other regions like 'ap-south-1', 'us-west-2', etc.)`,
});
}
return {
content: content,
};
}
if (name === "list_iam_users") {
const maxResults = Math.min(
Math.max(
(args as { max_results?: number })?.max_results ?? 100,
1
),
1000
);
const command = new ListUsersCommand({ MaxItems: maxResults });
const response = await withRetry(() => iamClient.send(command));
const users =
response.Users?.map((u) => ({
UserName: u.UserName,
UserId: u.UserId,
Arn: u.Arn,
CreateDate: u.CreateDate,
})) || [];
return {
content: [
{
type: "text",
text: JSON.stringify(users, null, 2),
},
],
};
}
if (name === "list_recent_cloudtrail_events") {
const limit = (args as any)?.limit || 10;
const lookupKey = (args as any)?.lookup_attribute_key;
const lookupValue = (args as any)?.lookup_attribute_value;
const commandInput: any = { MaxResults: limit };
if (lookupKey && lookupValue) {
commandInput.LookupAttributes = [
{ AttributeKey: lookupKey, AttributeValue: lookupValue },
];
}
const command = new LookupEventsCommand(commandInput);
const response = await cloudTrailClient.send(command);
const events =
response.Events?.map((e) => ({
EventId: e.EventId,
EventName: e.EventName,
EventTime: e.EventTime,
Username: e.Username,
Resources: e.Resources,
CloudTrailEvent: e.CloudTrailEvent
? JSON.parse(e.CloudTrailEvent).userAgent
: undefined, // Extract user agent if available
})) || [];
return {
content: [
{ type: "text", text: JSON.stringify(events, null, 2) },
],
};
}
if (name === "list_cloudwatch_alarms") {
const state = (args as any)?.state;
const commandInput: any = {};
if (state) commandInput.StateValue = state;
const command = new DescribeAlarmsCommand(commandInput);
const response = await cloudWatchClient.send(command);
const alarms =
response.MetricAlarms?.map((a) => ({
AlarmName: a.AlarmName,
StateValue: a.StateValue,
StateReason: a.StateReason,
MetricName: a.MetricName,
Namespace: a.Namespace,
})) || [];
return {
content: [
{ type: "text", text: JSON.stringify(alarms, null, 2) },
],
};
}
if (name === "get_recent_cost") {
const cacheKey =
"get_recent_cost:" +
JSON.stringify((args as Record<string, unknown>) ?? {});
if (config.cacheTtlSeconds > 0) {
const cached = cache.get<string>(cacheKey);
if (cached)
return succeed({
content: [{ type: "text", text: cached }],
});
}
const endDate = String(
(args as Record<string, unknown>)?.end_date ??
new Date().toISOString().split("T")[0]
);
const startDate = String(
(args as Record<string, unknown>)?.start_date ??
new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
.toISOString()
.split("T")[0]
);
const command = new GetCostAndUsageCommand({
TimePeriod: { Start: startDate, End: endDate },
Granularity: "DAILY",
Metrics: ["UnblendedCost"],
});
const response = await costExplorerClient.send(command);
const costs =
response.ResultsByTime?.map((r) => ({
TimePeriod: r.TimePeriod,
Total: r.Total?.UnblendedCost,
})) || [];
const text = JSON.stringify(costs, null, 2);
if (config.cacheTtlSeconds > 0)
cache.set(cacheKey, text, config.cacheTtlSeconds);
return succeed({ content: [{ type: "text", text }] });
}
if (name === "get_cost_by_service") {
const endDate =
(args as any)?.end_date ||
new Date().toISOString().split("T")[0];
const startDate =
(args as any)?.start_date ||
new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
.toISOString()
.split("T")[0];
const command = new GetCostAndUsageCommand({
TimePeriod: { Start: startDate, End: endDate },
Granularity: "DAILY",
Metrics: ["UnblendedCost"],
GroupBy: [{ Type: "DIMENSION", Key: "SERVICE" }],
});
const response = await costExplorerClient.send(command);
const costs =
response.ResultsByTime?.flatMap((r) =>
r.Groups?.map((g) => ({
Date: r.TimePeriod?.Start,
Service: g.Keys?.[0],
Cost: g.Metrics?.UnblendedCost?.Amount,
Unit: g.Metrics?.UnblendedCost?.Unit,
}))
) || [];
return {
content: [
{ type: "text", text: JSON.stringify(costs, null, 2) },
],
};
}
if (name === "get_cost_breakdown") {
const endDate =
(args as any)?.end_date ||
new Date().toISOString().split("T")[0];
const startDate =
(args as any)?.start_date ||
new Date(Date.now() - 14 * 24 * 60 * 60 * 1000)
.toISOString()
.split("T")[0];
const serviceName = (args as any)?.service_name;
const groupByKey = serviceName ? "USAGE_TYPE" : "SERVICE";
const filter = serviceName
? ({
Dimensions: { Key: "SERVICE", Values: [serviceName] },
} as any)
: undefined;
const command = new GetCostAndUsageCommand({
TimePeriod: { Start: startDate, End: endDate },
Granularity: "DAILY",
Metrics: ["UnblendedCost"],
GroupBy: [{ Type: "DIMENSION", Key: groupByKey }],
Filter: filter,
});
const response = await costExplorerClient.send(command);
const costs =
response.ResultsByTime?.flatMap((r) =>
r.Groups?.map((g) => ({
Date: r.TimePeriod?.Start,
[groupByKey === "USAGE_TYPE" ? "UsageType" : "Service"]:
g.Keys?.[0],
Cost: parseFloat(
g.Metrics?.UnblendedCost?.Amount || "0"
).toFixed(4),
Unit: g.Metrics?.UnblendedCost?.Unit,
}))
)
.filter((c) => c && parseFloat(c.Cost) > 0) // Filter out zero costs
.sort(
(a, b) =>
parseFloat(b?.Cost || "0") -
parseFloat(a?.Cost || "0")
) || [];
return {
content: [
{
type: "text",
text: JSON.stringify(costs.slice(0, 100), null, 2),
},
],
};
}
if (name === "get_cost_forecast") {
const command = new GetCostForecastCommand({
TimePeriod: {
Start: (args as any).start_date,
End: (args as any).end_date,
},
Granularity: (args as any)?.granularity || "DAILY",
Metric: "UNBLENDED_COST",
PredictionIntervalLevel:
(args as any)?.prediction_interval_level || 80,
});
const response = await costExplorerClient.send(command);
const forecast =
response.ForecastResultsByTime?.map((f) => ({
Date: f.TimePeriod?.Start,
MeanValue: f.MeanValue,
PredictionIntervalLower: f.PredictionIntervalLowerBound,
PredictionIntervalUpper: f.PredictionIntervalUpperBound,
})) || [];
return {
content: [
{
type: "text",
text: JSON.stringify(
{ Total: response.Total, Forecast: forecast },
null,
2
),
},
],
};
}
if (name === "get_budget_details") {
const accountId = (args as Record<string, unknown>)
.account_id as string;
const cacheKey = `get_budget_details:${accountId}`;
if (config.cacheTtlSeconds > 0) {
const cached = cache.get<string>(cacheKey);
if (cached)
return succeed({
content: [{ type: "text", text: cached }],
});
}
const command = new DescribeBudgetsCommand({
AccountId: accountId,
MaxResults: 100,
});
const response = await budgetsClient.send(command);
const budgets =
response.Budgets?.map((b) => ({
BudgetName: b.BudgetName,
Limit: b.BudgetLimit?.Amount + " " + b.BudgetLimit?.Unit,
CurrentSpend:
b.CalculatedSpend?.ActualSpend?.Amount +
" " +
b.CalculatedSpend?.ActualSpend?.Unit,
ForecastedSpend:
b.CalculatedSpend?.ForecastedSpend?.Amount +
" " +
b.CalculatedSpend?.ForecastedSpend?.Unit,
BudgetType: b.BudgetType,
LastUpdated: b.LastUpdatedTime,
})) || [];
const text = JSON.stringify(budgets, null, 2);
if (config.cacheTtlSeconds > 0)
cache.set(cacheKey, text, config.cacheTtlSeconds);
return succeed({ content: [{ type: "text", text }] });
}
if (name === "get_cost_anomalies") {
const command = new GetAnomaliesCommand({
DateInterval: {
StartDate: (args as any).start_date,
EndDate: (args as any).end_date,
},
MaxResults: 20,
});
const response = await costExplorerClient.send(command);
const anomalies =
response.Anomalies?.map((a) => ({
AnomalyId: a.AnomalyId,
AnomalyScore: a.AnomalyScore,
ImpactTotal: a.Impact?.TotalImpact,
MonitorArn: a.MonitorArn,
RootCauses: a.RootCauses,
Date: a.AnomalyEndDate,
})) || [];
return {
content: [
{ type: "text", text: JSON.stringify(anomalies, null, 2) },
],
};
}
if (name === "get_savings_plans_utilization") {
const command = new GetSavingsPlansUtilizationCommand({
TimePeriod: {
Start: (args as any).start_date,
End: (args as any).end_date,
},
});
const response = await costExplorerClient.send(command);
const utils =
response.SavingsPlansUtilizationsByTime?.map((u) => ({
Date: u.TimePeriod?.Start,
UtilizationPercentage:
u.Utilization?.UtilizationPercentage + "%",
TotalCommitment: u.Utilization?.TotalCommitment,
UsedCommitment: u.Utilization?.UsedCommitment,
UnusedCommitment: u.Utilization?.UnusedCommitment,
})) || [];
return {
content: [
{ type: "text", text: JSON.stringify(utils, null, 2) },
],
};
}
if (name === "get_reservation_utilization") {
const command = new GetReservationUtilizationCommand({
TimePeriod: {
Start: (args as any).start_date,
End: (args as any).end_date,
},
});
const response = await costExplorerClient.send(command);
const utils =
response.UtilizationsByTime?.map((u) => ({
Date: u.TimePeriod?.Start,
TotalPotentialRIHours: u.Total?.TotalPotentialRISavings, // Approximate proxy if direct hours not shown in all interfaces
UtilizationPercentage: u.Total?.UtilizationPercentage + "%",
PurchasedUnits: u.Total?.PurchasedHours,
TotalActualHours: u.Total?.TotalActualHours,
UnusedHours: u.Total?.UnusedHours,
})) || [];
return {
content: [
{ type: "text", text: JSON.stringify(utils, null, 2) },
],
};
}
if (name === "list_users_without_mfa") {
const listCmd = new ListUsersCommand({});
const listResp = await iamClient.send(listCmd);
const users = listResp.Users || [];
const noMfaUsers = [];
// Checking users sequentially to avoid rate limiting
for (const user of users) {
if (!user.UserName) continue;
try {
const mfaCmd = new ListMFADevicesCommand({
UserName: user.UserName,
});
const mfaResp = await iamClient.send(mfaCmd);
if (
!mfaResp.MFADevices ||
mfaResp.MFADevices.length === 0
) {
noMfaUsers.push({
UserName: user.UserName,
UserId: user.UserId,
CreateDate: user.CreateDate,
PasswordLastUsed: user.PasswordLastUsed,
});
}
} catch (_err) {
// Ignore errors (e.g. AccessDenied)
}
}
return {
content: [
{ type: "text", text: JSON.stringify(noMfaUsers, null, 2) },
],
};
}
if (name === "list_old_access_keys") {
const days = (args as any)?.days || 90;
const thresholdDate = new Date(
Date.now() - days * 24 * 60 * 60 * 1000
);
const listCmd = new ListUsersCommand({});
const listResp = await iamClient.send(listCmd);
const users = listResp.Users || [];
const oldKeys = [];
for (const user of users) {
if (!user.UserName) continue;
try {
const keysCmd = new ListAccessKeysCommand({
UserName: user.UserName,
});
const keysResp = await iamClient.send(keysCmd);
if (keysResp.AccessKeyMetadata) {
for (const key of keysResp.AccessKeyMetadata) {
if (
key.CreateDate &&
key.CreateDate < thresholdDate &&
key.Status === "Active"
) {
oldKeys.push({
UserName: user.UserName,
AccessKeyId: key.AccessKeyId,
CreateDate: key.CreateDate,
Status: key.Status,
DaysOld: Math.floor(
(Date.now() -
key.CreateDate.getTime()) /
(1000 * 60 * 60 * 24)
),
});
}
}
}
} catch (_err) {
// Ignore
}
}
return {
content: [
{ type: "text", text: JSON.stringify(oldKeys, null, 2) },
],
};
}
if (name === "list_expiring_certificates") {
const days = (args as any)?.days || 30;
const thresholdDate = new Date(
Date.now() + days * 24 * 60 * 60 * 1000
);
const listCmd = new ListCertificatesCommand({});
const listResp = await acmClient.send(listCmd); // Note: paginates 1000 by default
const expiringCerts = [];
// We need to describe to get 'NotAfter'
for (const certSummary of listResp.CertificateSummaryList || []) {
if (!certSummary.CertificateArn) continue;
try {
const descCmd = new DescribeCertificateCommand({
CertificateArn: certSummary.CertificateArn,
});
const descResp = await acmClient.send(descCmd);
const cert = descResp.Certificate;
if (
cert &&
cert.NotAfter &&
cert.NotAfter < thresholdDate
) {
expiringCerts.push({
DomainName: cert.DomainName,
CertificateArn: cert.CertificateArn,
NotAfter: cert.NotAfter,
Status: cert.Status,
InUseBy: cert.InUseBy,
});
}
} catch (_err) {
// Ignore
}
}
return {
content: [
{
type: "text",
text: JSON.stringify(expiringCerts, null, 2),
},
],
};
}
if (name === "list_rds_instances") {
const region = (args as { region?: string })?.region;
const tagFilter = (
args as {
tag_filter?: { key: string; value?: string };
}
)?.tag_filter;
const client = getRdsClient(region);
const filters =
tagFilter && tagFilter.key
? [
{
Name: tagFilter.value
? `tag:${tagFilter.key}`
: "tag-key",
Values: tagFilter.value
? [tagFilter.value]
: [tagFilter.key],
} as { Name: string; Values: string[] },
]
: undefined;
const command = new DescribeDBInstancesCommand({
Filters: filters,
});
const response = await client.send(command);
const instances =
response.DBInstances?.map((db) => ({
DBInstanceIdentifier: db.DBInstanceIdentifier,
Engine: db.Engine,
EngineVersion: db.EngineVersion,
DBInstanceClass: db.DBInstanceClass,
DBInstanceStatus: db.DBInstanceStatus,
Endpoint: db.Endpoint?.Address,
})) || [];
return {
content: [
{ type: "text", text: JSON.stringify(instances, null, 2) },
],
};
}
if (name === "list_lambda_functions") {
const region = (args as { region?: string })?.region;
const client = getLambdaClient(region);
const command = new ListFunctionsCommand({});
const response = await client.send(command);
const funcs =
response.Functions?.map((f) => ({
FunctionName: f.FunctionName,
Runtime: f.Runtime,
LastModified: f.LastModified,
Handler: f.Handler,
CodeSize: f.CodeSize,
})) || [];
return {
content: [
{ type: "text", text: JSON.stringify(funcs, null, 2) },
],
};
}
if (name === "list_backup_jobs") {
const state = (args as any)?.state || "FAILED";
const hours = (args as any)?.hours || 24;
const sinceDate = new Date(Date.now() - hours * 60 * 60 * 1000);
const command = new ListBackupJobsCommand({
ByState: state,
ByCreatedAfter: sinceDate,
});
const response = await backupClient.send(command);
const jobs =
response.BackupJobs?.map((j) => ({
BackupJobId: j.BackupJobId,
State: j.State,
CreationDate: j.CreationDate,
BackupVaultName: j.BackupVaultName,
ResourceArn: j.ResourceArn,
StatusMessage: j.StatusMessage,
})) || [];
return {
content: [
{ type: "text", text: JSON.stringify(jobs, null, 2) },
],
};
}
if (name === "get_instance_details") {
const instanceId = (args as any).instance_id;
const command = new DescribeInstancesCommand({
InstanceIds: [instanceId],
});
const response = await ec2Client.send(command);
const instance = response.Reservations?.[0]?.Instances?.[0];
if (!instance) {
return {
content: [{ type: "text", text: "Instance not found." }],
};
}
return {
content: [
{ type: "text", text: JSON.stringify(instance, null, 2) },
],
};
}
if (name === "list_vpcs") {
const region = (args as { region?: string })?.region;
const client = getEc2Client(region);
const command = new DescribeVpcsCommand({});
const response = await client.send(command);
const vpcs =
response.Vpcs?.map((v) => ({
VpcId: v.VpcId,
CidrBlock: v.CidrBlock,
IsDefault: v.IsDefault,
State: v.State,
Name: v.Tags?.find((t) => t.Key === "Name")?.Value,
})) || [];
return {
content: [
{ type: "text", text: JSON.stringify(vpcs, null, 2) },
],
};
}
if (name === "list_subnets") {
const vpcId = (args as any)?.vpc_id;
const input: any = {};
if (vpcId) input.Filters = [{ Name: "vpc-id", Values: [vpcId] }];
const command = new DescribeSubnetsCommand(input);
const response = await ec2Client.send(command);
const subnets =
response.Subnets?.map((s) => ({
SubnetId: s.SubnetId,
VpcId: s.VpcId,
AvailabilityZone: s.AvailabilityZone,
CidrBlock: s.CidrBlock,
AvailableIpAddressCount: s.AvailableIpAddressCount,
Name: s.Tags?.find((t) => t.Key === "Name")?.Value,
})) || [];
return {
content: [
{ type: "text", text: JSON.stringify(subnets, null, 2) },
],
};
}
if (name === "list_route_tables") {
const vpcId = (args as any)?.vpc_id;
const input: any = {};
if (vpcId) input.Filters = [{ Name: "vpc-id", Values: [vpcId] }];
const command = new DescribeRouteTablesCommand(input);
const response = await ec2Client.send(command);
const routeTables =
response.RouteTables?.map((rt) => ({
RouteTableId: rt.RouteTableId,
VpcId: rt.VpcId,
Routes: rt.Routes?.map((r) => ({
DestinationCidrBlock: r.DestinationCidrBlock,
GatewayId: r.GatewayId,
NatGatewayId: r.NatGatewayId,
State: r.State,
})),
Associations: rt.Associations?.map((a) => ({
RouteTableAssociationId: a.RouteTableAssociationId,
SubnetId: a.SubnetId,
Main: a.Main,
})),
Name: rt.Tags?.find((t) => t.Key === "Name")?.Value,
})) || [];
return {
content: [
{
type: "text",
text: JSON.stringify(routeTables, null, 2),
},
],
};
}
if (name === "list_internet_gateways") {
const command = new DescribeInternetGatewaysCommand({});
const response = await ec2Client.send(command);
const igws =
response.InternetGateways?.map((igw) => ({
InternetGatewayId: igw.InternetGatewayId,
Attachments: igw.Attachments,
Name: igw.Tags?.find((t) => t.Key === "Name")?.Value,
})) || [];
return {
content: [
{ type: "text", text: JSON.stringify(igws, null, 2) },
],
};
}
if (name === "list_nat_gateways") {
const vpcId = (args as any)?.vpc_id;
const input: any = {};
if (vpcId) input.Filter = [{ Name: "vpc-id", Values: [vpcId] }];
const command = new DescribeNatGatewaysCommand(input);
const response = await ec2Client.send(command);
const nats =
response.NatGateways?.map((nat) => ({
NatGatewayId: nat.NatGatewayId,
VpcId: nat.VpcId,
SubnetId: nat.SubnetId,
State: nat.State,
PublicIp: nat.NatGatewayAddresses?.[0]?.PublicIp,
Name: nat.Tags?.find((t) => t.Key === "Name")?.Value,
})) || [];
return {
content: [
{ type: "text", text: JSON.stringify(nats, null, 2) },
],
};
}
if (name === "list_security_groups") {
const vpcId = (args as any)?.vpc_id;
const filter = vpcId
? [{ Name: "vpc-id", Values: [vpcId] }]
: undefined;
const command = new DescribeSecurityGroupsCommand({
Filters: filter,
});
const response = await ec2Client.send(command);
const sgs =
response.SecurityGroups?.map((s) => ({
GroupId: s.GroupId,
GroupName: s.GroupName,
Description: s.Description,
VpcId: s.VpcId,
})) || [];
return {
content: [{ type: "text", text: JSON.stringify(sgs, null, 2) }],
};
}
if (name === "list_open_security_groups") {
const checkPorts = (args as any)?.ports; // If undefined, we check for ANY open port
// If user specifically requests some ports, use them. If checksPorts is undefined/empty, means "any port".
// But if user passes [], it might mean "any" or "none". Let's assume undefined means "any".
const checkSpecificPorts = checkPorts && checkPorts.length > 0;
const command = new DescribeSecurityGroupsCommand({
Filters: [
{ Name: "ip-permission.cidr", Values: ["0.0.0.0/0"] },
],
});
const response = await ec2Client.send(command);
const openSGs =
response.SecurityGroups?.filter((sg) => {
return sg.IpPermissions?.some((perm) => {
const isGlobal = perm.IpRanges?.some(
(r) => r.CidrIp === "0.0.0.0/0"
);
if (!isGlobal) return false;
if (!checkSpecificPorts) return true; // If we aren't filtering by specific ports, then ANY 0.0.0.0/0 is a match.
// Check if it overlaps with checked ports or is all traffic
if (perm.IpProtocol === "-1") return true; // All traffic
const fromPort = perm.FromPort || 0;
const toPort = perm.ToPort || 65535;
return checkPorts.some(
(p: number) => p >= fromPort && p <= toPort
);
});
}).map((sg) => ({
GroupId: sg.GroupId,
GroupName: sg.GroupName,
Description: sg.Description,
OpenPorts: sg.IpPermissions?.filter(
(perm) =>
perm.IpRanges?.some(
(r) => r.CidrIp === "0.0.0.0/0"
) &&
(!checkSpecificPorts ||
perm.IpProtocol === "-1" ||
checkPorts.some(
(p: number) =>
p >= (perm.FromPort || 0) &&
p <= (perm.ToPort || 65535)
))
).map((p) =>
p.IpProtocol === "-1"
? "All"
: `${p.FromPort}-${p.ToPort}`
),
})) || [];
return {
content: [
{ type: "text", text: JSON.stringify(openSGs, null, 2) },
],
};
}
if (name === "list_unused_ebs_volumes") {
const command = new DescribeVolumesCommand({
Filters: [{ Name: "status", Values: ["available"] }],
});
const response = await ec2Client.send(command);
const volumes =
response.Volumes?.map((v) => ({
VolumeId: v.VolumeId,
Size: v.Size,
Type: v.VolumeType,
CreateTime: v.CreateTime,
AvailabilityZone: v.AvailabilityZone,
})) || [];
return {
content: [
{ type: "text", text: JSON.stringify(volumes, null, 2) },
],
};
}
if (name === "list_unassociated_eips") {
const command = new DescribeAddressesCommand({});
const response = await ec2Client.send(command);
// Filter where AssociationId is missing
const unusedEips =
response.Addresses?.filter((a) => !a.AssociationId).map(
(a) => ({
PublicIp: a.PublicIp,
AllocationId: a.AllocationId,
Domain: a.Domain,
})
) || [];
return {
content: [
{ type: "text", text: JSON.stringify(unusedEips, null, 2) },
],
};
}
if (name === "list_guardduty_findings") {
// first list detectors
const detectorsCmd = new ListDetectorsCommand({});
const dResponse = await guardDutyClient.send(detectorsCmd);
const detectorId = dResponse.DetectorIds?.[0];
if (!detectorId) {
return {
content: [
{ type: "text", text: "No GuardDuty detector found." },
],
};
}
const severity = (args as any)?.severity || 4;
const limit = (args as any)?.limit || 10;
const listCmd = new ListFindingsCommand({
DetectorId: detectorId,
FindingCriteria: { Criterion: { severity: { Gte: severity } } },
MaxResults: limit,
});
const listResponse = await guardDutyClient.send(listCmd);
if (
!listResponse.FindingIds ||
listResponse.FindingIds.length === 0
) {
return {
content: [{ type: "text", text: "No findings found." }],
};
}
const getCmd = new GetFindingsCommand({
DetectorId: detectorId,
FindingIds: listResponse.FindingIds,
});
const getResponse = await guardDutyClient.send(getCmd);
const findings =
getResponse.Findings?.map((f) => ({
Title: f.Title,
Severity: f.Severity,
Type: f.Type,
Region: f.Region,
ResourceId:
f.Resource?.InstanceDetails?.InstanceId || "N/A",
})) || [];
return {
content: [
{ type: "text", text: JSON.stringify(findings, null, 2) },
],
};
}
if (name === "get_recent_logs") {
const groupName = (args as any).log_group_name;
const limit = (args as any)?.limit || 20;
// Get latest stream
const streamCmd = new DescribeLogStreamsCommand({
logGroupName: groupName,
orderBy: "LastEventTime",
descending: true,
limit: 1,
});
try {
const streamResp = await cloudWatchLogsClient.send(streamCmd);
const streamName = streamResp.logStreams?.[0]?.logStreamName;
if (!streamName) {
return {
content: [
{ type: "text", text: "No log streams found." },
],
};
}
const eventsCmd = new GetLogEventsCommand({
logGroupName: groupName,
logStreamName: streamName,
limit: limit,
startFromHead: false,
});
const eventsResp = await cloudWatchLogsClient.send(eventsCmd);
const logs =
eventsResp.events?.map((e) => ({
Timestamp: new Date(e.timestamp || 0).toISOString(),
Message: e.message,
})) || [];
return {
content: [
{ type: "text", text: JSON.stringify(logs, null, 2) },
],
};
} catch (err: any) {
return {
content: [
{
type: "text",
text: `Error fetching logs: ${err.message}`,
},
],
isError: true,
};
}
}
if (name === "search_cloudwatch_logs") {
const groupName = (args as any).log_group_name;
const filterPattern = (args as any).filter_pattern;
const limit = (args as any)?.limit || 50;
const hours = (args as any)?.hours || 24;
const endTime = (args as any)?.end_time
? new Date((args as any).end_time).getTime()
: Date.now();
const startTime = (args as any)?.start_time
? new Date((args as any).start_time).getTime()
: endTime - hours * 60 * 60 * 1000;
try {
const command = new FilterLogEventsCommand({
logGroupName: groupName,
filterPattern: filterPattern,
startTime: startTime,
endTime: endTime,
limit: limit,
});
const response = await cloudWatchLogsClient.send(command);
const events =
response.events?.map((e) => ({
Timestamp: new Date(e.timestamp || 0).toISOString(),
Message: e.message,
LogStreamName: e.logStreamName,
})) || [];
if (events.length === 0) {
return {
content: [
{ type: "text", text: "No matching logs found." },
],
};
}
return {
content: [
{ type: "text", text: JSON.stringify(events, null, 2) },
],
};
} catch (err: any) {
return {
content: [
{
type: "text",
text: `Error searching logs: ${err.message}`,
},
],
isError: true,
};
}
}
if (name === "list_cloudtrail_changes") {
const resourceId = (args as any)?.resource_id;
const lookupKey =
(args as any)?.lookup_key ||
(resourceId ? "ResourceName" : undefined);
const lookupValue = resourceId || (args as any)?.lookup_value;
const days = (args as any)?.days || 7;
if (!lookupKey || !lookupValue) {
return {
content: [
{
type: "text",
text: "Please provide a resource_id OR a lookup_key and lookup_value.",
},
],
isError: true,
};
}
const startTime = new Date(Date.now() - days * 24 * 60 * 60 * 1000);
const command = new LookupEventsCommand({
LookupAttributes: [
{ AttributeKey: lookupKey, AttributeValue: lookupValue },
],
StartTime: startTime,
MaxResults: 50,
});
const response = await cloudTrailClient.send(command);
// Filter for mutations (not ReadOnly)
// Note: 'ReadOnly' field in event isn't always populated in LookupEvents response types directly in all SDK versions,
// but we can infer or it is often there. Some events don't have it.
// We'll primarily rely on showing the event name and letting user see.
// But we can try to filter if resource JSON is parsed.
const events =
response.Events?.map((e) => {
let isReadOnly = true;
// Try to guess read-only if not explicit.
// Usually "Get", "Describe", "List" are read. "Create", "Update", "Delete", "Put", "Modify" are write.
const name = e.EventName || "";
if (
name.startsWith("Get") ||
name.startsWith("Describe") ||
name.startsWith("List")
) {
isReadOnly = true;
} else {
isReadOnly = false;
}
// If Resources tag is present, it's useful
return {
EventTime: e.EventTime,
EventName: e.EventName,
Username: e.Username,
EventSource: e.EventSource,
ResourceName: e.Resources?.[0]?.ResourceName,
IsAssumedReadOnly: isReadOnly,
};
}).filter((e) => !e.IsAssumedReadOnly) || []; // Show only changes
return {
content: [
{ type: "text", text: JSON.stringify(events, null, 2) },
],
};
}
if (name === "list_access_denied_events") {
const limit = (args as any)?.limit || 20;
// LookupEvents doesn't natively support filtering by 'AccessDenied' error code directly via LookupAttributes
// the way we want (it allows specific keys).
// Best approach: Fetch recent events and client-side filter for ErrorCode.
const command = new LookupEventsCommand({
MaxResults: 50, // Fetch a bit more to filter
});
const response = await cloudTrailClient.send(command);
// Note: LookupEvents output (Events) doesn't always contain ErrorCode as a top-level field?
// Actually, LookupEvents output contains 'CloudTrailEvent' string which has the full JSON.
const deniedEvents =
response.Events?.map((e) => {
let errorCode = "N/A";
let errorMessage = "N/A";
if (e.CloudTrailEvent) {
try {
const json = JSON.parse(e.CloudTrailEvent);
errorCode = json.errorCode;
errorMessage = json.errorMessage;
} catch (_err) {
/* JSON parse failed */
}
}
return {
EventTime: e.EventTime,
EventName: e.EventName,
Username: e.Username,
ErrorCode: errorCode,
ErrorMessage: errorMessage,
};
})
.filter(
(e) =>
e.ErrorCode &&
(e.ErrorCode === "AccessDenied" ||
e.ErrorCode ===
"Client.UnauthorizedOperation" ||
e.ErrorCode.includes("Unauthorized"))
)
.slice(0, limit) || [];
return {
content: [
{
type: "text",
text: JSON.stringify(deniedEvents, null, 2),
},
],
};
}
if (name === "get_service_health") {
const command = new DescribeEventsCommand({
filter: { eventStatusCodes: ["open", "upcoming"] },
});
const response = await healthClient.send(command);
const events =
response.events?.map((e) => ({
EventTypeCode: e.eventTypeCode,
Service: e.service,
Region: e.region,
StartTime: e.startTime,
Status: e.statusCode,
Description: e.eventScopeCode,
})) || [];
return {
content: [
{ type: "text", text: JSON.stringify(events, null, 2) },
],
};
}
if (name === "list_load_balancers") {
const command = new DescribeLoadBalancersCommand({});
const response = await elbv2Client.send(command);
const lbs =
response.LoadBalancers?.map((lb) => ({
LoadBalancerName: lb.LoadBalancerName,
DNSName: lb.DNSName,
Type: lb.Type,
Scheme: lb.Scheme,
VpcId: lb.VpcId,
State: lb.State?.Code,
LoadBalancerArn: lb.LoadBalancerArn,
})) || [];
return {
content: [{ type: "text", text: JSON.stringify(lbs, null, 2) }],
};
}
if (name === "list_target_groups") {
const lbArn = (args as any)?.load_balancer_arn;
const command = new DescribeTargetGroupsCommand(
lbArn ? { LoadBalancerArn: lbArn } : {}
);
const response = await elbv2Client.send(command);
const tgs =
response.TargetGroups?.map((tg) => ({
TargetGroupName: tg.TargetGroupName,
Protocol: tg.Protocol,
Port: tg.Port,
TargetType: tg.TargetType,
TargetGroupArn: tg.TargetGroupArn,
LoadBalancerArns: tg.LoadBalancerArns,
})) || [];
return {
content: [{ type: "text", text: JSON.stringify(tgs, null, 2) }],
};
}
if (name === "list_listener_rules") {
const lbArn = (args as any).load_balancer_arn;
const listenersCmd = new DescribeListenersCommand({
LoadBalancerArn: lbArn,
});
const listenersResp = await elbv2Client.send(listenersCmd);
const listeners = listenersResp.Listeners || [];
const detailedListeners = [];
for (const listener of listeners) {
if (!listener.ListenerArn) continue;
const rulesCmd = new DescribeRulesCommand({
ListenerArn: listener.ListenerArn,
});
const rulesResp = await elbv2Client.send(rulesCmd);
detailedListeners.push({
ListenerArn: listener.ListenerArn,
Port: listener.Port,
Protocol: listener.Protocol,
Rules: rulesResp.Rules?.map((r) => ({
Priority: r.Priority,
Conditions: r.Conditions?.map((c) => ({
Field: c.Field,
Values: c.Values,
HostHeaderConfig: c.HostHeaderConfig,
PathPatternConfig: c.PathPatternConfig,
})),
Actions: r.Actions?.map((a) => ({
Type: a.Type,
TargetGroupArn: a.TargetGroupArn,
})),
IsDefault: r.IsDefault,
})),
});
}
return {
content: [
{
type: "text",
text: JSON.stringify(detailedListeners, null, 2),
},
],
};
}
if (name === "get_target_health") {
const tgArn = (args as any)?.target_group_arn;
const command = new DescribeTargetHealthCommand({
TargetGroupArn: tgArn,
});
const response = await elbv2Client.send(command);
const healths =
response.TargetHealthDescriptions?.map((th) => ({
Target: { Id: th.Target?.Id, Port: th.Target?.Port },
State: th.TargetHealth?.State,
Reason: th.TargetHealth?.Reason,
Description: th.TargetHealth?.Description,
})) || [];
return {
content: [
{ type: "text", text: JSON.stringify(healths, null, 2) },
],
};
}
if (name === "list_web_acls") {
const scope = (args as any)?.scope || "REGIONAL";
const command = new ListWebACLsCommand({ Scope: scope });
const response = await wafv2Client.send(command);
const acls =
response.WebACLs?.map((acl) => ({
Name: acl.Name,
Id: acl.Id,
ARN: acl.ARN,
Description: acl.Description,
})) || [];
return {
content: [
{ type: "text", text: JSON.stringify(acls, null, 2) },
],
};
}
if (name === "get_waf_sampled_requests") {
const aclArn = (args as any)?.web_acl_arn;
const metricName = (args as any)?.rule_metric_name;
const scope = (args as any)?.scope || "REGIONAL";
const timeWindow = (args as any)?.time_window_seconds || 3600;
// WAFv2 Sampled Requests requires a time window
const endTime = new Date();
const startTime = new Date(endTime.getTime() - timeWindow * 1000);
const command = new GetSampledRequestsCommand({
WebAclArn: aclArn,
RuleMetricName: metricName,
Scope: scope,
TimeWindow: { StartTime: startTime, EndTime: endTime },
MaxItems: 100,
});
const response = await wafv2Client.send(command);
const requests =
response.SampledRequests?.map((r) => ({
ClientIP: r.Request?.ClientIP,
Country: r.Request?.Country,
URI: r.Request?.URI,
Method: r.Request?.Method,
Headers: r.Request?.Headers,
Action: r.Action,
Timestamp: r.Timestamp,
})) || [];
return {
content: [
{ type: "text", text: JSON.stringify(requests, null, 2) },
],
};
}
if (name === "check_ip_in_waf") {
const ip = (args as any)?.ip_address;
const scopes: ("REGIONAL" | "CLOUDFRONT")[] = [
"REGIONAL",
"CLOUDFRONT",
];
const foundIn: any[] = [];
for (const scope of scopes) {
try {
const listCmd = new ListIPSetsCommand({
Scope: scope,
Limit: 100,
});
const listResp = await wafv2Client.send(listCmd);
const ipSets = listResp.IPSets || [];
for (const setSummary of ipSets) {
if (!setSummary.Name || !setSummary.Id) continue;
const getCmd = new GetIPSetCommand({
Name: setSummary.Name,
Id: setSummary.Id,
Scope: scope,
});
const getResp = await wafv2Client.send(getCmd);
const addresses = getResp.IPSet?.Addresses || [];
if (checkIp(ip, addresses)) {
foundIn.push({
IPSetName: setSummary.Name,
IPSetId: setSummary.Id,
IPSetARN: setSummary.ARN,
Scope: scope,
Description: getResp.IPSet?.Description,
});
}
}
} catch (err) {
console.error(`Error checking WAF scope ${scope}:`, err);
}
}
if (foundIn.length === 0) {
return {
content: [
{
type: "text",
text: `IP ${ip} not found in any WAF IP Sets.`,
},
],
};
}
return {
content: [
{ type: "text", text: JSON.stringify(foundIn, null, 2) },
],
};
}
if (name === "get_metric_statistics") {
const {
namespace,
metric_name,
dimensions,
start_time,
end_time,
period,
statistics,
} = args as any;
// Defaults
const actualStartTime = start_time
? new Date(start_time)
: new Date(Date.now() - 24 * 60 * 60 * 1000); // 24h ago
const actualEndTime = end_time ? new Date(end_time) : new Date();
const actualPeriod = period || 300; // 5 mins
const actualStats = statistics || ["Average"];
// Convert dimensions to right format: { Name, Value } is already expected from args.
const command = new GetMetricStatisticsCommand({
Namespace: namespace,
MetricName: metric_name,
Dimensions: dimensions,
StartTime: actualStartTime,
EndTime: actualEndTime,
Period: actualPeriod,
Statistics: actualStats,
});
const response = await cloudWatchClient.send(command);
const datapoints =
response.Datapoints?.sort(
(a, b) =>
(a.Timestamp?.getTime() || 0) -
(b.Timestamp?.getTime() || 0)
).map((dp) => ({
Timestamp: dp.Timestamp,
Average: dp.Average,
Maximum: dp.Maximum,
Minimum: dp.Minimum,
Sum: dp.Sum,
SampleCount: dp.SampleCount,
Unit: dp.Unit,
})) || [];
return {
content: [
{ type: "text", text: JSON.stringify(datapoints, null, 2) },
],
};
}
if (name === "list_sns_topics") {
const command = new ListTopicsCommand({});
const response = await snsClient.send(command);
const topics =
response.Topics?.map((t) => ({ TopicArn: t.TopicArn })) || [];
return {
content: [
{ type: "text", text: JSON.stringify(topics, null, 2) },
],
};
}
if (name === "list_record_sets") {
const zoneId = (args as any)?.hosted_zone_id;
const command = new ListResourceRecordSetsCommand({
HostedZoneId: zoneId,
});
const response = await route53Client.send(command);
const records =
response.ResourceRecordSets?.map((r) => ({
Name: r.Name,
Type: r.Type,
TTL: r.TTL,
ResourceRecords: r.ResourceRecords,
AliasTarget: r.AliasTarget,
})) || [];
return {
content: [
{ type: "text", text: JSON.stringify(records, null, 2) },
],
};
}
if (name === "list_hosted_zones") {
const command = new ListHostedZonesCommand({});
const response = await route53Client.send(command);
const zones =
response.HostedZones?.map((z) => ({
Id: z.Id,
Name: z.Name,
Config: z.Config,
ResourceRecordSetCount: z.ResourceRecordSetCount,
})) || [];
return {
content: [
{ type: "text", text: JSON.stringify(zones, null, 2) },
],
};
}
if (name === "list_ecs_clusters") {
const maxResults = Math.min(
Math.max(
((args as Record<string, unknown>)
?.max_results as number) ?? 100,
1
),
100
);
const command = new ListClustersCommand({ maxResults });
const response = await ecsClient.send(command);
// Detail describe to get task counts
const clusters = response.clusterArns || [];
if (clusters.length === 0)
return { content: [{ type: "text", text: "[]" }] };
const descParams = { clusters: clusters };
const descCommand = new DescribeClustersCommand(descParams);
const descResponse = await ecsClient.send(descCommand);
const clusterDetails =
descResponse.clusters?.map((c) => ({
clusterName: c.clusterName,
status: c.status,
runningTasksCount: c.runningTasksCount,
pendingTasksCount: c.pendingTasksCount,
activeServicesCount: c.activeServicesCount,
})) || [];
return {
content: [
{
type: "text",
text: JSON.stringify(clusterDetails, null, 2),
},
],
};
}
if (name === "list_ecs_services") {
const cluster = (args as any).cluster;
const command = new ListServicesCommand({ cluster });
const response = await ecsClient.send(command);
const services = response.serviceArns || [];
if (services.length === 0)
return { content: [{ type: "text", text: "[]" }] };
// Describe for more info
const batch = services.slice(0, 10);
const descCommand = new DescribeServicesCommand({
cluster,
services: batch,
});
const descResponse = await ecsClient.send(descCommand);
const serviceDetails =
descResponse.services?.map((s) => ({
serviceName: s.serviceName,
status: s.status,
desiredCount: s.desiredCount,
runningCount: s.runningCount,
pendingCount: s.pendingCount,
taskDefinition: s.taskDefinition,
})) || [];
return {
content: [
{
type: "text",
text: JSON.stringify(serviceDetails, null, 2),
},
],
};
}
if (name === "list_eks_clusters") {
const command = new ListEksClustersCommand({});
const response = await eksClient.send(command);
return {
content: [
{
type: "text",
text: JSON.stringify(response.clusters || [], null, 2),
},
],
};
}
if (name === "list_auto_scaling_groups") {
const command = new DescribeAutoScalingGroupsCommand({});
const response = await asgClient.send(command);
const asgs =
response.AutoScalingGroups?.map((g) => ({
AutoScalingGroupName: g.AutoScalingGroupName,
MinSize: g.MinSize,
MaxSize: g.MaxSize,
DesiredCapacity: g.DesiredCapacity,
Instances: g.Instances?.length,
})) || [];
return {
content: [
{ type: "text", text: JSON.stringify(asgs, null, 2) },
],
};
}
if (name === "list_scaling_activities") {
const groupName = (args as any).auto_scaling_group_name;
const command = new DescribeScalingActivitiesCommand({
AutoScalingGroupName: groupName,
MaxRecords: 10,
});
const response = await asgClient.send(command);
const activities =
response.Activities?.map((a) => ({
ActivityId: a.ActivityId,
Description: a.Description,
Cause: a.Cause,
StartTime: a.StartTime,
StatusCode: a.StatusCode,
})) || [];
return {
content: [
{ type: "text", text: JSON.stringify(activities, null, 2) },
],
};
}
if (name === "list_cloudfront_distributions") {
const command = new ListDistributionsCommand({});
const response = await cloudFrontClient.send(command);
const dists =
response.DistributionList?.Items?.map((d) => ({
Id: d.Id,
DomainName: d.DomainName,
Status: d.Status,
Enabled: d.Enabled,
Aliases: d.Aliases?.Items,
})) || [];
return {
content: [
{ type: "text", text: JSON.stringify(dists, null, 2) },
],
};
}
if (name === "list_secrets") {
const command = new ListSecretsCommand({});
const response = await secretsManagerClient.send(command);
const secrets =
response.SecretList?.map((s) => ({
Name: s.Name,
Description: s.Description,
})) || [];
return {
content: [
{ type: "text", text: JSON.stringify(secrets, null, 2) },
],
};
}
if (name === "list_ssm_parameters") {
// DescribeParameters mainly lists them
const command = new DescribeParametersCommand({});
const response = await ssmClient.send(command);
const params =
response.Parameters?.map((p) => ({
Name: p.Name,
Type: p.Type,
Description: p.Description,
})) || [];
return {
content: [
{ type: "text", text: JSON.stringify(params, null, 2) },
],
};
}
if (name === "list_cloudformation_stacks") {
const maxResults = Math.min(
Math.max(
((args as Record<string, unknown>)
?.max_results as number) ?? 100,
1
),
100
);
const command = new ListStacksCommand({
StackStatusFilter: [
"CREATE_COMPLETE",
"UPDATE_COMPLETE",
"ROLLBACK_COMPLETE",
"CREATE_IN_PROGRESS",
"UPDATE_IN_PROGRESS",
],
});
const response = await cfnClient.send(command);
const stacks = (
response.StackSummaries?.map((s) => ({
StackName: s.StackName,
StackStatus: s.StackStatus,
DriftInformation: s.DriftInformation,
CreationTime: s.CreationTime,
})) || []
).slice(0, maxResults);
return {
content: [
{ type: "text", text: JSON.stringify(stacks, null, 2) },
],
};
}
if (name === "list_dynamodb_tables") {
const limit = Math.min(
Math.max(
((args as Record<string, unknown>)
?.max_results as number) ?? 100,
1
),
100
);
const command = new ListTablesCommand({ Limit: limit });
const response = await dynamoDbClient.send(command);
return {
content: [
{
type: "text",
text: JSON.stringify(
response.TableNames || [],
null,
2
),
},
],
};
}
if (name === "list_trusted_advisor_checks") {
try {
const command = new DescribeTrustedAdvisorChecksCommand({
language: "en",
});
const response = await supportClient.send(command);
const checks =
response.checks?.map((c) => ({
id: c.id,
name: c.name,
category: c.category,
})) || [];
return {
content: [
{ type: "text", text: JSON.stringify(checks, null, 2) },
],
};
} catch (error) {
// Return clear error if Support API is not available (e.g. Basic Support plan)
return {
content: [
{
type: "text",
text: JSON.stringify({
error: "Trusted Advisor check failed. Ensure you have Business/Enterprise support or access.",
details: (error as Error).message,
}),
},
],
};
}
}
if (name === "estimate_cost") {
const a = args as {
service: string;
region?: string;
quantity?: number;
unit?: string;
};
const service = a.service || "ec2";
const region = a.region || "us-east-1";
const qty = a.quantity ?? 1;
const unit = a.unit || "t3.micro";
const rates: Record<string, Record<string, number>> = {
ec2: {
"t3.micro": 0.0104,
"t3.small": 0.0208,
"t3.medium": 0.0416,
"m5.large": 0.096,
},
lambda: {
"128": 0.0000002,
"256": 0.0000004,
"512": 0.0000008,
},
rds: { "db.t3.micro": 0.017, "db.t3.small": 0.034 },
s3: { gb: 0.023 },
};
const svcRates = rates[service];
const rate = svcRates?.[unit] ?? (service === "s3" ? 0.023 : 0.01);
let estimate: number;
if (service === "s3") estimate = qty * rate;
else if (service === "lambda") estimate = qty * 1000000 * rate;
else estimate = qty * 730 * rate;
const text = JSON.stringify(
{
service,
region,
unit,
quantity: qty,
estimatedMonthlyUSD: estimate.toFixed(4),
note: "Approximate; actual costs vary.",
},
null,
2
);
return succeed({ content: [{ type: "text", text }] });
}
if (name === "scan_secrets_risks") {
const maxResults = Math.min(
Math.max(
((args as Record<string, unknown>)
?.max_results as number) ?? 50,
1
),
100
);
const command = new ListSecretsCommand({ MaxResults: maxResults });
const response = await secretsManagerClient.send(command);
const risky = (response.SecretList || [])
.map((s) => {
const name = (s.Name || "").toLowerCase();
const sensitiveKeywords = [
"password",
"secret",
"key",
"token",
"credential",
];
const hasSensitiveName = sensitiveKeywords.some((k) =>
name.includes(k)
);
const lastChanged = s.LastChangedDate;
const daysSinceChange = lastChanged
? Math.floor(
(Date.now() - lastChanged.getTime()) /
(24 * 60 * 60 * 1000)
)
: null;
const noRotation = !s.RotationEnabled;
const score =
(hasSensitiveName ? 1 : 0) +
(noRotation ? 1 : 0) +
(daysSinceChange !== null && daysSinceChange > 90
? 1
: 0);
return {
Name: s.Name,
HasSensitiveName: hasSensitiveName,
RotationEnabled: s.RotationEnabled ?? false,
DaysSinceLastChange: daysSinceChange,
RiskScore: score,
};
})
.filter((r) => r.RiskScore > 0)
.sort((a, b) => b.RiskScore - a.RiskScore);
return succeed({
content: [
{
type: "text",
text: JSON.stringify(risky, null, 2),
},
],
});
}
if (name === "aws_health_check") {
try {
const cmd = new GetCallerIdentityCommand({});
const res = await withRetry(() => stsClient.send(cmd));
return {
content: [
{
type: "text",
text: JSON.stringify(
{
status: "ok",
account: res.Account,
userId: res.UserId,
message:
"AWS credentials are valid and connectivity is working.",
},
null,
2
),
},
],
};
} catch (err) {
const classified = classifyError(err);
return {
content: [
{
type: "text",
text: JSON.stringify(
{
status: "error",
message: classified.userMessage,
},
null,
2
),
},
],
isError: true,
};
}
}
if (name === "get_iam_policy_for_tools") {
const toolNames =
(args as { tool_names?: string[] })?.tool_names ||
Object.keys(TOOL_IAM_MAPPING).filter(
(k) =>
![
"aws_health_check",
"get_iam_policy_for_tools",
].includes(k)
);
const policy = generatePolicyForTools(toolNames);
return {
content: [
{
type: "text",
text: policy,
},
],
};
}
throw new Error(`Unknown tool: ${name}`);
} catch (error: unknown) {
if (config.auditLog) logToolInvocation(name, false);
const cfg = loadConfig();
if (cfg.webhookUrl) {
notifyWebhook(cfg.webhookUrl, {
tool: name,
timestamp: new Date().toISOString(),
success: false,
});
}
logger.error("Tool failed", {
tool: name,
error: (error as Error).message,
});
const classified = classifyError(error);
return {
content: [
{
type: "text",
text: `Error executing ${name}: ${classified.userMessage}`,
},
],
isError: true,
};
}
});
// MCP Resources: expose AWS resource URIs (aws://region/service/id)
server.setRequestHandler(ListResourcesRequestSchema, async () => {
const region = process.env.AWS_REGION || "us-east-1";
return {
resources: [
{
uri: `aws://${region}/identity/caller`,
name: "AWS Caller Identity",
description: "Current IAM caller identity",
mimeType: "application/json",
},
{
uri: `aws://${region}/ec2/instances`,
name: "EC2 Instances",
description: "List of EC2 instances",
mimeType: "application/json",
},
{
uri: `aws://${region}/s3/buckets`,
name: "S3 Buckets",
description: "List of S3 buckets",
mimeType: "application/json",
},
{
uri: `aws://${region}/cost/recent`,
name: "Recent AWS Cost",
description: "Recent cost data",
mimeType: "application/json",
},
{
uri: `aws://${region}/rds/instances`,
name: "RDS Instances",
description: "List of RDS instances",
mimeType: "application/json",
},
{
uri: `aws://${region}/lambda/functions`,
name: "Lambda Functions",
description: "List of Lambda functions",
mimeType: "application/json",
},
{
uri: `aws://${region}/guardduty/findings`,
name: "GuardDuty Findings",
description: "GuardDuty security findings",
mimeType: "application/json",
},
],
};
});
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const uri = request.params.uri;
const match = uri.match(/^aws:\/\/([^/]+)\/([^/]+)\/(.+)$/);
if (!match) {
throw new Error(`Invalid AWS resource URI: ${uri}`);
}
const [, region, service, resource] = match;
let contents: string;
if (service === "identity" && resource === "caller") {
const cmd = new GetCallerIdentityCommand({});
const res = await stsClient.send(cmd);
contents = JSON.stringify(
{ UserId: res.UserId, Account: res.Account, Arn: res.Arn },
null,
2
);
} else if (service === "ec2" && resource === "instances") {
const client = getEc2Client(region);
const res = await client.send(new DescribeInstancesCommand({}));
const instances =
res.Reservations?.flatMap(
(r) =>
r.Instances?.map((i) => ({
InstanceId: i.InstanceId,
State: i.State?.Name,
InstanceType: i.InstanceType,
})) || []
) || [];
contents = JSON.stringify(instances, null, 2);
} else if (service === "s3" && resource === "buckets") {
const res = await s3Client.send(new ListBucketsCommand({}));
const buckets = res.Buckets?.map((b) => ({ Name: b.Name })) || [];
contents = JSON.stringify(buckets, null, 2);
} else if (service === "cost" && resource === "recent") {
const end = new Date().toISOString().split("T")[0];
const start = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
.toISOString()
.split("T")[0];
const res = await costExplorerClient.send(
new GetCostAndUsageCommand({
TimePeriod: { Start: start, End: end },
Granularity: "DAILY",
Metrics: ["UnblendedCost"],
})
);
contents = JSON.stringify(res.ResultsByTime || [], null, 2);
} else if (service === "rds" && resource === "instances") {
const client = getRdsClient(region);
const res = await client.send(new DescribeDBInstancesCommand({}));
const instances =
res.DBInstances?.map((db) => ({
DBInstanceIdentifier: db.DBInstanceIdentifier,
Engine: db.Engine,
DBInstanceStatus: db.DBInstanceStatus,
})) || [];
contents = JSON.stringify(instances, null, 2);
} else if (service === "lambda" && resource === "functions") {
const client = getLambdaClient(region);
const res = await client.send(new ListFunctionsCommand({}));
const funcs =
res.Functions?.map((f) => ({
FunctionName: f.FunctionName,
Runtime: f.Runtime,
})) || [];
contents = JSON.stringify(funcs, null, 2);
} else if (service === "guardduty" && resource === "findings") {
const detectorsRes = await guardDutyClient.send(
new ListDetectorsCommand({})
);
const detectorId = detectorsRes.DetectorIds?.[0];
if (!detectorId) {
contents = JSON.stringify({ findings: [], message: "No detector" });
} else {
const listRes = await guardDutyClient.send(
new ListFindingsCommand({
DetectorId: detectorId,
MaxResults: 10,
})
);
const findingIds = listRes.FindingIds || [];
if (findingIds.length === 0) {
contents = JSON.stringify({ findings: [] });
} else {
const getRes = await guardDutyClient.send(
new GetFindingsCommand({
DetectorId: detectorId,
FindingIds: findingIds,
})
);
const findings =
getRes.Findings?.map((f) => ({
Title: f.Title,
Severity: f.Severity,
Type: f.Type,
})) || [];
contents = JSON.stringify(findings, null, 2);
}
}
} else {
throw new Error(`Unknown resource: ${uri}`);
}
return {
contents: [{ uri, mimeType: "application/json", text: contents }],
};
});
// MCP Prompts: help AI choose the right tool
server.setRequestHandler(ListPromptsRequestSchema, async () => ({
prompts: [
{
name: "aws_cost_inquiry",
description: "User wants to check AWS costs or spending",
arguments: [],
},
{
name: "aws_security_check",
description:
"User wants to audit security (MFA, keys, certs, GuardDuty)",
arguments: [],
},
{
name: "aws_resource_list",
description:
"User wants to list AWS resources (EC2, S3, RDS, etc.)",
arguments: [],
},
],
}));
const PROMPT_CONTENT: Record<
string,
{ role: "user"; content: { type: "text"; text: string }[] }
> = {
aws_cost_inquiry: {
role: "user",
content: [
{
type: "text",
text: "Use these tools for cost inquiries: get_recent_cost (daily costs), get_cost_by_service (by service), get_cost_breakdown (detailed), get_cost_forecast, get_budget_details, get_cost_anomalies.",
},
],
},
aws_security_check: {
role: "user",
content: [
{
type: "text",
text: "Use these tools for security audit: list_users_without_mfa, list_old_access_keys, list_expiring_certificates, list_guardduty_findings, list_access_denied_events, list_open_security_groups.",
},
],
},
aws_resource_list: {
role: "user",
content: [
{
type: "text",
text: "Use these tools to list resources: list_ec2_instances, list_s3_buckets, list_rds_instances, list_lambda_functions, list_vpcs, list_ecs_clusters, list_eks_clusters. Add region parameter for multi-region.",
},
],
},
};
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
const prompt = PROMPT_CONTENT[request.params.name];
if (!prompt) throw new Error(`Unknown prompt: ${request.params.name}`);
return { messages: [prompt] };
});
// Start the server
const transport = new StdioServerTransport();
await server.connect(transport);