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
awslabs/__init__.py ADDED
@@ -0,0 +1,17 @@
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
+ # This file is part of the awslabs namespace.
16
+ # It is intentionally minimal to support PEP 420 namespace packages.
17
+ __path__ = __import__('pkgutil').extend_path(__path__, __name__)
@@ -0,0 +1,17 @@
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
+ """awslabs.dynamodb-mcp-server"""
16
+
17
+ __version__ = '2.0.10'
@@ -0,0 +1,19 @@
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
+ """CDK generator module for creating AWS CDK applications from DynamoDB data models."""
16
+
17
+ from awslabs.dynamodb_mcp_server.cdk_generator.generator import CdkGenerator, CdkGeneratorError
18
+
19
+ __all__ = ['CdkGenerator', 'CdkGeneratorError']
@@ -0,0 +1,276 @@
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
+ """CDK project generator for DynamoDB data models."""
16
+
17
+ import json
18
+ import re
19
+ import shutil
20
+ import subprocess # nosec B404 - used to invoke `cdk init` locally
21
+ from awslabs.dynamodb_mcp_server.cdk_generator.models import DataModel
22
+ from jinja2 import Environment, FileSystemLoader, TemplateNotFound
23
+ from loguru import logger
24
+ from pathlib import Path
25
+
26
+
27
+ # CDK Generation Configuration
28
+ CDK_INIT_TIMEOUT_SECONDS = 120
29
+ CDK_DIRECTORY_NAME = 'cdk'
30
+ STACK_TEMPLATE_NAME = 'stack.ts.j2'
31
+
32
+
33
+ class CdkGeneratorError(Exception):
34
+ """Exception raised for CDK generation errors."""
35
+
36
+ pass
37
+
38
+
39
+ class CdkGenerator:
40
+ """Generates CDK projects from DynamoDB data model JSON files."""
41
+
42
+ def __init__(self):
43
+ """Initialize the generator.
44
+
45
+ The templates directory is determined internally based on the module location.
46
+ """
47
+ self.templates_dir = Path(__file__).parent / 'templates'
48
+ self.jinja_env = Environment( # nosec B701 - Content is NOT HTML and NOT served
49
+ loader=FileSystemLoader(str(self.templates_dir)),
50
+ trim_blocks=True,
51
+ lstrip_blocks=True,
52
+ autoescape=False,
53
+ )
54
+ self.jinja_env.filters['to_camel_case'] = self._to_camel_case
55
+ self.jinja_env.filters['to_pascal_case'] = self._to_pascal_case
56
+
57
+ def generate(self, json_file_path: Path) -> None:
58
+ """Generate a CDK project from the given JSON file.
59
+
60
+ Args:
61
+ json_file_path: Path to dynamodb_data_model.json
62
+
63
+ Returns:
64
+ None - returns nothing on success
65
+
66
+ Raises:
67
+ CdkGeneratorError: If generation fails with descriptive error message
68
+ ValueError: If data model validation fails
69
+ """
70
+ logger.info(f'Starting CDK generation. json_file_path: {json_file_path}')
71
+
72
+ if not json_file_path.exists():
73
+ raise CdkGeneratorError(f"JSON file not found. json_file_path: '{json_file_path}'")
74
+
75
+ cdk_dir = json_file_path.parent / CDK_DIRECTORY_NAME
76
+ if cdk_dir.exists():
77
+ raise CdkGeneratorError(
78
+ f"CDK directory already exists. To generate again, remove or rename it and try again. cdk_dir: '{cdk_dir}'"
79
+ )
80
+
81
+ logger.info('Creating cdk directory')
82
+ cdk_dir.mkdir()
83
+
84
+ logger.info('Running cdk init')
85
+ self._run_cdk_init(cdk_dir)
86
+
87
+ logger.info('Parsing data model')
88
+ data_model = self._parse_data_model(json_file_path)
89
+
90
+ logger.info('Checking for table name collisions')
91
+ self._check_table_name_collisions(data_model)
92
+
93
+ logger.info('Rendering template')
94
+ self._render_template(data_model, cdk_dir)
95
+
96
+ logger.info('Copying README template')
97
+ self._copy_readme_template(cdk_dir)
98
+
99
+ logger.info(f'CDK project generated successfully. cdk_dir: {cdk_dir}')
100
+
101
+ def _run_cdk_init(self, target_dir: Path) -> None:
102
+ """Execute cdk init in the target directory.
103
+
104
+ Args:
105
+ target_dir: Directory where cdk init should be executed
106
+
107
+ Raises:
108
+ CdkGeneratorError: If cdk init fails
109
+ """
110
+ try:
111
+ subprocess.run( # nosec B603, B607 - user local env, hardcoded cmd, no shell, timeout
112
+ ['npx', 'cdk', 'init', 'app', '--language', 'typescript'],
113
+ cwd=target_dir,
114
+ capture_output=True,
115
+ text=True,
116
+ timeout=CDK_INIT_TIMEOUT_SECONDS,
117
+ check=True,
118
+ )
119
+
120
+ logger.info('cdk init completed successfully')
121
+
122
+ except subprocess.CalledProcessError as e:
123
+ raise CdkGeneratorError(
124
+ f'cdk init failed. exit_code: {e.returncode}, stderr: {e.stderr}'
125
+ ) from e
126
+ except subprocess.TimeoutExpired as e:
127
+ raise CdkGeneratorError(
128
+ f'cdk init timed out. timeout_seconds: {CDK_INIT_TIMEOUT_SECONDS}'
129
+ ) from e
130
+ except FileNotFoundError as e:
131
+ raise CdkGeneratorError(
132
+ 'npx command not found. Install Node.js and npm, then try again.'
133
+ ) from e
134
+ except Exception as e:
135
+ raise CdkGeneratorError(f'cdk init failed. error: {str(e)}') from e
136
+
137
+ def _parse_data_model(self, json_file_path: Path) -> DataModel:
138
+ """Parse the JSON file into a DataModel object with validation.
139
+
140
+ Args:
141
+ json_file_path: Path to dynamodb_data_model.json
142
+
143
+ Returns:
144
+ DataModel instance
145
+
146
+ Raises:
147
+ ValueError: If JSON is invalid or validation fails
148
+ """
149
+ try:
150
+ with open(json_file_path, 'r', encoding='utf-8') as f:
151
+ json_data = json.load(f)
152
+ except json.JSONDecodeError as e:
153
+ raise ValueError(
154
+ f'Invalid JSON. json_file_path: {json_file_path}, error: {str(e)}'
155
+ ) from e
156
+ except Exception as e:
157
+ raise ValueError(
158
+ f'Failed to read JSON file. json_file_path: {json_file_path}, error: {str(e)}'
159
+ ) from e
160
+
161
+ return DataModel.from_json(json_data)
162
+
163
+ def _to_camel_case(self, table_name: str) -> str:
164
+ """Convert table name to camelCase for variable names.
165
+
166
+ Args:
167
+ table_name: Original table name (e.g., 'UserProfiles', 'Product-Catalog', 'Analytics_Events')
168
+
169
+ Returns:
170
+ camelCase variable name (e.g., 'userProfiles', 'productCatalog', 'analyticsEvents')
171
+ """
172
+ name = table_name.replace('-', ' ').replace('_', ' ')
173
+
174
+ # Split camelCase/PascalCase words (e.g., 'UserProfiles' -> 'User Profiles')
175
+ # Insert space before uppercase letters that follow lowercase letters
176
+ name = re.sub(r'([a-z])([A-Z])', r'\1 \2', name)
177
+
178
+ words = name.split()
179
+
180
+ # First word lowercase, rest title case
181
+ parts = [words[0].lower()]
182
+ parts.extend(word.capitalize() for word in words[1:])
183
+ return ''.join(parts)
184
+
185
+ def _to_pascal_case(self, table_name: str) -> str:
186
+ """Convert table name to PascalCase for method names.
187
+
188
+ Args:
189
+ table_name: Original table name (e.g., 'UserProfiles', 'Product-Catalog', 'Analytics_Events')
190
+
191
+ Returns:
192
+ PascalCase name (e.g., 'UserProfiles', 'ProductCatalog', 'AnalyticsEvents')
193
+ """
194
+ camel = self._to_camel_case(table_name)
195
+ return camel[0].upper() + camel[1:] if camel else camel
196
+
197
+ def _check_table_name_collisions(self, data_model: DataModel) -> None:
198
+ """Check for table name collisions after sanitization.
199
+
200
+ Args:
201
+ data_model: Parsed data model
202
+
203
+ Raises:
204
+ CdkGeneratorError: If two tables would produce the same variable name
205
+ """
206
+ seen: dict[str, str] = {} # camelCase_name -> original_name
207
+ for table in data_model.tables:
208
+ camel_case = self._to_camel_case(table.table_name)
209
+ if camel_case in seen:
210
+ raise CdkGeneratorError(
211
+ f'Table name collision detected. Rename one of the tables to fix. '
212
+ f"table1: '{seen[camel_case]}', table2: '{table.table_name}', camelCase_name: '{camel_case}'"
213
+ )
214
+ seen[camel_case] = table.table_name
215
+
216
+ def _render_template(self, data_model: DataModel, target_dir: Path) -> None:
217
+ """Render the stack template and write to target directory.
218
+
219
+ Stack filename and class name are derived from the CDK directory name.
220
+ For directory 'cdk', generates 'cdk-stack.ts' with class 'CdkStack'.
221
+
222
+ Args:
223
+ data_model: Parsed data model
224
+ target_dir: Directory where rendered file should be written (the CDK project root)
225
+
226
+ Raises:
227
+ CdkGeneratorError: If template rendering fails
228
+ """
229
+ # Derive stack info from CDK directory name
230
+ dir_name = target_dir.name
231
+ stack_class_name = ''.join(word.capitalize() for word in dir_name.split('-')) + 'Stack'
232
+ stack_filename = f'{dir_name}-stack.ts'
233
+
234
+ try:
235
+ template = self.jinja_env.get_template(STACK_TEMPLATE_NAME)
236
+ except TemplateNotFound as e:
237
+ raise CdkGeneratorError(
238
+ f"Required template file is missing. template_name: '{STACK_TEMPLATE_NAME}'"
239
+ ) from e
240
+
241
+ try:
242
+ rendered_content = template.render(
243
+ data_model=data_model, stack_class_name=stack_class_name
244
+ )
245
+ output_path = target_dir / 'lib' / stack_filename
246
+ output_path.parent.mkdir(parents=True, exist_ok=True)
247
+
248
+ output_path.write_text(rendered_content, encoding='utf-8')
249
+ logger.info(
250
+ f'Rendered template. template_name: {STACK_TEMPLATE_NAME}, output_path: {output_path}'
251
+ )
252
+ except Exception as e:
253
+ raise CdkGeneratorError(f'Failed to render template. error: {str(e)}') from e
254
+
255
+ def _copy_readme_template(self, target_dir: Path) -> None:
256
+ """Copy the README template to the target directory.
257
+
258
+ Args:
259
+ target_dir: Directory where README.md should be copied
260
+
261
+ Raises:
262
+ CdkGeneratorError: If copy fails
263
+
264
+ Note:
265
+ The README template is part of the package and should always exist.
266
+ If it's missing, that indicates a packaging issue.
267
+ """
268
+ readme_template = self.templates_dir / 'README.md'
269
+ readme_dest = target_dir / 'README.md'
270
+
271
+ try:
272
+ shutil.copy2(readme_template, readme_dest)
273
+ except Exception as e:
274
+ raise CdkGeneratorError(
275
+ f"README template copy failed. readme_template: '{readme_template}', readme_dest: '{readme_dest}', error: {str(e)}"
276
+ ) from e