Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions mcp/rds-aidba/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
MIT No Attribution

Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
43 changes: 43 additions & 0 deletions mcp/rds-aidba/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# rds-aidba - Read-Only MySQL/PostgreSQL MCP Server

Custom MCP server for AWS DevOps Agent providing safe, query-allowlisted diagnostic access to Aurora MySQL, RDS MySQL, Aurora PostgreSQL, and RDS PostgreSQL via the RDS Data API.

## Tools (10)

| Tool | Description |
|------|-------------|
| execute_health_query | Run predefined query by engine + category + query_id |
| list_health_queries | List all queries for an engine (mysql/postgresql) |
| run_category_check | All queries in a category |
| run_full_health_check | Key queries from all categories |
| list_clusters | List Aurora/RDS clusters |
| get_cluster_health | Config, encryption, backups, monitoring |
| get_cluster_metrics | CloudWatch: CPU, connections, IOPS, lag |
| get_performance_insights | PI wait events and DB load |
| get_proxy_health | RDS Proxy status and targets |
| get_serverless_capacity | Serverless v2 ACU utilization |

## Queries: 54 total (24 MySQL + 30 PostgreSQL, 10 categories each)

## Security

- Query allowlist only (no dynamic SQL)
- Cluster/database allowlists (defense-in-depth)
- Production enforcement blocks wildcards
- No VPC required (RDS Data API)
- Function URL with AWS_IAM auth

## Deploy

sam build
sam deploy --stack-name rds-aidba-mcp --capabilities CAPABILITY_NAMED_IAM --resolve-s3 --no-confirm-changeset

## Register in DevOps Agent

- URL: Function URL from stack output (use as-is, already includes /mcp)
- Service Name: lambda
- Auth: IAM (SigV4)

## Disclaimer

This is sample code, not intended for production use without review. Validate in non-production first.
3 changes: 3 additions & 0 deletions mcp/rds-aidba/layers/dependencies/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
build-DependenciesLayer:
pip3 install -r requirements.txt -t python/ --platform manylinux2014_x86_64 --python-version 3.12 --only-binary=:all: --implementation cp
cp -r python/ $(ARTIFACTS_DIR)/python/
3 changes: 3 additions & 0 deletions mcp/rds-aidba/layers/dependencies/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
mcp-proxy>=0.5.0
fastmcp>=2.0.0,<4.0.0
boto3>=1.34.0
5 changes: 5 additions & 0 deletions mcp/rds-aidba/src/run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/bin/bash
export PYTHONPATH="/var/task:${PYTHONPATH}"
cd /var/task
exec python3 -m mcp_proxy --port=8000 --stateless --pass-environment -- \
python3 server.py
627 changes: 627 additions & 0 deletions mcp/rds-aidba/src/server.py

Large diffs are not rendered by default.

95 changes: 95 additions & 0 deletions mcp/rds-aidba/template.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
rds-aidba MCP Server for AWS DevOps Agent.
Uses mcp-proxy + Lambda Web Adapter for Streamable HTTP. Function URL with IAM auth.

Globals:
Function:
Timeout: 180
Runtime: python3.12
MemorySize: 1024

Parameters:
ClusterArn:
Type: String
SecretArn:
Type: String
DatabaseName:
Type: String
Default: information_schema
StageName:
Type: String
Default: dev
AllowedValues: [dev, staging, prod]
AllowedClusters:
Type: String
Default: '*'
AllowedDatabases:
Type: String
Default: '*'

Resources:
RdsAidbaFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: !Sub rds-aidba-mcp-${StageName}
Handler: run.sh
CodeUri: src/
Architectures: [x86_64]
Layers:
- !Sub arn:aws:lambda:${AWS::Region}:753240598075:layer:LambdaAdapterLayerX86:24
- !Ref DependenciesLayer
Environment:
Variables:
AWS_LAMBDA_EXEC_WRAPPER: /opt/bootstrap
AWS_LWA_PORT: '8000'
AWS_LWA_READINESS_CHECK_PATH: /mcp
AWS_LWA_READINESS_CHECK_MIN_UNHEALTHY_STATUS: '500'
AWS_LWA_INVOKE_MODE: response_stream
AWS_LWA_READINESS_CHECK_PROTOCOL: http
AWS_LWA_ASYNC_INIT: 'true'
PYTHONPATH: /opt/python
CLUSTER_ARN: !Ref ClusterArn
SECRET_ARN: !Ref SecretArn
DATABASE_NAME: !Ref DatabaseName
STAGE_NAME: !Ref StageName
ALLOWED_CLUSTERS: !Ref AllowedClusters
ALLOWED_DATABASES: !Ref AllowedDatabases
FunctionUrlConfig:
AuthType: AWS_IAM
InvokeMode: RESPONSE_STREAM
Policies:
- Statement:
- Effect: Allow
Action: [rds-data:ExecuteStatement, rds-data:BatchExecuteStatement]
Resource: !Ref ClusterArn
- Statement:
- Effect: Allow
Action: secretsmanager:GetSecretValue
Resource: !Ref SecretArn
- Statement:
- Effect: Allow
Action: [rds:DescribeDBClusters, rds:DescribeDBInstances, rds:DescribeDBEngineVersions, rds:DescribeDBProxies, rds:DescribeDBProxyTargets]
Resource: '*'
- Statement:
- Effect: Allow
Action: [cloudwatch:GetMetricStatistics, cloudwatch:GetMetricData, pi:GetResourceMetrics, pi:DescribeDimensionKeys]
Resource: '*'

DependenciesLayer:
Type: AWS::Serverless::LayerVersion
Properties:
LayerName: !Sub rds-aidba-mcp-deps-${StageName}
ContentUri: layers/dependencies/
CompatibleRuntimes: [python3.12]
CompatibleArchitectures: [x86_64]
Metadata:
BuildMethod: makefile

Outputs:
McpEndpointUrl:
Description: Register in DevOps Agent (Service Name = lambda, Auth = SigV4)
Value: !Sub ${RdsAidbaFunctionUrl.FunctionUrl}mcp
FunctionArn:
Value: !GetAtt RdsAidbaFunction.Arn
106 changes: 106 additions & 0 deletions mcp/rds-aidba/tests/e2e_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
"""End-to-end tests for rds-aidba MCP Server.

These tests require:
- A deployed Lambda Function URL
- AWS credentials with lambda:InvokeFunctionUrl permission

Run with: pytest tests/e2e_test.py -v
Set MCP_ENDPOINT_URL env var to your deployed endpoint.
"""

import json
import os
import urllib.request
import boto3
import pytest
from botocore.auth import SigV4Auth
from botocore.awsrequest import AWSRequest

MCP_ENDPOINT_URL = os.environ.get("MCP_ENDPOINT_URL", "")
REGION = os.environ.get("AWS_REGION", "us-east-1")


def sign_request(url, body, session_id=None):
"""Sign request with SigV4 for Lambda Function URL."""
session = boto3.Session(region_name=REGION)
credentials = session.get_credentials().get_frozen_credentials()
headers = {
"Content-Type": "application/json",
"Accept": "application/json, text/event-stream",
}
if session_id:
headers["Mcp-Session-Id"] = session_id
aws_request = AWSRequest(method="POST", url=url, data=body, headers=headers)
SigV4Auth(credentials, "lambda", REGION).add_auth(aws_request)
return dict(aws_request.headers), aws_request.body


def mcp_call(method, params=None, req_id=1, session_id=None):
"""Make an MCP JSON-RPC call to the Function URL."""
payload = {"jsonrpc": "2.0", "method": method, "id": req_id}
if params:
payload["params"] = params
body = json.dumps(payload)
url = MCP_ENDPOINT_URL.rstrip("/")
headers, signed_body = sign_request(url, body, session_id)
data = signed_body if isinstance(signed_body, bytes) else signed_body.encode()
req = urllib.request.Request(url, data=data, headers=headers, method="POST")
try:
resp = urllib.request.urlopen(req, timeout=60)
response_body = resp.read().decode()
for line in response_body.split("\n"):
line = line.strip()
if line.startswith("data:"):
return json.loads(line[5:].strip())
return json.loads(response_body)
except urllib.error.HTTPError as e:
return {"error": f"HTTP {e.code}: {e.read().decode()[:200]}"}


@pytest.mark.skipif(not MCP_ENDPOINT_URL, reason="Set MCP_ENDPOINT_URL env var")
class TestE2E:
"""End-to-end tests against deployed MCP server."""

def test_initialize(self):
result = mcp_call("initialize", params={
"protocolVersion": "2025-03-26",
"capabilities": {},
"clientInfo": {"name": "e2e-test", "version": "1.0"},
})
assert "result" in result
assert result["result"]["serverInfo"]["name"] == "rds-aidba"

def test_tools_list(self):
result = mcp_call("tools/list", req_id=2)
assert "result" in result
tools = result["result"]["tools"]
tool_names = [t["name"] for t in tools]
assert "execute_health_query" in tool_names
assert "list_health_queries" in tool_names
assert "run_full_health_check" in tool_names

def test_list_health_queries(self):
result = mcp_call("tools/call", params={
"name": "list_health_queries",
"arguments": {"engine": "mysql"},
}, req_id=3)
assert "result" in result
content = result["result"]["content"][0]["text"]
assert "Category 1" in content
assert "Category 9" in content

def test_execute_health_query(self):
result = mcp_call("tools/call", params={
"name": "execute_health_query",
"arguments": {"engine": "mysql", "category": "3", "query_id": "3.1"},
}, req_id=4)
assert "result" in result
content = result["result"]["content"][0]["text"]
assert "Connection Overview" in content

def test_execute_pg_query(self):
result = mcp_call("tools/call", params={
"name": "execute_health_query",
"arguments": {"engine": "postgresql", "category": "1", "query_id": "1.1"},
}, req_id=5)
assert "result" in result
57 changes: 57 additions & 0 deletions mcp/rds-aidba/tests/test_allowlist.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"""Tests for rds-aidba MCP Server."""
import os, sys, pytest
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))

class TestQueryAllowlist:
def test_mysql_categories(self):
from server import MYSQL_QUERIES
assert set(MYSQL_QUERIES.keys()) == {"1","2","3","4","5","6","7","8","9","10"}

def test_pg_categories(self):
from server import PG_QUERIES
assert set(PG_QUERIES.keys()) == {"1","2","3","4","5","6","7","8","9","10"}

def test_no_mutative_sql(self):
from server import MYSQL_QUERIES, PG_QUERIES
blocked = ["INSERT","UPDATE","DELETE","DROP","CREATE","ALTER","TRUNCATE"]
for queries in [MYSQL_QUERIES, PG_QUERIES]:
for cat in queries.values():
for qid, qdef in cat.items():
if qid.startswith("_"): continue
for kw in blocked:
assert not qdef["sql"].upper().strip().startswith(kw)

def test_mysql_count(self):
from server import MYSQL_QUERY_COUNT
assert MYSQL_QUERY_COUNT == 24

def test_pg_count(self):
from server import PG_QUERY_COUNT
assert PG_QUERY_COUNT == 30

class TestValidation:
def test_validate_cluster(self):
from server import validate_cluster, ALLOWED_CLUSTERS
ALLOWED_CLUSTERS.clear()
ALLOWED_CLUSTERS.update({"test-cluster"})
assert validate_cluster("test-cluster")[0] is True
assert validate_cluster("other")[0] is False

def test_validate_instance(self):
from server import validate_instance, ALLOWED_CLUSTERS
ALLOWED_CLUSTERS.clear()
ALLOWED_CLUSTERS.update({"my-cluster"})
assert validate_instance("my-cluster-instance-1")[0] is True
assert validate_instance("other-instance")[0] is False

def test_validate_proxy(self):
from server import validate_proxy, ALLOWED_CLUSTERS
ALLOWED_CLUSTERS.clear()
ALLOWED_CLUSTERS.update({"my-proxy"})
assert validate_proxy("my-proxy")[0] is True
assert validate_proxy("other")[0] is False

def test_wildcard_allows_all(self):
from server import validate_cluster, ALLOWED_CLUSTERS
ALLOWED_CLUSTERS.clear()
assert validate_cluster("anything")[0] is True
17 changes: 17 additions & 0 deletions skills/database-rds-devops/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Changelog

## [1.0.0] - 2026-07-16

### Added
- Initial release adapted from internal database expertise skill
- 12-dimension AWS-level health scoring for Aurora MySQL/PostgreSQL
- 8-dimension database-level health scoring (50 points max)
- 23 MySQL health check queries across 9 diagnostic categories
- 4 PostgreSQL health check queries (active sessions, bloat, top queries, wraparound)
- CloudWatch Metrics analysis with severity thresholds
- CloudWatch Logs Insights integration (slow query, error, PostgreSQL logs)
- 33-check Aurora operational validation checklist
- Platform-aware diagnostics (Aurora MySQL vs RDS MySQL vs Aurora PostgreSQL)
- Correlation engine for cross-layer diagnostic findings
- Error pattern recognition for common MySQL/PostgreSQL issues
- Support for Aurora MySQL 2.x/3.x, RDS MySQL 5.7/8.0, Aurora/RDS PostgreSQL
Loading