1af1db7ffc
- AWSService: SigV4 request signing, XML parser, credential management - AWSS3Service: bucket/object CRUD, website hosting, deploy pipeline - AWSRoute53Service: hosted zones, DNS records, S3/CloudFront aliases - AWSCloudFrontService: distributions, cache invalidation, ACM certs - AWSLambdaService: functions, invoke, zip deploy - AWSTools: 15 MCP agent tools for all AWS operations - CredentialStore: AWS credential convenience properties - AWSAndWebsiteTests: 72 tests covering models, XML parsing, errors, credentials, templates, deploy providers (165 total tests passing)
688 lines
33 KiB
Swift
688 lines
33 KiB
Swift
// AWSTools.swift
|
|
// CxIDE Agent — AWS integration tools for S3, CloudFront, Route 53, Lambda, and ACM
|
|
//
|
|
// 14 tools: aws_configure, aws_s3_list, aws_s3_upload, aws_s3_deploy,
|
|
// aws_s3_download, aws_s3_delete, aws_cloudfront_list,
|
|
// aws_cloudfront_create, aws_cloudfront_invalidate,
|
|
// aws_route53_zones, aws_route53_records, aws_route53_set,
|
|
// aws_lambda_list, aws_lambda_invoke
|
|
|
|
import Foundation
|
|
|
|
enum AWSTools {
|
|
static func register(on server: MCPServer, config: AgentConfig, memory: AgentMemory) {
|
|
|
|
// ── aws_configure ─────────────────────────────────────────────
|
|
server.registerTool(
|
|
"aws_configure",
|
|
description: "Configure AWS credentials and default region. Credentials are AES-256 encrypted locally.",
|
|
inputSchema: [
|
|
"type": "object",
|
|
"required": ["action"],
|
|
"properties": [
|
|
"action": ["type": "string", "description": "status, set, or set-region."],
|
|
"access_key_id": ["type": "string", "description": "AWS Access Key ID (for set)."],
|
|
"secret_access_key": ["type": "string", "description": "AWS Secret Access Key (for set)."],
|
|
"region": ["type": "string", "description": "AWS region (default: us-east-1)."],
|
|
] as [String: Any],
|
|
] as [String: Any]
|
|
) { args in
|
|
let action = args["action"] as? String ?? "status"
|
|
let store = CredentialStore.shared
|
|
let wsDir = URL(fileURLWithPath: config.workspaceRoot)
|
|
try? store.load(from: wsDir)
|
|
|
|
switch action.lowercased() {
|
|
case "status":
|
|
let aws = AWSService.shared
|
|
let hasKey = aws.accessKeyId != nil
|
|
let hasSecret = aws.secretAccessKey != nil
|
|
let region = aws.defaultRegion
|
|
var output = "AWS Configuration:\n"
|
|
output += " Access Key: \(hasKey ? "✅ Configured (\(aws.accessKeyId?.prefix(8) ?? "")...)" : "❌ Not set")\n"
|
|
output += " Secret Key: \(hasSecret ? "✅ Configured" : "❌ Not set")\n"
|
|
output += " Region: \(region)\n"
|
|
output += " Authenticated: \(aws.isAuthenticated ? "✅ Yes" : "❌ No")\n"
|
|
return ok(output)
|
|
|
|
case "set":
|
|
guard let keyId = args["access_key_id"] as? String, !keyId.isEmpty,
|
|
let secret = args["secret_access_key"] as? String, !secret.isEmpty else {
|
|
return err("access_key_id and secret_access_key required")
|
|
}
|
|
let region = args["region"] as? String ?? "us-east-1"
|
|
AWSService.shared.setCredentials(accessKeyId: keyId, secretAccessKey: secret, region: region)
|
|
do {
|
|
try store.save(to: wsDir)
|
|
return ok("AWS credentials saved (encrypted). Region: \(region)")
|
|
} catch {
|
|
return err("Failed to save: \(error.localizedDescription)")
|
|
}
|
|
|
|
case "set-region":
|
|
let region = args["region"] as? String ?? "us-east-1"
|
|
store.set("AWS_DEFAULT_REGION", value: region)
|
|
do {
|
|
try store.save(to: wsDir)
|
|
return ok("AWS region set to: \(region)")
|
|
} catch {
|
|
return err("Failed to save: \(error.localizedDescription)")
|
|
}
|
|
|
|
default:
|
|
return err("Unknown action '\(action)'. Available: status, set, set-region")
|
|
}
|
|
}
|
|
|
|
// ── aws_s3_list ───────────────────────────────────────────────
|
|
server.registerTool(
|
|
"aws_s3_list",
|
|
description: "List S3 buckets or objects in a bucket.",
|
|
inputSchema: [
|
|
"type": "object",
|
|
"properties": [
|
|
"bucket": ["type": "string", "description": "Bucket name (omit to list all buckets)."],
|
|
"prefix": ["type": "string", "description": "Object key prefix filter."],
|
|
"region": ["type": "string", "description": "AWS region."],
|
|
] as [String: Any],
|
|
] as [String: Any],
|
|
annotations: ToolAnnotations(readOnlyHint: true)
|
|
) { args in
|
|
let s3 = AWSS3Service.shared
|
|
do {
|
|
if let bucket = args["bucket"] as? String, !bucket.isEmpty {
|
|
let prefix = args["prefix"] as? String
|
|
let region = args["region"] as? String
|
|
let objects = try await s3.listObjects(bucket: bucket, prefix: prefix, region: region)
|
|
if objects.isEmpty {
|
|
return ok("Bucket '\(bucket)' is empty\(prefix != nil ? " (prefix: \(prefix!))" : "")")
|
|
}
|
|
var output = "Objects in s3://\(bucket)\(prefix != nil ? "/\(prefix!)" : "") (\(objects.count)):\n\n"
|
|
output += String(format: "%-50s %10s %s\n", "Key", "Size", "Last Modified")
|
|
output += String(repeating: "-", count: 85) + "\n"
|
|
for obj in objects {
|
|
output += String(format: "%-50s %10s %s\n",
|
|
String(obj.key.prefix(50)), obj.formattedSize, obj.lastModified ?? "")
|
|
}
|
|
return ok(output)
|
|
} else {
|
|
let buckets = try await s3.listBuckets()
|
|
if buckets.isEmpty {
|
|
return ok("No S3 buckets found in this account.")
|
|
}
|
|
var output = "S3 Buckets (\(buckets.count)):\n\n"
|
|
for b in buckets {
|
|
output += " • \(b.name)"
|
|
if let date = b.creationDate { output += " (created: \(date))" }
|
|
output += "\n"
|
|
}
|
|
return ok(output)
|
|
}
|
|
} catch {
|
|
return err(error.localizedDescription)
|
|
}
|
|
}
|
|
|
|
// ── aws_s3_upload ─────────────────────────────────────────────
|
|
server.registerTool(
|
|
"aws_s3_upload",
|
|
description: "Upload a file to S3.",
|
|
inputSchema: [
|
|
"type": "object",
|
|
"required": ["bucket", "file"],
|
|
"properties": [
|
|
"bucket": ["type": "string", "description": "S3 bucket name."],
|
|
"file": ["type": "string", "description": "Local file path."],
|
|
"key": ["type": "string", "description": "S3 object key (default: filename)."],
|
|
"region": ["type": "string", "description": "AWS region."],
|
|
] as [String: Any],
|
|
] as [String: Any]
|
|
) { args in
|
|
guard let bucket = args["bucket"] as? String,
|
|
let filePath = args["file"] as? String,
|
|
let resolved = config.resolvePath(filePath) else {
|
|
return err("bucket and file required")
|
|
}
|
|
|
|
let fileURL = URL(fileURLWithPath: resolved)
|
|
let key = args["key"] as? String ?? fileURL.lastPathComponent
|
|
let region = args["region"] as? String
|
|
|
|
do {
|
|
let data = try Data(contentsOf: fileURL)
|
|
try await AWSS3Service.shared.putObject(bucket: bucket, key: key, data: data, region: region)
|
|
return ok("Uploaded \(fileURL.lastPathComponent) → s3://\(bucket)/\(key) (\(data.count) bytes)")
|
|
} catch {
|
|
return err(error.localizedDescription)
|
|
}
|
|
}
|
|
|
|
// ── aws_s3_deploy ─────────────────────────────────────────────
|
|
server.registerTool(
|
|
"aws_s3_deploy",
|
|
description: "Deploy a website directory to S3 with static hosting enabled. Creates the bucket if needed.",
|
|
inputSchema: [
|
|
"type": "object",
|
|
"required": ["directory", "bucket"],
|
|
"properties": [
|
|
"directory": ["type": "string", "description": "Local website directory."],
|
|
"bucket": ["type": "string", "description": "S3 bucket name (will be created if needed)."],
|
|
"region": ["type": "string", "description": "AWS region (default: us-east-1)."],
|
|
"cloudfront": ["type": "boolean", "description": "Also create a CloudFront distribution (default: false)."],
|
|
"domain": ["type": "string", "description": "Custom domain (requires Route 53 hosted zone)."],
|
|
] as [String: Any],
|
|
] as [String: Any]
|
|
) { args in
|
|
guard let dirPath = args["directory"] as? String,
|
|
let resolved = config.resolvePath(dirPath),
|
|
let bucket = args["bucket"] as? String else {
|
|
return err("directory and bucket required")
|
|
}
|
|
|
|
let region = args["region"] as? String
|
|
let useCF = args["cloudfront"] as? Bool ?? false
|
|
let domain = args["domain"] as? String
|
|
|
|
do {
|
|
let result = try await AWSS3Service.shared.deployWebsite(
|
|
directory: URL(fileURLWithPath: resolved),
|
|
bucket: bucket,
|
|
region: region
|
|
)
|
|
|
|
var output = "S3 Deployment \(result.success ? "Succeeded" : "Completed with errors")\n\n"
|
|
output += " Bucket: s3://\(result.bucket)\n"
|
|
output += " Region: \(result.region)\n"
|
|
output += " Files uploaded: \(result.uploadedFiles.count)\n"
|
|
output += " Duration: \(String(format: "%.1f", result.duration))s\n"
|
|
|
|
if let url = result.websiteURL {
|
|
output += " Website URL: \(url)\n"
|
|
}
|
|
|
|
if !result.errors.isEmpty {
|
|
output += "\n Errors:\n"
|
|
for e in result.errors { output += " • \(e)\n" }
|
|
}
|
|
|
|
// Create CloudFront distribution if requested
|
|
if useCF {
|
|
let cf = AWSCloudFrontService.shared
|
|
let aliases = domain != nil ? [domain!] : []
|
|
let dist = try await cf.createDistributionForS3(
|
|
bucket: bucket, region: result.region, aliases: aliases
|
|
)
|
|
output += "\n CloudFront Distribution:\n"
|
|
output += " ID: \(dist.id)\n"
|
|
output += " Domain: https://\(dist.domainName)\n"
|
|
output += " Status: \(dist.status) (may take 5-10 min)\n"
|
|
|
|
// Point Route 53 if domain specified
|
|
if let domain = domain {
|
|
let r53 = AWSRoute53Service.shared
|
|
if let zone = try await r53.findHostedZone(domain: domain) {
|
|
try await r53.pointToCloudFront(
|
|
zoneId: zone.id, domain: domain,
|
|
distributionDomain: dist.domainName
|
|
)
|
|
output += " Route 53: \(domain) → \(dist.domainName)\n"
|
|
}
|
|
}
|
|
}
|
|
|
|
return ok(output)
|
|
} catch {
|
|
return err(error.localizedDescription)
|
|
}
|
|
}
|
|
|
|
// ── aws_s3_download ───────────────────────────────────────────
|
|
server.registerTool(
|
|
"aws_s3_download",
|
|
description: "Download an object from S3.",
|
|
inputSchema: [
|
|
"type": "object",
|
|
"required": ["bucket", "key"],
|
|
"properties": [
|
|
"bucket": ["type": "string", "description": "S3 bucket name."],
|
|
"key": ["type": "string", "description": "S3 object key."],
|
|
"output": ["type": "string", "description": "Local output path (default: current directory + filename)."],
|
|
"region": ["type": "string", "description": "AWS region."],
|
|
] as [String: Any],
|
|
] as [String: Any]
|
|
) { args in
|
|
guard let bucket = args["bucket"] as? String,
|
|
let key = args["key"] as? String else {
|
|
return err("bucket and key required")
|
|
}
|
|
|
|
let region = args["region"] as? String
|
|
let outputPath: String
|
|
if let out = args["output"] as? String, let resolved = config.resolvePath(out) {
|
|
outputPath = resolved
|
|
} else {
|
|
let filename = (key as NSString).lastPathComponent
|
|
outputPath = config.workspaceRoot + "/" + filename
|
|
}
|
|
|
|
do {
|
|
let data = try await AWSS3Service.shared.getObject(bucket: bucket, key: key, region: region)
|
|
try data.write(to: URL(fileURLWithPath: outputPath))
|
|
return ok("Downloaded s3://\(bucket)/\(key) → \(outputPath) (\(data.count) bytes)")
|
|
} catch {
|
|
return err(error.localizedDescription)
|
|
}
|
|
}
|
|
|
|
// ── aws_s3_delete ─────────────────────────────────────────────
|
|
server.registerTool(
|
|
"aws_s3_delete",
|
|
description: "Delete an object from S3.",
|
|
inputSchema: [
|
|
"type": "object",
|
|
"required": ["bucket", "key"],
|
|
"properties": [
|
|
"bucket": ["type": "string", "description": "S3 bucket name."],
|
|
"key": ["type": "string", "description": "S3 object key to delete."],
|
|
"region": ["type": "string", "description": "AWS region."],
|
|
] as [String: Any],
|
|
] as [String: Any]
|
|
) { args in
|
|
guard let bucket = args["bucket"] as? String,
|
|
let key = args["key"] as? String else {
|
|
return err("bucket and key required")
|
|
}
|
|
do {
|
|
try await AWSS3Service.shared.deleteObject(bucket: bucket, key: key, region: args["region"] as? String)
|
|
return ok("Deleted s3://\(bucket)/\(key)")
|
|
} catch {
|
|
return err(error.localizedDescription)
|
|
}
|
|
}
|
|
|
|
// ── aws_cloudfront_list ───────────────────────────────────────
|
|
server.registerTool(
|
|
"aws_cloudfront_list",
|
|
description: "List CloudFront CDN distributions.",
|
|
inputSchema: ["type": "object", "properties": [:] as [String: Any]] as [String: Any],
|
|
annotations: ToolAnnotations(readOnlyHint: true)
|
|
) { _ in
|
|
do {
|
|
let distributions = try await AWSCloudFrontService.shared.listDistributions()
|
|
if distributions.isEmpty {
|
|
return ok("No CloudFront distributions found.")
|
|
}
|
|
var output = "CloudFront Distributions (\(distributions.count)):\n\n"
|
|
for d in distributions {
|
|
output += " \(d.id) — \(d.domainName)\n"
|
|
output += " Status: \(d.status) | Enabled: \(d.enabled)\n"
|
|
if !d.aliases.isEmpty {
|
|
output += " Aliases: \(d.aliases.joined(separator: ", "))\n"
|
|
}
|
|
output += " Origins: \(d.origins.joined(separator: ", "))\n\n"
|
|
}
|
|
return ok(output)
|
|
} catch {
|
|
return err(error.localizedDescription)
|
|
}
|
|
}
|
|
|
|
// ── aws_cloudfront_create ─────────────────────────────────────
|
|
server.registerTool(
|
|
"aws_cloudfront_create",
|
|
description: "Create a CloudFront distribution for an S3 website bucket.",
|
|
inputSchema: [
|
|
"type": "object",
|
|
"required": ["bucket"],
|
|
"properties": [
|
|
"bucket": ["type": "string", "description": "S3 bucket name (must have website hosting enabled)."],
|
|
"region": ["type": "string", "description": "S3 bucket region."],
|
|
"domains": ["type": "array", "items": ["type": "string"], "description": "Custom domain names (CNAMEs)."],
|
|
"certificate_arn": ["type": "string", "description": "ACM certificate ARN for HTTPS."],
|
|
] as [String: Any],
|
|
] as [String: Any]
|
|
) { args in
|
|
guard let bucket = args["bucket"] as? String else {
|
|
return err("bucket required")
|
|
}
|
|
let region = args["region"] as? String ?? AWSService.shared.defaultRegion
|
|
let domains = args["domains"] as? [String] ?? []
|
|
let certArn = args["certificate_arn"] as? String
|
|
|
|
do {
|
|
let dist = try await AWSCloudFrontService.shared.createDistributionForS3(
|
|
bucket: bucket, region: region,
|
|
aliases: domains, certificateArn: certArn
|
|
)
|
|
var output = "CloudFront Distribution Created:\n\n"
|
|
output += " ID: \(dist.id)\n"
|
|
output += " Domain: https://\(dist.domainName)\n"
|
|
output += " Status: \(dist.status)\n"
|
|
if !domains.isEmpty {
|
|
output += " CNAMEs: \(domains.joined(separator: ", "))\n"
|
|
}
|
|
output += "\n Note: Distribution may take 5-10 minutes to deploy globally."
|
|
return ok(output)
|
|
} catch {
|
|
return err(error.localizedDescription)
|
|
}
|
|
}
|
|
|
|
// ── aws_cloudfront_invalidate ─────────────────────────────────
|
|
server.registerTool(
|
|
"aws_cloudfront_invalidate",
|
|
description: "Invalidate CloudFront cache (clear cached content after updates).",
|
|
inputSchema: [
|
|
"type": "object",
|
|
"required": ["distribution_id"],
|
|
"properties": [
|
|
"distribution_id": ["type": "string", "description": "CloudFront distribution ID."],
|
|
"paths": ["type": "array", "items": ["type": "string"], "description": "Paths to invalidate (default: [/*])."],
|
|
] as [String: Any],
|
|
] as [String: Any]
|
|
) { args in
|
|
guard let distId = args["distribution_id"] as? String else {
|
|
return err("distribution_id required")
|
|
}
|
|
let paths = args["paths"] as? [String] ?? ["/*"]
|
|
do {
|
|
let invalidationId = try await AWSCloudFrontService.shared.createInvalidation(
|
|
distributionId: distId, paths: paths
|
|
)
|
|
return ok("Cache invalidation created (ID: \(invalidationId))\nPaths: \(paths.joined(separator: ", "))")
|
|
} catch {
|
|
return err(error.localizedDescription)
|
|
}
|
|
}
|
|
|
|
// ── aws_route53_zones ─────────────────────────────────────────
|
|
server.registerTool(
|
|
"aws_route53_zones",
|
|
description: "List or create Route 53 hosted zones.",
|
|
inputSchema: [
|
|
"type": "object",
|
|
"required": ["action"],
|
|
"properties": [
|
|
"action": ["type": "string", "description": "list or create."],
|
|
"domain": ["type": "string", "description": "Domain name (for create)."],
|
|
] as [String: Any],
|
|
] as [String: Any]
|
|
) { args in
|
|
let action = args["action"] as? String ?? "list"
|
|
let r53 = AWSRoute53Service.shared
|
|
do {
|
|
if action == "create" {
|
|
guard let domain = args["domain"] as? String, !domain.isEmpty else {
|
|
return err("domain required for create")
|
|
}
|
|
let zone = try await r53.createHostedZone(domain: domain)
|
|
var output = "Hosted Zone Created:\n\n"
|
|
output += " Zone ID: \(zone.id)\n"
|
|
output += " Domain: \(zone.domainName)\n"
|
|
if !zone.nameservers.isEmpty {
|
|
output += " Nameservers (set these at your registrar):\n"
|
|
for ns in zone.nameservers {
|
|
output += " • \(ns)\n"
|
|
}
|
|
}
|
|
return ok(output)
|
|
} else {
|
|
let zones = try await r53.listHostedZones()
|
|
if zones.isEmpty {
|
|
return ok("No Route 53 hosted zones found.")
|
|
}
|
|
var output = "Route 53 Hosted Zones (\(zones.count)):\n\n"
|
|
for z in zones {
|
|
output += " \(z.id) — \(z.domainName)\n"
|
|
output += " Records: \(z.recordCount) | Private: \(z.isPrivate)\n"
|
|
if let comment = z.comment { output += " Comment: \(comment)\n" }
|
|
output += "\n"
|
|
}
|
|
return ok(output)
|
|
}
|
|
} catch {
|
|
return err(error.localizedDescription)
|
|
}
|
|
}
|
|
|
|
// ── aws_route53_records ───────────────────────────────────────
|
|
server.registerTool(
|
|
"aws_route53_records",
|
|
description: "List DNS records in a Route 53 hosted zone.",
|
|
inputSchema: [
|
|
"type": "object",
|
|
"required": ["zone_id"],
|
|
"properties": [
|
|
"zone_id": ["type": "string", "description": "Hosted zone ID."],
|
|
] as [String: Any],
|
|
] as [String: Any],
|
|
annotations: ToolAnnotations(readOnlyHint: true)
|
|
) { args in
|
|
guard let zoneId = args["zone_id"] as? String else {
|
|
return err("zone_id required")
|
|
}
|
|
do {
|
|
let records = try await AWSRoute53Service.shared.listRecordSets(zoneId: zoneId)
|
|
if records.isEmpty {
|
|
return ok("No records in zone \(zoneId)")
|
|
}
|
|
var output = "DNS Records (Zone: \(zoneId)):\n\n"
|
|
output += String(format: "%-8s %-35s %-10s %s\n", "Type", "Name", "TTL", "Value(s)")
|
|
output += String(repeating: "-", count: 90) + "\n"
|
|
for r in records {
|
|
let vals: String
|
|
if let alias = r.aliasTarget {
|
|
vals = "ALIAS → \(alias.dnsName)"
|
|
} else {
|
|
vals = r.values.joined(separator: ", ")
|
|
}
|
|
output += String(format: "%-8s %-35s %-10d %s\n",
|
|
r.type, String(r.name.prefix(35)), r.ttl, vals)
|
|
}
|
|
return ok(output)
|
|
} catch {
|
|
return err(error.localizedDescription)
|
|
}
|
|
}
|
|
|
|
// ── aws_route53_set ───────────────────────────────────────────
|
|
server.registerTool(
|
|
"aws_route53_set",
|
|
description: "Create or update a DNS record in Route 53.",
|
|
inputSchema: [
|
|
"type": "object",
|
|
"required": ["zone_id", "name", "type", "values"],
|
|
"properties": [
|
|
"zone_id": ["type": "string", "description": "Hosted zone ID."],
|
|
"name": ["type": "string", "description": "Record name (e.g., example.com or sub.example.com)."],
|
|
"type": ["type": "string", "description": "Record type: A, AAAA, CNAME, MX, TXT, NS."],
|
|
"values": ["type": "array", "items": ["type": "string"], "description": "Record values."],
|
|
"ttl": ["type": "integer", "description": "TTL in seconds (default: 300)."],
|
|
] as [String: Any],
|
|
] as [String: Any]
|
|
) { args in
|
|
guard let zoneId = args["zone_id"] as? String,
|
|
let name = args["name"] as? String,
|
|
let type = args["type"] as? String,
|
|
let values = args["values"] as? [String] else {
|
|
return err("zone_id, name, type, and values required")
|
|
}
|
|
let ttl = args["ttl"] as? Int ?? 300
|
|
do {
|
|
try await AWSRoute53Service.shared.upsertRecord(
|
|
zoneId: zoneId, name: name, type: type,
|
|
values: values, ttl: ttl
|
|
)
|
|
return ok("Set \(type) record: \(name) → \(values.joined(separator: ", ")) (TTL: \(ttl))")
|
|
} catch {
|
|
return err(error.localizedDescription)
|
|
}
|
|
}
|
|
|
|
// ── aws_lambda_list ───────────────────────────────────────────
|
|
server.registerTool(
|
|
"aws_lambda_list",
|
|
description: "List Lambda functions or get function details.",
|
|
inputSchema: [
|
|
"type": "object",
|
|
"properties": [
|
|
"name": ["type": "string", "description": "Function name (omit to list all)."],
|
|
"region": ["type": "string", "description": "AWS region."],
|
|
] as [String: Any],
|
|
] as [String: Any],
|
|
annotations: ToolAnnotations(readOnlyHint: true)
|
|
) { args in
|
|
let lambda = AWSLambdaService.shared
|
|
let region = args["region"] as? String
|
|
do {
|
|
if let name = args["name"] as? String, !name.isEmpty {
|
|
let f = try await lambda.getFunction(name: name, region: region)
|
|
var output = "Lambda Function: \(f.name)\n\n"
|
|
output += " ARN: \(f.arn)\n"
|
|
output += " Runtime: \(f.runtime)\n"
|
|
output += " Handler: \(f.handler)\n"
|
|
output += " Memory: \(f.memorySize) MB\n"
|
|
output += " Timeout: \(f.timeout)s\n"
|
|
output += " Code Size: \(f.formattedCodeSize)\n"
|
|
if let desc = f.description, !desc.isEmpty { output += " Description: \(desc)\n" }
|
|
if let mod = f.lastModified { output += " Last Modified: \(mod)\n" }
|
|
return ok(output)
|
|
} else {
|
|
let functions = try await lambda.listFunctions(region: region)
|
|
if functions.isEmpty {
|
|
return ok("No Lambda functions found.")
|
|
}
|
|
var output = "Lambda Functions (\(functions.count)):\n\n"
|
|
for f in functions {
|
|
output += " \(f.name) — \(f.runtime) | \(f.memorySize)MB | \(f.formattedCodeSize)\n"
|
|
}
|
|
return ok(output)
|
|
}
|
|
} catch {
|
|
return err(error.localizedDescription)
|
|
}
|
|
}
|
|
|
|
// ── aws_lambda_invoke ─────────────────────────────────────────
|
|
server.registerTool(
|
|
"aws_lambda_invoke",
|
|
description: "Invoke a Lambda function with optional JSON payload.",
|
|
inputSchema: [
|
|
"type": "object",
|
|
"required": ["name"],
|
|
"properties": [
|
|
"name": ["type": "string", "description": "Lambda function name."],
|
|
"payload": ["type": "object", "description": "JSON payload to send."],
|
|
"async": ["type": "boolean", "description": "Invoke asynchronously (default: false)."],
|
|
"region": ["type": "string", "description": "AWS region."],
|
|
] as [String: Any],
|
|
] as [String: Any]
|
|
) { args in
|
|
guard let name = args["name"] as? String else {
|
|
return err("Function name required")
|
|
}
|
|
let payload = args["payload"] as? [String: Any]
|
|
let isAsync = args["async"] as? Bool ?? false
|
|
let region = args["region"] as? String
|
|
|
|
do {
|
|
let result = try await AWSLambdaService.shared.invoke(
|
|
name: name, payload: payload,
|
|
async: isAsync, region: region
|
|
)
|
|
var output = "Lambda Invocation Result:\n\n"
|
|
output += " Status: \(result.statusCode)\n"
|
|
if isAsync {
|
|
output += " Mode: Async (fire-and-forget)\n"
|
|
} else {
|
|
output += " Response:\n\(result.payload)\n"
|
|
}
|
|
return ok(output)
|
|
} catch {
|
|
return err(error.localizedDescription)
|
|
}
|
|
}
|
|
|
|
// ── aws_ssl_certificate ───────────────────────────────────────
|
|
server.registerTool(
|
|
"aws_ssl_certificate",
|
|
description: "Manage free SSL certificates via AWS Certificate Manager (ACM).",
|
|
inputSchema: [
|
|
"type": "object",
|
|
"required": ["action"],
|
|
"properties": [
|
|
"action": ["type": "string", "description": "list, request, or validate."],
|
|
"domain": ["type": "string", "description": "Domain for certificate (for request)."],
|
|
"arn": ["type": "string", "description": "Certificate ARN (for validate)."],
|
|
"alt_names": ["type": "array", "items": ["type": "string"], "description": "Subject alternative names."],
|
|
] as [String: Any],
|
|
] as [String: Any]
|
|
) { args in
|
|
let action = args["action"] as? String ?? "list"
|
|
let cf = AWSCloudFrontService.shared
|
|
|
|
do {
|
|
switch action.lowercased() {
|
|
case "list":
|
|
let certs = try await cf.listCertificates()
|
|
if certs.isEmpty {
|
|
return ok("No ACM certificates found.")
|
|
}
|
|
var output = "ACM Certificates (\(certs.count)):\n\n"
|
|
for c in certs {
|
|
output += " \(c.domainName) — \(c.status)\n"
|
|
output += " ARN: \(c.arn)\n\n"
|
|
}
|
|
return ok(output)
|
|
|
|
case "request":
|
|
guard let domain = args["domain"] as? String, !domain.isEmpty else {
|
|
return err("domain required")
|
|
}
|
|
let altNames = args["alt_names"] as? [String] ?? []
|
|
let arn = try await cf.requestCertificate(domain: domain, subjectAlternativeNames: altNames)
|
|
var output = "SSL Certificate Requested:\n\n"
|
|
output += " Domain: \(domain)\n"
|
|
output += " ARN: \(arn)\n"
|
|
output += " Status: PENDING_VALIDATION\n\n"
|
|
output += " Next: Use action='validate' with arn to get DNS validation records."
|
|
return ok(output)
|
|
|
|
case "validate":
|
|
guard let arn = args["arn"] as? String, !arn.isEmpty else {
|
|
return err("arn required")
|
|
}
|
|
let records = try await cf.getCertificateValidation(arn: arn)
|
|
if records.isEmpty {
|
|
return ok("No validation records found (certificate may already be validated)")
|
|
}
|
|
var output = "Certificate Validation Records:\n\n"
|
|
output += "Add these CNAME records to your DNS:\n\n"
|
|
for r in records {
|
|
output += " Domain: \(r.domain)\n"
|
|
output += " Name: \(r.recordName)\n"
|
|
output += " Value: \(r.recordValue)\n"
|
|
output += " Type: \(r.recordType)\n\n"
|
|
}
|
|
return ok(output)
|
|
|
|
default:
|
|
return err("Unknown action '\(action)'. Available: list, request, validate")
|
|
}
|
|
} catch {
|
|
return err(error.localizedDescription)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Helpers
|
|
|
|
private static func ok(_ text: String) -> [[String: Any]] {
|
|
[["type": "text", "text": text]]
|
|
}
|
|
|
|
private static func err(_ message: String) -> [[String: Any]] {
|
|
[["type": "text", "text": "Error: \(message)"]]
|
|
}
|
|
}
|