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.
Files changed (27) hide show
  1. awslabs/__init__.py +17 -0
  2. awslabs/dynamodb_mcp_server/__init__.py +17 -0
  3. awslabs/dynamodb_mcp_server/cdk_generator/__init__.py +19 -0
  4. awslabs/dynamodb_mcp_server/cdk_generator/generator.py +276 -0
  5. awslabs/dynamodb_mcp_server/cdk_generator/models.py +521 -0
  6. awslabs/dynamodb_mcp_server/cdk_generator/templates/README.md +57 -0
  7. awslabs/dynamodb_mcp_server/cdk_generator/templates/stack.ts.j2 +70 -0
  8. awslabs/dynamodb_mcp_server/common.py +94 -0
  9. awslabs/dynamodb_mcp_server/db_analyzer/__init__.py +30 -0
  10. awslabs/dynamodb_mcp_server/db_analyzer/analyzer_utils.py +394 -0
  11. awslabs/dynamodb_mcp_server/db_analyzer/base_plugin.py +355 -0
  12. awslabs/dynamodb_mcp_server/db_analyzer/mysql.py +450 -0
  13. awslabs/dynamodb_mcp_server/db_analyzer/plugin_registry.py +73 -0
  14. awslabs/dynamodb_mcp_server/db_analyzer/postgresql.py +215 -0
  15. awslabs/dynamodb_mcp_server/db_analyzer/sqlserver.py +255 -0
  16. awslabs/dynamodb_mcp_server/markdown_formatter.py +513 -0
  17. awslabs/dynamodb_mcp_server/model_validation_utils.py +845 -0
  18. awslabs/dynamodb_mcp_server/prompts/dynamodb_architect.md +851 -0
  19. awslabs/dynamodb_mcp_server/prompts/json_generation_guide.md +185 -0
  20. awslabs/dynamodb_mcp_server/prompts/transform_model_validation_result.md +168 -0
  21. awslabs/dynamodb_mcp_server/server.py +524 -0
  22. awslabs_dynamodb_mcp_server-2.0.10.dist-info/METADATA +306 -0
  23. awslabs_dynamodb_mcp_server-2.0.10.dist-info/RECORD +27 -0
  24. awslabs_dynamodb_mcp_server-2.0.10.dist-info/WHEEL +4 -0
  25. awslabs_dynamodb_mcp_server-2.0.10.dist-info/entry_points.txt +2 -0
  26. awslabs_dynamodb_mcp_server-2.0.10.dist-info/licenses/LICENSE +175 -0
  27. awslabs_dynamodb_mcp_server-2.0.10.dist-info/licenses/NOTICE +2 -0
@@ -0,0 +1,355 @@
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
+ #!/usr/bin/env python3
16
+
17
+ """Base plugin interface for database analyzers."""
18
+
19
+ import os
20
+ from abc import ABC, abstractmethod
21
+ from awslabs.dynamodb_mcp_server.common import validate_database_name
22
+ from datetime import datetime
23
+ from typing import Any, Dict
24
+
25
+
26
+ class DatabasePlugin(ABC):
27
+ """Base class for database-specific analyzer plugins."""
28
+
29
+ @abstractmethod
30
+ def get_queries(self) -> Dict[str, Any]:
31
+ """Get all analysis queries for this database type.
32
+
33
+ Returns:
34
+ Dictionary of query definitions with metadata
35
+ """
36
+ pass
37
+
38
+ @abstractmethod
39
+ def get_database_display_name(self) -> str:
40
+ """Get the display name of the database type.
41
+
42
+ Returns:
43
+ Database type name (e.g., 'MySQL', 'PostgreSQL', 'SQL Server')
44
+ """
45
+ pass
46
+
47
+ def apply_result_limit(self, sql: str, max_results: int) -> str:
48
+ """Apply result limit to SQL query.
49
+
50
+ Default implementation uses LIMIT syntax (MySQL/PostgreSQL).
51
+ Override for databases with different syntax (e.g., SQL Server uses TOP).
52
+
53
+ Args:
54
+ sql: SQL query string
55
+ max_results: Maximum number of results
56
+
57
+ Returns:
58
+ SQL query with limit applied
59
+ """
60
+ sql = sql.rstrip(';')
61
+ return f'{sql} LIMIT {max_results};'
62
+
63
+ def write_queries_to_file(
64
+ self, target_database: str, max_results: int, output_file: str
65
+ ) -> str:
66
+ """Generate SQL file with all analysis queries.
67
+
68
+ Database-specific behavior is handled through get_database_display_name() and
69
+ apply_result_limit() methods.
70
+
71
+ Args:
72
+ target_database: Target database/schema name
73
+ max_results: Maximum results per query
74
+ output_file: Path to output SQL file
75
+
76
+ Returns:
77
+ Path to generated file
78
+ """
79
+ # Validate database name before using it
80
+ validate_database_name(target_database)
81
+
82
+ queries = self.get_queries()
83
+
84
+ sql_content = [
85
+ f'-- {self.get_database_display_name()} Database Analysis Queries',
86
+ f'-- Target Database: {target_database}',
87
+ f'-- Generated: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}',
88
+ '',
89
+ '-- EXECUTION INSTRUCTIONS:',
90
+ '-- 1. Review all queries before execution',
91
+ '-- 2. Run during off-peak hours if possible',
92
+ '-- 3. Each query has a LIMIT clause to prevent excessive results',
93
+ '',
94
+ '-- Generated for DynamoDB Data Modeling\n',
95
+ ]
96
+
97
+ total_queries = sum(1 for q in queries.values() if q.get('category') != 'internal')
98
+ query_number = 0
99
+
100
+ for query_name, query_info in queries.items():
101
+ # Skip internal queries
102
+ if query_info.get('category') == 'internal':
103
+ continue
104
+
105
+ query_number += 1
106
+
107
+ sql_content.append('')
108
+ sql_content.append('-- ============================================')
109
+ sql_content.append(f'-- QUERY {query_number}/{total_queries}: {query_name}')
110
+ sql_content.append('-- ============================================')
111
+ sql_content.append(f'-- Description: {query_info.get("description", "N/A")}')
112
+ sql_content.append(f'-- Category: {query_info.get("category", "N/A")}')
113
+
114
+ # Add marker as a SELECT statement that outputs to results
115
+ sql_content.append(f"SELECT '-- QUERY_NAME_START: {query_name}' AS marker;")
116
+
117
+ sql = query_info['sql']
118
+ # Substitute target_database parameter
119
+ if 'target_database' in query_info.get('parameters', []):
120
+ sql = sql.format(target_database=target_database)
121
+
122
+ # Apply result limit (database-specific)
123
+ sql = self.apply_result_limit(sql, max_results)
124
+
125
+ sql_content.append(sql)
126
+
127
+ # Add end marker as a SELECT statement
128
+ sql_content.append(f"SELECT '-- QUERY_NAME_END: {query_name}' AS marker;")
129
+ sql_content.append('')
130
+
131
+ # Write to file
132
+ with open(output_file, 'w', encoding='utf-8') as f:
133
+ f.write('\n'.join(sql_content))
134
+
135
+ return output_file
136
+
137
+ def parse_results_from_file(self, result_file_path: str) -> Dict[str, Any]:
138
+ """Parse query results from user-provided file.
139
+
140
+ It parses results with QUERY_NAME_START/END markers and supports
141
+ both pipe-separated and tab-separated formats.
142
+
143
+ Args:
144
+ result_file_path: Path to file containing query results
145
+
146
+ Returns:
147
+ Dictionary mapping query names to result data in standard format
148
+ """
149
+ # Validate path to prevent path traversal attacks
150
+ # Use absolute path and check for path traversal patterns
151
+ if '..' in result_file_path:
152
+ raise ValueError(f'Path traversal detected in result file path: {result_file_path}')
153
+
154
+ result_file_path = os.path.abspath(result_file_path)
155
+
156
+ if not os.path.exists(result_file_path):
157
+ raise FileNotFoundError(f'Result file not found: {result_file_path}')
158
+
159
+ with open(result_file_path, 'r', encoding='utf-8') as f:
160
+ content = f.read()
161
+
162
+ results = {}
163
+ current_query = None
164
+ current_headers = []
165
+ current_data = []
166
+
167
+ lines = content.split('\n')
168
+ i = 0
169
+
170
+ while i < len(lines):
171
+ line = lines[i].strip()
172
+
173
+ # Skip empty lines
174
+ if not line:
175
+ if current_query and current_data:
176
+ results[current_query] = {
177
+ 'description': f'Results for {current_query}',
178
+ 'data': current_data,
179
+ }
180
+ current_query = None
181
+ current_headers = []
182
+ current_data = []
183
+ i += 1
184
+ continue
185
+
186
+ # Check for query name markers
187
+ if line.startswith('--'):
188
+ if 'QUERY_NAME_START:' in line:
189
+ # Save previous query if exists
190
+ if current_query and current_data:
191
+ results[current_query] = {
192
+ 'description': f'Results for {current_query}',
193
+ 'data': current_data,
194
+ }
195
+ # Extract query name from marker
196
+ current_query = line.split('QUERY_NAME_START:')[1].strip()
197
+ current_headers = []
198
+ current_data = []
199
+ elif 'QUERY_NAME_END:' in line:
200
+ # Save current query results (even if empty)
201
+ if current_query:
202
+ results[current_query] = {
203
+ 'description': f'Results for {current_query}',
204
+ 'data': current_data,
205
+ }
206
+ current_query = None
207
+ current_headers = []
208
+ current_data = []
209
+ i += 1
210
+ continue
211
+
212
+ # Skip separator lines
213
+ if all(c in '-+| ' for c in line):
214
+ i += 1
215
+ continue
216
+
217
+ # Skip row count lines
218
+ if line.startswith('(') and 'row' in line.lower():
219
+ i += 1
220
+ continue
221
+
222
+ # Parse data row (support both tab and pipe separated)
223
+ if '\t' in line:
224
+ values = [v.strip() for v in line.split('\t')]
225
+ elif '|' in line:
226
+ parts = line.split('|')
227
+ if parts and not parts[0].strip():
228
+ parts = parts[1:]
229
+ if parts and not parts[-1].strip():
230
+ parts = parts[:-1]
231
+ values = [v.strip() for v in parts]
232
+ else:
233
+ i += 1
234
+ continue
235
+
236
+ if not values:
237
+ i += 1
238
+ continue
239
+
240
+ # Check if this is a marker row (from SELECT '-- QUERY_NAME_START: ...' AS marker)
241
+ if len(values) > 0 and 'QUERY_NAME_START:' in values[0]:
242
+ # Save previous query if exists
243
+ if current_query and current_data:
244
+ results[current_query] = {
245
+ 'description': f'Results for {current_query}',
246
+ 'data': current_data,
247
+ }
248
+ # Extract query name from marker value
249
+ marker_text = values[0]
250
+ if 'QUERY_NAME_START:' in marker_text:
251
+ current_query = marker_text.split('QUERY_NAME_START:')[1].strip()
252
+ current_headers = []
253
+ current_data = []
254
+ i += 1
255
+ continue
256
+ elif len(values) > 0 and 'QUERY_NAME_END:' in values[0]:
257
+ # Save current query results (even if empty)
258
+ if current_query:
259
+ results[current_query] = {
260
+ 'description': f'Results for {current_query}',
261
+ 'data': current_data,
262
+ }
263
+ current_query = None
264
+ current_headers = []
265
+ current_data = []
266
+ i += 1
267
+ continue
268
+
269
+ # First line is headers
270
+ if not current_headers:
271
+ current_headers = values
272
+ # Skip if this is the marker column header
273
+ if len(current_headers) == 1 and current_headers[0].lower() == 'marker':
274
+ current_headers = []
275
+ else:
276
+ # Data row
277
+ if len(values) == len(current_headers):
278
+ row_dict = {}
279
+ for header, value in zip(current_headers, values):
280
+ if value.lower() in ['null', 'none', '']:
281
+ row_dict[header] = None
282
+ elif value.replace('.', '', 1).replace('-', '', 1).isdigit():
283
+ try:
284
+ if '.' in value:
285
+ row_dict[header] = float(value)
286
+ else:
287
+ row_dict[header] = int(value)
288
+ except ValueError:
289
+ row_dict[header] = value
290
+ else:
291
+ row_dict[header] = value
292
+ current_data.append(row_dict)
293
+
294
+ i += 1
295
+
296
+ # Save last query
297
+ if current_query and current_data:
298
+ results[current_query] = {
299
+ 'description': f'Results for {current_query}',
300
+ 'data': current_data,
301
+ }
302
+
303
+ return results
304
+
305
+ @abstractmethod
306
+ async def execute_managed_mode(self, connection_params: Dict[str, Any]) -> Dict[str, Any]:
307
+ """Execute analysis in managed mode (direct database connection).
308
+
309
+ Args:
310
+ connection_params: Database connection parameters
311
+
312
+ Returns:
313
+ Analysis result dictionary with results, errors, and metadata
314
+ """
315
+ pass
316
+
317
+ def get_queries_by_category(self, category: str) -> list[str]:
318
+ """Get list of query names for a specific category.
319
+
320
+ Args:
321
+ category: Query category ('information_schema', 'performance_schema', 'internal')
322
+
323
+ Returns:
324
+ List of query names in the specified category
325
+ """
326
+ queries = self.get_queries()
327
+ return [
328
+ query_name
329
+ for query_name, query_info in queries.items()
330
+ if query_info.get('category') == category
331
+ ]
332
+
333
+ def get_query_descriptions(self) -> Dict[str, str]:
334
+ """Get mapping of query names to their descriptions.
335
+
336
+ Returns:
337
+ Dictionary mapping query names to human-readable descriptions
338
+ """
339
+ queries = self.get_queries()
340
+ descriptions = {}
341
+ for query_name, query_info in queries.items():
342
+ # Skip internal queries (like performance_schema_check)
343
+ if query_info.get('category') != 'internal':
344
+ descriptions[query_name] = query_info.get(
345
+ 'description', 'No description available'
346
+ )
347
+ return descriptions
348
+
349
+ def get_schema_queries(self) -> list[str]:
350
+ """Get list of schema-related query names."""
351
+ return self.get_queries_by_category('information_schema')
352
+
353
+ def get_performance_queries(self) -> list[str]:
354
+ """Get list of performance-related query names."""
355
+ return self.get_queries_by_category('performance_schema')