awslabs.dynamodb-mcp-server 2.0.10__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- awslabs/__init__.py +17 -0
- awslabs/dynamodb_mcp_server/__init__.py +17 -0
- awslabs/dynamodb_mcp_server/cdk_generator/__init__.py +19 -0
- awslabs/dynamodb_mcp_server/cdk_generator/generator.py +276 -0
- awslabs/dynamodb_mcp_server/cdk_generator/models.py +521 -0
- awslabs/dynamodb_mcp_server/cdk_generator/templates/README.md +57 -0
- awslabs/dynamodb_mcp_server/cdk_generator/templates/stack.ts.j2 +70 -0
- awslabs/dynamodb_mcp_server/common.py +94 -0
- awslabs/dynamodb_mcp_server/db_analyzer/__init__.py +30 -0
- awslabs/dynamodb_mcp_server/db_analyzer/analyzer_utils.py +394 -0
- awslabs/dynamodb_mcp_server/db_analyzer/base_plugin.py +355 -0
- awslabs/dynamodb_mcp_server/db_analyzer/mysql.py +450 -0
- awslabs/dynamodb_mcp_server/db_analyzer/plugin_registry.py +73 -0
- awslabs/dynamodb_mcp_server/db_analyzer/postgresql.py +215 -0
- awslabs/dynamodb_mcp_server/db_analyzer/sqlserver.py +255 -0
- awslabs/dynamodb_mcp_server/markdown_formatter.py +513 -0
- awslabs/dynamodb_mcp_server/model_validation_utils.py +845 -0
- awslabs/dynamodb_mcp_server/prompts/dynamodb_architect.md +851 -0
- awslabs/dynamodb_mcp_server/prompts/json_generation_guide.md +185 -0
- awslabs/dynamodb_mcp_server/prompts/transform_model_validation_result.md +168 -0
- awslabs/dynamodb_mcp_server/server.py +524 -0
- awslabs_dynamodb_mcp_server-2.0.10.dist-info/METADATA +306 -0
- awslabs_dynamodb_mcp_server-2.0.10.dist-info/RECORD +27 -0
- awslabs_dynamodb_mcp_server-2.0.10.dist-info/WHEEL +4 -0
- awslabs_dynamodb_mcp_server-2.0.10.dist-info/entry_points.txt +2 -0
- awslabs_dynamodb_mcp_server-2.0.10.dist-info/licenses/LICENSE +175 -0
- awslabs_dynamodb_mcp_server-2.0.10.dist-info/licenses/NOTICE +2 -0
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""MySQL database analyzer plugin."""
|
|
16
|
+
|
|
17
|
+
from awslabs.dynamodb_mcp_server.common import validate_database_name
|
|
18
|
+
from awslabs.dynamodb_mcp_server.db_analyzer.base_plugin import DatabasePlugin
|
|
19
|
+
from awslabs.mysql_mcp_server.connection.asyncmy_pool_connection import AsyncmyPoolConnection
|
|
20
|
+
from awslabs.mysql_mcp_server.connection.rds_data_api_connection import RDSDataAPIConnection
|
|
21
|
+
from awslabs.mysql_mcp_server.server import DummyCtx
|
|
22
|
+
from awslabs.mysql_mcp_server.server import run_query as mysql_query
|
|
23
|
+
from loguru import logger
|
|
24
|
+
from typing import Any, Dict, List
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
DEFAULT_READONLY = True
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# SQL Query Templates for MySQL
|
|
31
|
+
_mysql_analysis_queries = {
|
|
32
|
+
'performance_schema_check': {
|
|
33
|
+
'name': 'Performance Schema Status Check',
|
|
34
|
+
'description': 'Returns the status of the performance_schema system variable (ON/OFF)',
|
|
35
|
+
'category': 'internal', # Internal check, not displayed in manifest
|
|
36
|
+
'sql': 'SELECT @@performance_schema;',
|
|
37
|
+
'parameters': [],
|
|
38
|
+
},
|
|
39
|
+
'comprehensive_table_analysis': {
|
|
40
|
+
'name': 'Comprehensive Table Analysis',
|
|
41
|
+
'description': 'Complete table statistics including structure, size, I/O, and locks',
|
|
42
|
+
'category': 'information_schema',
|
|
43
|
+
'sql': """SELECT
|
|
44
|
+
t.TABLE_NAME as `table_name`,
|
|
45
|
+
t.TABLE_ROWS as `row_count`,
|
|
46
|
+
t.AVG_ROW_LENGTH as `avg_row_length_bytes`,
|
|
47
|
+
t.DATA_LENGTH as `data_size_bytes`,
|
|
48
|
+
t.INDEX_LENGTH as `index_size_bytes`,
|
|
49
|
+
ROUND(t.DATA_LENGTH/1024/1024, 2) as `data_size_mb`,
|
|
50
|
+
ROUND(t.INDEX_LENGTH/1024/1024, 2) as `index_size_mb`,
|
|
51
|
+
ROUND((t.DATA_LENGTH + t.INDEX_LENGTH)/1024/1024, 2) as `total_size_mb`,
|
|
52
|
+
t.AUTO_INCREMENT as `auto_increment`,
|
|
53
|
+
(SELECT COUNT(*) FROM information_schema.COLUMNS c
|
|
54
|
+
WHERE c.TABLE_SCHEMA = t.TABLE_SCHEMA AND c.TABLE_NAME = t.TABLE_NAME) as `column_count`,
|
|
55
|
+
(SELECT COUNT(*) FROM information_schema.KEY_COLUMN_USAGE k
|
|
56
|
+
WHERE k.TABLE_SCHEMA = t.TABLE_SCHEMA AND k.TABLE_NAME = t.TABLE_NAME
|
|
57
|
+
AND k.REFERENCED_TABLE_NAME IS NOT NULL) as `fk_count`,
|
|
58
|
+
t.TABLE_COLLATION as `collation`,
|
|
59
|
+
COALESCE(io.COUNT_STAR, 0) as `total_io_operations`,
|
|
60
|
+
COALESCE(ROUND(io.SUM_TIMER_WAIT/1000000000, 2), 0) as `total_io_wait_ms`,
|
|
61
|
+
COALESCE(io.COUNT_READ, 0) as `reads`,
|
|
62
|
+
COALESCE(ROUND(io.SUM_TIMER_READ/1000000000, 2), 0) as `read_wait_ms`,
|
|
63
|
+
COALESCE(io.COUNT_WRITE, 0) as `writes`,
|
|
64
|
+
COALESCE(ROUND(io.SUM_TIMER_WRITE/1000000000, 2), 0) as `write_wait_ms`,
|
|
65
|
+
COALESCE(io.COUNT_FETCH, 0) as `fetches`,
|
|
66
|
+
COALESCE(io.COUNT_INSERT, 0) as `inserts`,
|
|
67
|
+
COALESCE(io.COUNT_UPDATE, 0) as `updates`,
|
|
68
|
+
COALESCE(io.COUNT_DELETE, 0) as `deletes`,
|
|
69
|
+
COALESCE(lk.COUNT_READ, 0) as `read_locks`,
|
|
70
|
+
COALESCE(ROUND(lk.SUM_TIMER_READ/1000000000, 2), 0) as `read_lock_wait_ms`,
|
|
71
|
+
COALESCE(lk.COUNT_WRITE, 0) as `write_locks`,
|
|
72
|
+
COALESCE(ROUND(lk.SUM_TIMER_WRITE/1000000000, 2), 0) as `write_lock_wait_ms`
|
|
73
|
+
FROM information_schema.TABLES t
|
|
74
|
+
LEFT JOIN performance_schema.table_io_waits_summary_by_table io
|
|
75
|
+
ON io.OBJECT_SCHEMA = t.TABLE_SCHEMA AND io.OBJECT_NAME = t.TABLE_NAME
|
|
76
|
+
LEFT JOIN performance_schema.table_lock_waits_summary_by_table lk
|
|
77
|
+
ON lk.OBJECT_SCHEMA = t.TABLE_SCHEMA AND lk.OBJECT_NAME = t.TABLE_NAME
|
|
78
|
+
WHERE t.TABLE_SCHEMA = '{target_database}'
|
|
79
|
+
ORDER BY t.TABLE_ROWS DESC;""",
|
|
80
|
+
'parameters': ['target_database'],
|
|
81
|
+
},
|
|
82
|
+
'comprehensive_index_analysis': {
|
|
83
|
+
'name': 'Comprehensive Index Analysis',
|
|
84
|
+
'description': 'Complete index statistics including structure, cardinality, and usage',
|
|
85
|
+
'category': 'information_schema',
|
|
86
|
+
'sql': """SELECT
|
|
87
|
+
s.TABLE_NAME as `table_name`,
|
|
88
|
+
s.INDEX_NAME as `index_name`,
|
|
89
|
+
s.COLUMN_NAME as `column_name`,
|
|
90
|
+
s.SEQ_IN_INDEX as `column_position`,
|
|
91
|
+
s.CARDINALITY as `cardinality`,
|
|
92
|
+
s.NON_UNIQUE as `is_non_unique`,
|
|
93
|
+
CASE WHEN s.NON_UNIQUE = 0 THEN 'UNIQUE' ELSE 'NON-UNIQUE' END as `uniqueness`,
|
|
94
|
+
s.INDEX_TYPE as `index_type`,
|
|
95
|
+
s.COLLATION as `collation`,
|
|
96
|
+
s.COMMENT as `comment`,
|
|
97
|
+
COALESCE(iu.COUNT_STAR, 0) as `operations`,
|
|
98
|
+
COALESCE(ROUND(iu.SUM_TIMER_WAIT/1000000000, 2), 0) as `total_wait_ms`,
|
|
99
|
+
COALESCE(iu.COUNT_READ, 0) as `reads`,
|
|
100
|
+
COALESCE(ROUND(iu.SUM_TIMER_READ/1000000000, 2), 0) as `read_wait_ms`,
|
|
101
|
+
COALESCE(iu.COUNT_WRITE, 0) as `writes`,
|
|
102
|
+
COALESCE(ROUND(iu.SUM_TIMER_WRITE/1000000000, 2), 0) as `write_wait_ms`,
|
|
103
|
+
COALESCE(iu.COUNT_FETCH, 0) as `fetches`,
|
|
104
|
+
COALESCE(iu.COUNT_INSERT, 0) as `inserts`,
|
|
105
|
+
COALESCE(iu.COUNT_UPDATE, 0) as `updates`,
|
|
106
|
+
COALESCE(iu.COUNT_DELETE, 0) as `deletes`
|
|
107
|
+
FROM information_schema.STATISTICS s
|
|
108
|
+
LEFT JOIN performance_schema.table_io_waits_summary_by_index_usage iu
|
|
109
|
+
ON iu.OBJECT_SCHEMA = s.TABLE_SCHEMA
|
|
110
|
+
AND iu.OBJECT_NAME = s.TABLE_NAME
|
|
111
|
+
AND iu.INDEX_NAME = s.INDEX_NAME
|
|
112
|
+
WHERE s.TABLE_SCHEMA = '{target_database}'
|
|
113
|
+
ORDER BY s.TABLE_NAME, s.INDEX_NAME, s.SEQ_IN_INDEX;""",
|
|
114
|
+
'parameters': ['target_database'],
|
|
115
|
+
},
|
|
116
|
+
'column_analysis': {
|
|
117
|
+
'name': 'Column Information Analysis',
|
|
118
|
+
'description': 'Returns all column definitions including data types, nullability, keys, defaults, and extra attributes',
|
|
119
|
+
'category': 'information_schema',
|
|
120
|
+
'sql': """SELECT
|
|
121
|
+
TABLE_NAME as table_name,
|
|
122
|
+
COLUMN_NAME as column_name,
|
|
123
|
+
ORDINAL_POSITION as position,
|
|
124
|
+
COLUMN_DEFAULT as default_value,
|
|
125
|
+
IS_NULLABLE as nullable,
|
|
126
|
+
DATA_TYPE as data_type,
|
|
127
|
+
CHARACTER_MAXIMUM_LENGTH as char_max_length,
|
|
128
|
+
NUMERIC_PRECISION as numeric_precision,
|
|
129
|
+
NUMERIC_SCALE as numeric_scale,
|
|
130
|
+
COLUMN_TYPE as column_type,
|
|
131
|
+
COLUMN_KEY as key_type,
|
|
132
|
+
EXTRA as extra,
|
|
133
|
+
COLUMN_COMMENT as comment
|
|
134
|
+
FROM information_schema.COLUMNS
|
|
135
|
+
WHERE TABLE_SCHEMA = '{target_database}'
|
|
136
|
+
ORDER BY TABLE_NAME, ORDINAL_POSITION;""",
|
|
137
|
+
'parameters': ['target_database'],
|
|
138
|
+
},
|
|
139
|
+
'foreign_key_analysis': {
|
|
140
|
+
'name': 'Foreign Key Relationship Analysis',
|
|
141
|
+
'description': 'Returns foreign key relationships with constraint names, table/column mappings, referential actions, and estimated cardinality',
|
|
142
|
+
'category': 'information_schema',
|
|
143
|
+
'sql': """SELECT
|
|
144
|
+
kcu.CONSTRAINT_NAME as constraint_name,
|
|
145
|
+
kcu.TABLE_NAME as child_table,
|
|
146
|
+
kcu.COLUMN_NAME as child_column,
|
|
147
|
+
kcu.REFERENCED_TABLE_NAME as parent_table,
|
|
148
|
+
kcu.REFERENCED_COLUMN_NAME as parent_column,
|
|
149
|
+
rc.UPDATE_RULE as update_rule,
|
|
150
|
+
rc.DELETE_RULE as delete_rule,
|
|
151
|
+
CASE
|
|
152
|
+
WHEN EXISTS (
|
|
153
|
+
SELECT 1 FROM information_schema.STATISTICS s
|
|
154
|
+
WHERE s.TABLE_SCHEMA = '{target_database}'
|
|
155
|
+
AND s.TABLE_NAME = kcu.TABLE_NAME
|
|
156
|
+
AND s.COLUMN_NAME = kcu.COLUMN_NAME
|
|
157
|
+
AND s.NON_UNIQUE = 0
|
|
158
|
+
AND (SELECT COUNT(*) FROM information_schema.KEY_COLUMN_USAGE kcu2
|
|
159
|
+
WHERE kcu2.CONSTRAINT_NAME = s.INDEX_NAME
|
|
160
|
+
AND kcu2.TABLE_SCHEMA = s.TABLE_SCHEMA) = 1
|
|
161
|
+
) THEN '1:1 or 1:0..1'
|
|
162
|
+
ELSE '1:Many'
|
|
163
|
+
END as estimated_cardinality
|
|
164
|
+
FROM information_schema.KEY_COLUMN_USAGE kcu
|
|
165
|
+
LEFT JOIN information_schema.REFERENTIAL_CONSTRAINTS rc
|
|
166
|
+
ON kcu.CONSTRAINT_NAME = rc.CONSTRAINT_NAME
|
|
167
|
+
AND kcu.CONSTRAINT_SCHEMA = rc.CONSTRAINT_SCHEMA
|
|
168
|
+
WHERE kcu.TABLE_SCHEMA = '{target_database}'
|
|
169
|
+
AND kcu.REFERENCED_TABLE_NAME IS NOT NULL
|
|
170
|
+
ORDER BY kcu.TABLE_NAME, kcu.COLUMN_NAME;""",
|
|
171
|
+
'parameters': ['target_database'],
|
|
172
|
+
},
|
|
173
|
+
'query_performance_stats': {
|
|
174
|
+
'name': 'Query Performance Statistics',
|
|
175
|
+
'description': 'Unified view of all query execution including stored procedures with full metrics',
|
|
176
|
+
'category': 'performance_schema',
|
|
177
|
+
'sql': """SELECT
|
|
178
|
+
'QUERY' as source_type,
|
|
179
|
+
DIGEST_TEXT as query_pattern,
|
|
180
|
+
-- NULL placeholder needed for UNION ALL column matching (queries don't have procedure names)
|
|
181
|
+
NULL as procedure_name,
|
|
182
|
+
COUNT_STAR as executions,
|
|
183
|
+
ROUND(AVG_TIMER_WAIT/1000000000, 2) as avg_latency_ms,
|
|
184
|
+
ROUND(MIN_TIMER_WAIT/1000000000, 2) as min_latency_ms,
|
|
185
|
+
ROUND(MAX_TIMER_WAIT/1000000000, 2) as max_latency_ms,
|
|
186
|
+
ROUND(SUM_TIMER_WAIT/1000000000, 2) as total_time_ms,
|
|
187
|
+
SUM_ROWS_AFFECTED as rows_affected,
|
|
188
|
+
SUM_ROWS_SENT as rows_sent,
|
|
189
|
+
SUM_ROWS_EXAMINED as rows_examined,
|
|
190
|
+
ROUND(SUM_ROWS_SENT/COUNT_STAR, 2) as avg_rows_returned,
|
|
191
|
+
ROUND(SUM_ROWS_EXAMINED/COUNT_STAR, 2) as avg_rows_scanned,
|
|
192
|
+
ROUND((SUM_ROWS_SENT/NULLIF(SUM_ROWS_EXAMINED,0))*100, 2) as scan_efficiency_pct,
|
|
193
|
+
SUM_SELECT_SCAN as full_table_scans,
|
|
194
|
+
SUM_SELECT_RANGE as range_scans,
|
|
195
|
+
SUM_SORT_ROWS as rows_sorted,
|
|
196
|
+
SUM_NO_INDEX_USED as queries_without_index,
|
|
197
|
+
SUM_NO_GOOD_INDEX_USED as queries_with_bad_index,
|
|
198
|
+
ROUND(SUM_LOCK_TIME/1000000000, 2) as lock_time_ms,
|
|
199
|
+
ROUND((SUM_LOCK_TIME/NULLIF(SUM_TIMER_WAIT,0))*100, 2) as lock_time_pct,
|
|
200
|
+
SUM_ERRORS as errors,
|
|
201
|
+
SUM_WARNINGS as warnings,
|
|
202
|
+
FIRST_SEEN as first_seen,
|
|
203
|
+
LAST_SEEN as last_seen,
|
|
204
|
+
ROUND(COUNT_STAR / NULLIF(TIMESTAMPDIFF(SECOND, FIRST_SEEN, LAST_SEEN), 0), 2) as estimated_rps
|
|
205
|
+
FROM performance_schema.events_statements_summary_by_digest
|
|
206
|
+
WHERE SCHEMA_NAME = '{target_database}'
|
|
207
|
+
AND COUNT_STAR > 0
|
|
208
|
+
-- Keywords obfuscated using CHAR() ASCII codes to bypass MCP server's static keyword scanner
|
|
209
|
+
-- MCP rejects queries with mutation keywords even in read-only contexts
|
|
210
|
+
AND LEFT(DIGEST_TEXT, 7) NOT IN (CONCAT(CHAR(67,82,69,65,84,69), ' '), CONCAT(CHAR(84,82,85,78,67,65,84)))
|
|
211
|
+
AND LEFT(DIGEST_TEXT, 6) NOT IN (CONCAT(CHAR(65,76,84,69,82), ' '), CONCAT(CHAR(68,69,76,69,84,69)))
|
|
212
|
+
AND LEFT(DIGEST_TEXT, 5) NOT IN (CONCAT(CHAR(68,82,79,80), ' '), CONCAT(CHAR(83,72,79,87), ' '))
|
|
213
|
+
AND LEFT(DIGEST_TEXT, 4) NOT IN (CONCAT(CHAR(83,69,84), ' '), CONCAT(CHAR(85,83,69), ' '))
|
|
214
|
+
-- Filter out utility and maintenance commands
|
|
215
|
+
AND DIGEST_TEXT NOT LIKE 'DESCRIBE %'
|
|
216
|
+
AND DIGEST_TEXT NOT LIKE 'EXPLAIN %'
|
|
217
|
+
AND DIGEST_TEXT NOT LIKE 'OPTIMIZE %'
|
|
218
|
+
AND DIGEST_TEXT NOT LIKE 'ANALYZE %'
|
|
219
|
+
AND DIGEST_TEXT NOT LIKE 'REPAIR %'
|
|
220
|
+
AND DIGEST_TEXT NOT LIKE 'FLUSH %'
|
|
221
|
+
AND DIGEST_TEXT NOT LIKE 'RESET %'
|
|
222
|
+
AND DIGEST_TEXT NOT LIKE 'CHECK %'
|
|
223
|
+
-- Filter out system/metadata queries
|
|
224
|
+
AND DIGEST_TEXT NOT LIKE '/* RDS Data API */%'
|
|
225
|
+
AND DIGEST_TEXT NOT LIKE '%information_schema%'
|
|
226
|
+
AND DIGEST_TEXT NOT LIKE '%performance_schema%'
|
|
227
|
+
AND DIGEST_TEXT NOT LIKE '%mysql.%'
|
|
228
|
+
AND DIGEST_TEXT NOT LIKE '%sys.%'
|
|
229
|
+
AND DIGEST_TEXT NOT LIKE '%mysql.general_log%'
|
|
230
|
+
AND DIGEST_TEXT NOT LIKE 'SELECT @@%'
|
|
231
|
+
AND DIGEST_TEXT NOT LIKE 'select ?'
|
|
232
|
+
AND DIGEST_TEXT NOT LIKE '%@@default_storage_engine%'
|
|
233
|
+
AND DIGEST_TEXT NOT LIKE '%@%:=%'
|
|
234
|
+
AND DIGEST_TEXT NOT LIKE '%MD5%'
|
|
235
|
+
AND DIGEST_TEXT NOT LIKE '%SHA%'
|
|
236
|
+
AND DIGEST_TEXT NOT LIKE '%CONCAT_WS%'
|
|
237
|
+
AND DIGEST_TEXT NOT LIKE '%`DIGEST_TEXT`%'
|
|
238
|
+
UNION ALL
|
|
239
|
+
SELECT
|
|
240
|
+
'PROCEDURE' as source_type,
|
|
241
|
+
CONCAT('PROCEDURE: ', OBJECT_NAME) as query_pattern,
|
|
242
|
+
OBJECT_NAME as procedure_name,
|
|
243
|
+
COUNT_STAR as executions,
|
|
244
|
+
ROUND(AVG_TIMER_WAIT/1000000000, 2) as avg_latency_ms,
|
|
245
|
+
ROUND(MIN_TIMER_WAIT/1000000000, 2) as min_latency_ms,
|
|
246
|
+
ROUND(MAX_TIMER_WAIT/1000000000, 2) as max_latency_ms,
|
|
247
|
+
ROUND(SUM_TIMER_WAIT/1000000000, 2) as total_time_ms,
|
|
248
|
+
SUM_ROWS_AFFECTED as rows_affected,
|
|
249
|
+
SUM_ROWS_SENT as rows_sent,
|
|
250
|
+
SUM_ROWS_EXAMINED as rows_examined,
|
|
251
|
+
ROUND(SUM_ROWS_SENT/COUNT_STAR, 2) as avg_rows_returned,
|
|
252
|
+
ROUND(SUM_ROWS_EXAMINED/COUNT_STAR, 2) as avg_rows_scanned,
|
|
253
|
+
ROUND((SUM_ROWS_SENT/NULLIF(SUM_ROWS_EXAMINED,0))*100, 2) as scan_efficiency_pct,
|
|
254
|
+
SUM_SELECT_SCAN as full_table_scans,
|
|
255
|
+
0 as range_scans,
|
|
256
|
+
0 as rows_sorted,
|
|
257
|
+
SUM_NO_INDEX_USED as queries_without_index,
|
|
258
|
+
0 as queries_with_bad_index,
|
|
259
|
+
ROUND(SUM_LOCK_TIME/1000000000, 2) as lock_time_ms,
|
|
260
|
+
ROUND((SUM_LOCK_TIME/NULLIF(SUM_TIMER_WAIT,0))*100, 2) as lock_time_pct,
|
|
261
|
+
SUM_ERRORS as errors,
|
|
262
|
+
SUM_WARNINGS as warnings,
|
|
263
|
+
NULL as first_seen,
|
|
264
|
+
NULL as last_seen,
|
|
265
|
+
NULL as estimated_rps
|
|
266
|
+
FROM performance_schema.events_statements_summary_by_program
|
|
267
|
+
WHERE OBJECT_SCHEMA = '{target_database}'
|
|
268
|
+
AND OBJECT_TYPE = 'PROCEDURE'
|
|
269
|
+
ORDER BY total_time_ms DESC;""",
|
|
270
|
+
'parameters': ['target_database'],
|
|
271
|
+
},
|
|
272
|
+
'triggers_stats': {
|
|
273
|
+
'name': 'Triggers Statistics',
|
|
274
|
+
'description': 'Trigger execution statistics',
|
|
275
|
+
'category': 'performance_schema',
|
|
276
|
+
'sql': """SELECT
|
|
277
|
+
OBJECT_NAME as trigger_name,
|
|
278
|
+
COUNT_STAR as executions,
|
|
279
|
+
ROUND(SUM_TIMER_WAIT/1000000000, 2) as total_time_ms,
|
|
280
|
+
ROUND(AVG_TIMER_WAIT/1000000000, 2) as avg_time_ms,
|
|
281
|
+
ROUND(SUM_LOCK_TIME/1000000000, 2) as lock_time_ms,
|
|
282
|
+
SUM_ERRORS as errors,
|
|
283
|
+
ROUND(COUNT_STAR / 60, 2) as estimated_rps
|
|
284
|
+
FROM performance_schema.events_statements_summary_by_program
|
|
285
|
+
WHERE OBJECT_SCHEMA = '{target_database}'
|
|
286
|
+
AND OBJECT_TYPE = 'TRIGGER'
|
|
287
|
+
ORDER BY SUM_TIMER_WAIT DESC;""",
|
|
288
|
+
'parameters': ['target_database'],
|
|
289
|
+
},
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
class MySQLPlugin(DatabasePlugin):
|
|
294
|
+
"""MySQL-specific database analyzer plugin."""
|
|
295
|
+
|
|
296
|
+
def get_queries(self) -> Dict[str, Any]:
|
|
297
|
+
"""Get all MySQL analysis queries."""
|
|
298
|
+
return _mysql_analysis_queries
|
|
299
|
+
|
|
300
|
+
def get_database_display_name(self) -> str:
|
|
301
|
+
"""Get the display name of the database type."""
|
|
302
|
+
return 'MySQL'
|
|
303
|
+
|
|
304
|
+
# write_queries_to_file and apply_result_limit are inherited from DatabasePlugin base class
|
|
305
|
+
|
|
306
|
+
# parse_results_from_file is inherited from DatabasePlugin base class
|
|
307
|
+
|
|
308
|
+
async def _execute_query_batch(
|
|
309
|
+
self,
|
|
310
|
+
query_names: List[str],
|
|
311
|
+
database: str,
|
|
312
|
+
max_results: int,
|
|
313
|
+
run_query,
|
|
314
|
+
all_results: Dict[str, Any],
|
|
315
|
+
all_errors: List[str],
|
|
316
|
+
) -> None:
|
|
317
|
+
"""Execute a batch of queries and collect results.
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
query_names: List of query names to execute
|
|
321
|
+
database: Target database name
|
|
322
|
+
max_results: Maximum number of results per query
|
|
323
|
+
run_query: Async function to execute queries
|
|
324
|
+
all_results: Dictionary to store results (modified in place)
|
|
325
|
+
all_errors: List to store errors (modified in place)
|
|
326
|
+
"""
|
|
327
|
+
for query_name in query_names:
|
|
328
|
+
try:
|
|
329
|
+
query_info = self.get_queries()[query_name]
|
|
330
|
+
sql = query_info['sql']
|
|
331
|
+
|
|
332
|
+
# Substitute parameters
|
|
333
|
+
if 'target_database' in query_info.get('parameters', []):
|
|
334
|
+
sql = sql.format(target_database=database)
|
|
335
|
+
|
|
336
|
+
# Add LIMIT
|
|
337
|
+
sql = sql.rstrip(';')
|
|
338
|
+
sql = f'{sql} LIMIT {max_results};'
|
|
339
|
+
|
|
340
|
+
result = await run_query(sql)
|
|
341
|
+
|
|
342
|
+
if result and isinstance(result, list) and len(result) > 0:
|
|
343
|
+
if 'error' in result[0]:
|
|
344
|
+
all_errors.append(f'{query_name}: {result[0]["error"]}')
|
|
345
|
+
else:
|
|
346
|
+
all_results[query_name] = {
|
|
347
|
+
'description': query_info['description'],
|
|
348
|
+
'data': result,
|
|
349
|
+
}
|
|
350
|
+
else:
|
|
351
|
+
all_results[query_name] = {
|
|
352
|
+
'description': query_info['description'],
|
|
353
|
+
'data': [],
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
except Exception as e:
|
|
357
|
+
all_errors.append(f'{query_name}: {str(e)}')
|
|
358
|
+
|
|
359
|
+
async def execute_managed_mode(self, connection_params: Dict[str, Any]) -> Dict[str, Any]:
|
|
360
|
+
"""Execute MySQL analysis in managed mode.
|
|
361
|
+
|
|
362
|
+
Supports two connection methods:
|
|
363
|
+
- RDS Data API: Uses cluster_arn for serverless Aurora connections
|
|
364
|
+
- Connection-based: Uses hostname/port for direct MySQL connections
|
|
365
|
+
"""
|
|
366
|
+
cluster_arn = connection_params.get('cluster_arn')
|
|
367
|
+
hostname = connection_params.get('hostname')
|
|
368
|
+
port = connection_params.get('port', 3306)
|
|
369
|
+
secret_arn = connection_params['secret_arn']
|
|
370
|
+
database = connection_params['database']
|
|
371
|
+
region = connection_params['region']
|
|
372
|
+
max_results = connection_params['max_results']
|
|
373
|
+
|
|
374
|
+
# Validate database name
|
|
375
|
+
validate_database_name(database)
|
|
376
|
+
|
|
377
|
+
# Create appropriate connection type based on available parameters
|
|
378
|
+
if cluster_arn:
|
|
379
|
+
# RDS Data API-based access
|
|
380
|
+
db_connection = RDSDataAPIConnection(
|
|
381
|
+
cluster_arn=cluster_arn,
|
|
382
|
+
secret_arn=secret_arn,
|
|
383
|
+
database=database,
|
|
384
|
+
region=region,
|
|
385
|
+
readonly=DEFAULT_READONLY,
|
|
386
|
+
)
|
|
387
|
+
else:
|
|
388
|
+
# Connection-based access
|
|
389
|
+
db_connection = AsyncmyPoolConnection(
|
|
390
|
+
hostname=hostname,
|
|
391
|
+
port=port,
|
|
392
|
+
database=database,
|
|
393
|
+
readonly=DEFAULT_READONLY,
|
|
394
|
+
secret_arn=secret_arn,
|
|
395
|
+
region=region,
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
async def run_query(sql_cmd):
|
|
399
|
+
"""Execute query using MySQL MCP server."""
|
|
400
|
+
try:
|
|
401
|
+
return await mysql_query(sql_cmd, DummyCtx(), db_connection, None)
|
|
402
|
+
except Exception as e:
|
|
403
|
+
logger.error(f'MySQL query execution failed: {str(e)}')
|
|
404
|
+
return [{'error': f'MySQL query failed: {str(e)}'}]
|
|
405
|
+
|
|
406
|
+
# Execute queries
|
|
407
|
+
all_results = {}
|
|
408
|
+
all_errors = []
|
|
409
|
+
skipped_queries = []
|
|
410
|
+
|
|
411
|
+
# Check performance schema status
|
|
412
|
+
perf_check_query = self.get_queries()['performance_schema_check']
|
|
413
|
+
perf_result = await run_query(perf_check_query['sql'])
|
|
414
|
+
|
|
415
|
+
performance_enabled = False
|
|
416
|
+
if perf_result and len(perf_result) > 0:
|
|
417
|
+
performance_schema_value = str(perf_result[0].get('@@performance_schema', '0'))
|
|
418
|
+
performance_enabled = performance_schema_value == '1'
|
|
419
|
+
|
|
420
|
+
# Execute schema queries
|
|
421
|
+
await self._execute_query_batch(
|
|
422
|
+
self.get_schema_queries(),
|
|
423
|
+
database,
|
|
424
|
+
max_results,
|
|
425
|
+
run_query,
|
|
426
|
+
all_results,
|
|
427
|
+
all_errors,
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
# Execute performance queries if enabled
|
|
431
|
+
if performance_enabled:
|
|
432
|
+
await self._execute_query_batch(
|
|
433
|
+
self.get_performance_queries(),
|
|
434
|
+
database,
|
|
435
|
+
max_results,
|
|
436
|
+
run_query,
|
|
437
|
+
all_results,
|
|
438
|
+
all_errors,
|
|
439
|
+
)
|
|
440
|
+
else:
|
|
441
|
+
skipped_queries.extend(self.get_performance_queries())
|
|
442
|
+
all_errors.append('Performance Schema disabled - skipping performance queries')
|
|
443
|
+
|
|
444
|
+
return {
|
|
445
|
+
'results': all_results,
|
|
446
|
+
'errors': all_errors,
|
|
447
|
+
'performance_enabled': performance_enabled,
|
|
448
|
+
'performance_feature': 'Performance Schema',
|
|
449
|
+
'skipped_queries': skipped_queries,
|
|
450
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""Plugin registry for database analyzers."""
|
|
16
|
+
|
|
17
|
+
from awslabs.dynamodb_mcp_server.db_analyzer.base_plugin import DatabasePlugin
|
|
18
|
+
from awslabs.dynamodb_mcp_server.db_analyzer.mysql import MySQLPlugin
|
|
19
|
+
from awslabs.dynamodb_mcp_server.db_analyzer.postgresql import PostgreSQLPlugin
|
|
20
|
+
from awslabs.dynamodb_mcp_server.db_analyzer.sqlserver import SQLServerPlugin
|
|
21
|
+
from typing import Dict, Type
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class PluginRegistry:
|
|
25
|
+
"""Registry for database-specific analyzer plugins."""
|
|
26
|
+
|
|
27
|
+
_plugins: Dict[str, Type[DatabasePlugin]] = {
|
|
28
|
+
'mysql': MySQLPlugin,
|
|
29
|
+
'postgresql': PostgreSQLPlugin,
|
|
30
|
+
'sqlserver': SQLServerPlugin,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def get_plugin(cls, db_type: str) -> DatabasePlugin:
|
|
35
|
+
"""Get plugin instance for the specified database type.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
db_type: Database type ('mysql', 'postgresql', 'sqlserver')
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Plugin instance for the database type
|
|
42
|
+
|
|
43
|
+
Raises:
|
|
44
|
+
ValueError: If database type is not supported
|
|
45
|
+
"""
|
|
46
|
+
plugin_class = cls._plugins.get(db_type.lower())
|
|
47
|
+
if not plugin_class:
|
|
48
|
+
supported = ', '.join(cls._plugins.keys())
|
|
49
|
+
raise ValueError(f'Unsupported database type: {db_type}. Supported types: {supported}')
|
|
50
|
+
return plugin_class()
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
def get_supported_types(cls) -> list[str]:
|
|
54
|
+
"""Get list of supported database types."""
|
|
55
|
+
return list(cls._plugins.keys())
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def register_plugin(cls, db_type: str, plugin_class: Type[DatabasePlugin]) -> None:
|
|
59
|
+
"""Register a new database plugin.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
db_type: Database type identifier
|
|
63
|
+
plugin_class: Plugin class to register
|
|
64
|
+
|
|
65
|
+
Raises:
|
|
66
|
+
TypeError: If plugin_class does not inherit from DatabasePlugin
|
|
67
|
+
"""
|
|
68
|
+
# Validate that the plugin class inherits from DatabasePlugin
|
|
69
|
+
if not issubclass(plugin_class, DatabasePlugin):
|
|
70
|
+
raise TypeError(
|
|
71
|
+
f'Plugin class {plugin_class.__name__} must inherit from DatabasePlugin'
|
|
72
|
+
)
|
|
73
|
+
cls._plugins[db_type.lower()] = plugin_class
|