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
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
|