55access to Aurora MySQL and Aurora PostgreSQL clusters (RDS Data API required).
66Includes CloudWatch metrics, Performance Insights, RDS Proxy, and Serverless v2.
77
8+ Dynamic multi-cluster: data-plane tools take a cluster_identifier and auto-discover
9+ the cluster ARN, engine, and credentials (MasterUserSecret). No per-cluster config.
10+
811Engines: Aurora MySQL, Aurora PostgreSQL (Data API enabled clusters only)
912Queries: 54 predefined (24 MySQL + 30 PostgreSQL) across 10 categories
1013Data Sources: CloudWatch, Performance Insights, RDS Data API
11- Transport: Streamable HTTP (Lambda Web Adapter + FastMCP)
14+ Transport: Streamable HTTP (Lambda Web Adapter + FastMCP) behind API Gateway
1215
1316Safety:
1417 + Query allowlist only — no dynamic SQL
3437# CONFIGURATION
3538# =============================================================================
3639
40+ # CLUSTER_ARN / SECRET_ARN are legacy/optional — data-plane tools now resolve the
41+ # cluster dynamically from the cluster_identifier passed to each tool.
3742CLUSTER_ARN = os .environ .get ("CLUSTER_ARN" , "" )
3843SECRET_ARN = os .environ .get ("SECRET_ARN" , "" )
39- DATABASE = os .environ .get ("DATABASE_NAME" , "information_schema" )
44+ DEFAULT_DATABASE = os .environ .get ("DATABASE_NAME" , "" ) # optional default DB override
4045REGION = os .environ .get ("AWS_REGION_NAME" , os .environ .get ("AWS_REGION" , "us-east-1" ))
4146STAGE = os .environ .get ("STAGE_NAME" , "dev" )
4247
@@ -104,22 +109,94 @@ def validate_proxy(proxy_name: str) -> tuple:
104109 return False , f"ERROR: Proxy '{ proxy_name } ' not in allowed list."
105110
106111
112+ # =============================================================================
113+ # CLUSTER RESOLUTION (dynamic — auto-discovers engine + credentials)
114+ # =============================================================================
115+
116+
117+ def _engine_family (engine : str ) -> str :
118+ """Map an RDS engine string to a query family."""
119+ if engine .startswith ("aurora-mysql" ):
120+ return "mysql"
121+ if engine .startswith ("aurora-postgresql" ):
122+ return "postgresql"
123+ return ""
124+
125+
126+ def _default_database (family : str , override : str = None ) -> str :
127+ """Pick the default database per engine unless overridden."""
128+ if override :
129+ return override
130+ if DEFAULT_DATABASE :
131+ return DEFAULT_DATABASE
132+ return "information_schema" if family == "mysql" else "postgres"
133+
134+
135+ def _resolve_cluster (cluster_identifier : str , secret_arn_override : str = None ) -> dict :
136+ """
137+ Resolve a cluster identifier to its ARN, engine family, and credentials.
138+ Auto-discovers the Secrets Manager ARN from the cluster's MasterUserSecret
139+ (AWS-managed master credentials). No hardcoding or per-cluster config needed.
140+ """
141+ ok , msg = validate_cluster (cluster_identifier )
142+ if not ok :
143+ return {"ok" : False , "error" : msg }
144+ try :
145+ c = rds_client .describe_db_clusters (
146+ DBClusterIdentifier = cluster_identifier
147+ )["DBClusters" ][0 ]
148+ except Exception as e :
149+ return {"ok" : False , "error" : f"ERROR: Cannot describe cluster '{ cluster_identifier } ': { e } " }
150+
151+ engine = c .get ("Engine" , "" )
152+ family = _engine_family (engine )
153+ if not family :
154+ return {"ok" : False , "error" : (
155+ f"ERROR: '{ cluster_identifier } ' engine '{ engine } ' is not Aurora MySQL or "
156+ "Aurora PostgreSQL. Only Aurora clusters with the RDS Data API are supported."
157+ )}
158+
159+ if not c .get ("HttpEndpointEnabled" , False ):
160+ return {"ok" : False , "error" : (
161+ f"ERROR: RDS Data API is not enabled on '{ cluster_identifier } '. Enable it with:\n "
162+ f"aws rds modify-db-cluster --db-cluster-identifier { cluster_identifier } "
163+ "--enable-http-endpoint"
164+ )}
165+
166+ secret_arn = secret_arn_override or c .get ("MasterUserSecret" , {}).get ("SecretArn" )
167+ if not secret_arn :
168+ return {"ok" : False , "error" : (
169+ f"ERROR: No discoverable credentials for '{ cluster_identifier } '. The cluster has no "
170+ "AWS-managed MasterUserSecret. Either enable managed master credentials, or pass a "
171+ "secret_arn override (note: a customer-managed secret ARN must also be permitted by "
172+ "the Lambda role's secretsmanager policy)."
173+ )}
174+
175+ return {
176+ "ok" : True ,
177+ "cluster_arn" : c ["DBClusterArn" ],
178+ "secret_arn" : secret_arn ,
179+ "engine" : engine ,
180+ "family" : family ,
181+ }
182+
183+
107184# =============================================================================
108185# RDS DATA API EXECUTION
109186# =============================================================================
110187
111188
112- def _execute_sql (sql : str , database : str = None ) -> dict :
113- """Execute read-only SQL via RDS Data API."""
114- db = database or DATABASE
115- ok , msg = validate_database (db )
189+ def _execute_sql (sql : str , cluster_arn : str , secret_arn : str , database : str ) -> dict :
190+ """Execute read-only SQL via RDS Data API against a resolved cluster."""
191+ ok , msg = validate_database (database )
116192 if not ok :
117193 return {"success" : False , "error" : msg , "columns" : [], "rows" : [], "rowCount" : 0 }
118194 try :
119195 response = rds_data .execute_statement (
120- resourceArn = CLUSTER_ARN , secretArn = SECRET_ARN , database = db , sql = sql ,
196+ resourceArn = cluster_arn , secretArn = secret_arn , database = database , sql = sql , includeResultMetadata = True ,
121197 )
122- columns = [col ["name" ] for col in response .get ("columnMetadata" , [])]
198+ columns = [col .get ("label" ) or col .get ("name" ) or f"col_{ i } "
199+ for i , col in enumerate (response .get ("columnMetadata" , []))]
123200 rows = []
124201 for record in response .get ("records" , []):
125202 row = {}
@@ -289,37 +366,49 @@ def _format_table(result: dict) -> str:
289366 "and Aurora PostgreSQL. Provides 54 predefined health check "
290367 "queries (24 MySQL + 30 PostgreSQL) across 10 categories, plus CloudWatch "
291368 "metrics, Performance Insights, RDS Proxy health, and Serverless v2 capacity. "
369+ "Data-plane tools take a cluster_identifier and auto-detect the engine and "
370+ "credentials — any allowlisted Aurora cluster, no per-cluster config. "
292371 "Only allowlisted queries — no arbitrary SQL."
293372 ),
294373)
295374
296375
297376@mcp .tool ()
298- def execute_health_query (engine : str , category : str , query_id : str ) -> str :
377+ def execute_health_query (cluster_identifier : str , category : str , query_id : str ,
378+ database : str = None , secret_arn : str = None ) -> str :
299379 """
300- Run a predefined health check query by engine, category, and query ID.
380+ Run a predefined health check query against an Aurora cluster.
381+ Engine and credentials are auto-detected from the cluster — no config needed.
301382
302383 Args:
303- engine: "mysql" or "postgresql"
304- category: Category number (1-10)
305- query_id: Query ID (e.g., "3.1", "6.2")
384+ cluster_identifier: Aurora cluster identifier (engine auto-detected).
385+ category: Category number (1-10).
386+ query_id: Query ID (e.g., "3.1", "6.2").
387+ database: Optional database override (defaults per engine).
388+ secret_arn: Optional Secrets Manager ARN override (defaults to the
389+ cluster's AWS-managed MasterUserSecret).
306390 """
307- queries = MYSQL_QUERIES if engine == "mysql" else PG_QUERIES
391+ r = _resolve_cluster (cluster_identifier , secret_arn )
392+ if not r ["ok" ]:
393+ return r ["error" ]
394+ queries = MYSQL_QUERIES if r ["family" ] == "mysql" else PG_QUERIES
308395 if category not in queries :
309- return f"ERROR: Unknown category '{ category } ' for { engine } . Available: { ', ' .join (sorted (queries .keys ()))} "
396+ return f"ERROR: Unknown category '{ category } ' for { r [ 'family' ] } . Available: { ', ' .join (sorted (queries .keys ()))} "
310397 cat = queries [category ]
311398 if query_id not in cat :
312399 available = [k for k in cat if not k .startswith ("_" )]
313400 return f"ERROR: Unknown query_id '{ query_id } '. Available: { ', ' .join (available )} "
314401 query = cat [query_id ]
315- result = _execute_sql (query ["sql" ])
316- return f"## { query_id } : { query ['name' ]} \n **Category { category } : { cat ['_category' ]} ** | Engine: { engine } \n \n { _format_table (result )} "
402+ db = _default_database (r ["family" ], database )
403+ result = _execute_sql (query ["sql" ], r ["cluster_arn" ], r ["secret_arn" ], db )
404+ return (f"## { query_id } : { query ['name' ]} \n **Category { category } : { cat ['_category' ]} ** | "
405+ f"Engine: { r ['engine' ]} | Cluster: { cluster_identifier } \n \n { _format_table (result )} " )
317406
318407
319408@mcp .tool ()
320409def list_health_queries (engine : str = "mysql" ) -> str :
321410 """
322- List all available health check queries for an engine.
411+ List all available health check queries for an engine (static reference — no DB access) .
323412
324413 Args:
325414 engine: "mysql" (24 queries) or "postgresql" (30 queries)
@@ -338,48 +427,61 @@ def list_health_queries(engine: str = "mysql") -> str:
338427
339428
340429@mcp .tool ()
341- def run_category_check (engine : str , category : str ) -> str :
430+ def run_category_check (cluster_identifier : str , category : str ,
431+ database : str = None , secret_arn : str = None ) -> str :
342432 """
343- Run all health checks in a category.
433+ Run all health checks in a category against an Aurora cluster (engine auto-detected) .
344434
345435 Args:
346- engine: "mysql" or "postgresql"
347- category: Category number (1-10)
436+ cluster_identifier: Aurora cluster identifier.
437+ category: Category number (1-10).
438+ database: Optional database override.
439+ secret_arn: Optional secret ARN override.
348440 """
349- queries = MYSQL_QUERIES if engine == "mysql" else PG_QUERIES
441+ r = _resolve_cluster (cluster_identifier , secret_arn )
442+ if not r ["ok" ]:
443+ return r ["error" ]
444+ queries = MYSQL_QUERIES if r ["family" ] == "mysql" else PG_QUERIES
350445 if category not in queries :
351446 return f"ERROR: Unknown category '{ category } '."
352447 cat = queries [category ]
353- output = f"# Category { category } : { cat ['_category' ]} ({ engine } )\n \n "
448+ db = _default_database (r ["family" ], database )
449+ output = f"# Category { category } : { cat ['_category' ]} ({ r ['engine' ]} ) | Cluster: { cluster_identifier } \n \n "
354450 for qid , qdef in cat .items ():
355451 if qid .startswith ("_" ):
356452 continue
357- result = _execute_sql (qdef ["sql" ])
453+ result = _execute_sql (qdef ["sql" ], r [ "cluster_arn" ], r [ "secret_arn" ], db )
358454 output += f"## { qid } : { qdef ['name' ]} \n { _format_table (result )} \n \n "
359455 return output
360456
361457
362458@mcp .tool ()
363- def run_full_health_check (engine : str = "mysql" ) -> str :
459+ def run_full_health_check (cluster_identifier : str ,
460+ database : str = None , secret_arn : str = None ) -> str :
364461 """
365- Run key queries from all categories for a comprehensive assessment .
462+ Run key queries from all categories against an Aurora cluster (engine auto-detected) .
366463
367464 Args:
368- engine: "mysql" or "postgresql"
465+ cluster_identifier: Aurora cluster identifier.
466+ database: Optional database override.
467+ secret_arn: Optional secret ARN override.
369468 """
370- if engine == "mysql" :
469+ r = _resolve_cluster (cluster_identifier , secret_arn )
470+ if not r ["ok" ]:
471+ return r ["error" ]
472+ if r ["family" ] == "mysql" :
371473 key_queries = ["1.1" , "2.2" , "3.1" , "5.3" , "6.1" , "7.1" , "8.1" , "9.1" , "10.4" ]
372474 else :
373475 key_queries = ["1.1" , "2.1" , "3.1" , "5.2" , "6.1" , "7.2" , "8.1" , "9.1" , "10.2" ]
374- queries = MYSQL_QUERIES if engine == "mysql" else PG_QUERIES
375- output = f"# Full Health Check ({ engine } )\n \n "
476+ queries = MYSQL_QUERIES if r ["family" ] == "mysql" else PG_QUERIES
477+ db = _default_database (r ["family" ], database )
478+ output = f"# Full Health Check ({ r ['engine' ]} ) | Cluster: { cluster_identifier } \n \n "
376479 for qid in key_queries :
377480 cat_num = qid .split ("." )[0 ]
378- cat = queries .get (cat_num , {})
379- qdef = cat .get (qid )
481+ qdef = queries .get (cat_num , {}).get (qid )
380482 if not qdef :
381483 continue
382- result = _execute_sql (qdef ["sql" ])
484+ result = _execute_sql (qdef ["sql" ], r [ "cluster_arn" ], r [ "secret_arn" ], db )
383485 output += f"## { qid } : { qdef ['name' ]} \n { _format_table (result )} \n \n "
384486 return output
385487
@@ -455,7 +557,6 @@ def get_cluster_metrics(cluster_identifier: str, hours_back: int = 3) -> str:
455557 ("FreeableMemory" , "bytes" ), ("ReadIOPS" , "count/sec" ),
456558 ("WriteIOPS" , "count/sec" ), ("AuroraReplicaLag" , "ms" ),
457559 ]
458- # Get cluster members to query instance-level metrics
459560 try :
460561 cluster_resp = rds_client .describe_db_clusters (DBClusterIdentifier = cluster_identifier )
461562 members = cluster_resp ["DBClusters" ][0 ].get ("DBClusterMembers" , [])
@@ -502,7 +603,6 @@ def get_performance_insights(instance_identifier: str) -> str:
502603 return msg
503604 try :
504605 resource_id = f"db-{ instance_identifier } "
505- # Try to get the actual DbiResourceId
506606 try :
507607 inst = rds_client .describe_db_instances (DBInstanceIdentifier = instance_identifier )
508608 resource_id = inst ["DBInstances" ][0 ]["DbiResourceId" ]
@@ -567,7 +667,6 @@ def get_proxy_health(proxy_name: str) -> str:
567667| Auth | { proxy .get ('Auth' , [{}])[0 ].get ('AuthScheme' , 'N/A' )} |
568668| Idle Timeout | { proxy .get ('IdleClientTimeout' )} sec |
569669"""
570- # Get targets
571670 try :
572671 targets = rds_client .describe_db_proxy_targets (DBProxyName = proxy_name )
573672 output += "\n ### Targets\n | Target | Type | State | Health |\n | --- | --- | --- | --- |\n "
0 commit comments