awslabs.terraform-mcp-server 1.0.14__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 (30) hide show
  1. awslabs/__init__.py +17 -0
  2. awslabs/terraform_mcp_server/__init__.py +17 -0
  3. awslabs/terraform_mcp_server/impl/resources/__init__.py +25 -0
  4. awslabs/terraform_mcp_server/impl/resources/terraform_aws_provider_resources_listing.py +66 -0
  5. awslabs/terraform_mcp_server/impl/resources/terraform_awscc_provider_resources_listing.py +69 -0
  6. awslabs/terraform_mcp_server/impl/tools/__init__.py +33 -0
  7. awslabs/terraform_mcp_server/impl/tools/execute_terraform_command.py +223 -0
  8. awslabs/terraform_mcp_server/impl/tools/execute_terragrunt_command.py +320 -0
  9. awslabs/terraform_mcp_server/impl/tools/run_checkov_scan.py +376 -0
  10. awslabs/terraform_mcp_server/impl/tools/search_aws_provider_docs.py +691 -0
  11. awslabs/terraform_mcp_server/impl/tools/search_awscc_provider_docs.py +641 -0
  12. awslabs/terraform_mcp_server/impl/tools/search_specific_aws_ia_modules.py +458 -0
  13. awslabs/terraform_mcp_server/impl/tools/search_user_provided_module.py +349 -0
  14. awslabs/terraform_mcp_server/impl/tools/utils.py +572 -0
  15. awslabs/terraform_mcp_server/models/__init__.py +49 -0
  16. awslabs/terraform_mcp_server/models/models.py +381 -0
  17. awslabs/terraform_mcp_server/scripts/generate_aws_provider_resources.py +1240 -0
  18. awslabs/terraform_mcp_server/scripts/generate_awscc_provider_resources.py +1039 -0
  19. awslabs/terraform_mcp_server/scripts/scrape_aws_terraform_best_practices.py +143 -0
  20. awslabs/terraform_mcp_server/server.py +440 -0
  21. awslabs/terraform_mcp_server/static/AWSCC_PROVIDER_RESOURCES.md +3125 -0
  22. awslabs/terraform_mcp_server/static/AWS_PROVIDER_RESOURCES.md +3833 -0
  23. awslabs/terraform_mcp_server/static/AWS_TERRAFORM_BEST_PRACTICES.md +2523 -0
  24. awslabs/terraform_mcp_server/static/MCP_INSTRUCTIONS.md +142 -0
  25. awslabs/terraform_mcp_server/static/TERRAFORM_WORKFLOW_GUIDE.md +330 -0
  26. awslabs/terraform_mcp_server/static/__init__.py +38 -0
  27. awslabs_terraform_mcp_server-1.0.14.dist-info/METADATA +166 -0
  28. awslabs_terraform_mcp_server-1.0.14.dist-info/RECORD +30 -0
  29. awslabs_terraform_mcp_server-1.0.14.dist-info/WHEEL +4 -0
  30. awslabs_terraform_mcp_server-1.0.14.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,458 @@
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
+ """Implementation of specific AWS-IA module search tool for four key modules."""
16
+
17
+ import asyncio
18
+ import re
19
+ import requests
20
+ import time
21
+ import traceback
22
+ from .utils import (
23
+ clean_description,
24
+ extract_outputs_from_readme,
25
+ get_github_release_details,
26
+ get_submodules,
27
+ get_variables_tf,
28
+ )
29
+ from awslabs.terraform_mcp_server.models import ModuleSearchResult, SubmoduleInfo
30
+ from loguru import logger
31
+ from typing import Dict, List, Optional
32
+
33
+
34
+ # Define the specific modules we want to check
35
+ SPECIFIC_MODULES = [
36
+ {'namespace': 'aws-ia', 'name': 'bedrock', 'provider': 'aws'},
37
+ {'namespace': 'aws-ia', 'name': 'opensearch-serverless', 'provider': 'aws'},
38
+ {'namespace': 'aws-ia', 'name': 'sagemaker-endpoint', 'provider': 'aws'},
39
+ {'namespace': 'aws-ia', 'name': 'serverless-streamlit-app', 'provider': 'aws'},
40
+ ]
41
+
42
+
43
+ async def get_module_details(namespace: str, name: str, provider: str = 'aws') -> Dict:
44
+ """Fetch detailed information about a specific Terraform module.
45
+
46
+ Args:
47
+ namespace: The module namespace (e.g., aws-ia)
48
+ name: The module name (e.g., vpc)
49
+ provider: The provider (default: aws)
50
+
51
+ Returns:
52
+ Dictionary containing module details including README content and submodules
53
+ """
54
+ logger.info(f'Fetching details for module {namespace}/{name}/{provider}')
55
+
56
+ try:
57
+ # Get basic module info via API
58
+ details_url = f'https://registry.terraform.io/v1/modules/{namespace}/{name}/{provider}'
59
+ logger.debug(f'Making API request to: {details_url}')
60
+
61
+ response = requests.get(details_url)
62
+ response.raise_for_status()
63
+
64
+ details = response.json()
65
+ logger.debug(
66
+ f'Received module details. Status code: {response.status_code}, Content size: {len(response.text)} bytes'
67
+ )
68
+
69
+ # Debug log the version info we initially have
70
+ initial_version = details.get('latest_version', 'unknown')
71
+ if 'latest' in details and 'version' in details['latest']:
72
+ initial_version = details['latest']['version']
73
+ logger.debug(f'Initial version from primary API: {initial_version}')
74
+
75
+ # Add additional API call to get the latest version if not in details
76
+ if 'latest' not in details or 'version' not in details.get('latest', {}):
77
+ versions_url = f'{details_url}/versions'
78
+ logger.debug(f'Making API request to get versions: {versions_url}')
79
+
80
+ versions_response = requests.get(versions_url)
81
+ logger.debug(f'Versions API response code: {versions_response.status_code}')
82
+
83
+ if versions_response.status_code == 200:
84
+ versions_data = versions_response.json()
85
+ logger.debug(
86
+ f'Received versions data with {len(versions_data.get("modules", []))} module versions'
87
+ )
88
+
89
+ if versions_data.get('modules') and len(versions_data['modules']) > 0:
90
+ latest_version = versions_data['modules'][0].get('version', '')
91
+ details['latest_version'] = latest_version
92
+ logger.debug(f'Updated latest version to: {latest_version}')
93
+ else:
94
+ logger.debug('No modules found in versions response')
95
+ else:
96
+ logger.debug(
97
+ f'Failed to fetch versions. Status code: {versions_response.status_code}'
98
+ )
99
+ else:
100
+ logger.debug('Latest version already available in primary API response')
101
+
102
+ # Try to get README content and version details, starting with direct API if available
103
+ readme_content = None
104
+ version_details = None
105
+ version_from_github = ''
106
+
107
+ # APPROACH 1: Try to see if the registry API provides README content directly
108
+ logger.debug('APPROACH 1: Checking for README content in API response')
109
+ if 'readme' in details and details['readme']:
110
+ readme_content = details['readme']
111
+ logger.info(
112
+ f'Found README content directly in API response: {len(readme_content)} chars'
113
+ )
114
+
115
+ # APPROACH 2: Try using the GitHub repo URL for README content and version details
116
+ if 'source' in details:
117
+ source_url = details.get('source')
118
+ # Properly validate GitHub URL using regex to ensure it's actually from github.com domain
119
+ if isinstance(source_url, str) and re.match(r'https://github.com/', source_url):
120
+ logger.info(f'Found GitHub source URL: {source_url}')
121
+
122
+ # Extract GitHub owner and repo
123
+ github_parts = re.match(r'https://github.com/([^/]+)/([^/]+)', source_url)
124
+ if github_parts:
125
+ owner, repo = github_parts.groups()
126
+ logger.info(f'Extracted GitHub repo: {owner}/{repo}')
127
+
128
+ # Get version details from GitHub
129
+ github_version_info = await get_github_release_details(owner, repo)
130
+ version_details = github_version_info['details']
131
+ version_from_github = github_version_info['version']
132
+
133
+ if version_from_github:
134
+ logger.info(f'Found version from GitHub: {version_from_github}')
135
+ details['latest_version'] = version_from_github
136
+
137
+ # Get variables.tf content and parsed variables
138
+ variables_content, variables = await get_variables_tf(owner, repo, 'main')
139
+ if variables_content and variables:
140
+ logger.info(f'Found variables.tf with {len(variables)} variables')
141
+ details['variables_content'] = variables_content
142
+ details['variables'] = [var.dict() for var in variables]
143
+ else:
144
+ # Try master branch as fallback if main didn't work
145
+ variables_content, variables = await get_variables_tf(
146
+ owner, repo, 'master'
147
+ )
148
+ if variables_content and variables:
149
+ logger.info(
150
+ f'Found variables.tf in master branch with {len(variables)} variables'
151
+ )
152
+ details['variables_content'] = variables_content
153
+ details['variables'] = [var.dict() for var in variables]
154
+
155
+ # If README content not already found, try fetching it from GitHub
156
+ if not readme_content:
157
+ logger.debug(
158
+ f'APPROACH 2: Fetching README from GitHub source: {source_url}'
159
+ )
160
+
161
+ # Convert HTTPS URL to raw content URL
162
+ try:
163
+ # Try main branch first, then fall back to master if needed
164
+ found_readme_branch = None
165
+ for branch in ['main', 'master']:
166
+ raw_readme_url = f'https://raw.githubusercontent.com/{owner}/{repo}/{branch}/README.md'
167
+ logger.debug(f'Trying to fetch README from: {raw_readme_url}')
168
+
169
+ readme_response = requests.get(raw_readme_url)
170
+ if readme_response.status_code == 200:
171
+ readme_content = readme_response.text
172
+ found_readme_branch = branch
173
+ logger.info(
174
+ f'Successfully fetched README from GitHub ({branch}): {len(readme_content)} chars'
175
+ )
176
+ break
177
+
178
+ # Look for submodules now that we have identified the main branch
179
+ if found_readme_branch:
180
+ logger.info(
181
+ f'Fetching submodules using {found_readme_branch} branch'
182
+ )
183
+ start_time = time.time()
184
+ submodules = await get_submodules(owner, repo, found_readme_branch)
185
+ if submodules:
186
+ logger.info(
187
+ f'Found {len(submodules)} submodules in {time.time() - start_time:.2f} seconds'
188
+ )
189
+ details['submodules'] = [
190
+ submodule.dict() for submodule in submodules
191
+ ]
192
+ else:
193
+ logger.info('No submodules found')
194
+ else:
195
+ # Try both main branches for submodules if readme wasn't found
196
+ for branch in ['main', 'master']:
197
+ logger.debug(f'Trying {branch} branch for submodules')
198
+ start_time = time.time()
199
+ submodules = await get_submodules(owner, repo, branch)
200
+ if submodules:
201
+ logger.info(
202
+ f'Found {len(submodules)} submodules in {branch} branch in {time.time() - start_time:.2f} seconds'
203
+ )
204
+ details['submodules'] = [
205
+ submodule.dict() for submodule in submodules
206
+ ]
207
+ break
208
+ except Exception as ex:
209
+ logger.error(f'Error fetching README from GitHub: {ex}')
210
+ logger.debug(f'Stack trace: {traceback.format_exc()}')
211
+
212
+ # Process content we've gathered
213
+
214
+ # Add readme_content to details if available
215
+ if readme_content:
216
+ logger.info(f'Successfully extracted README content ({len(readme_content)} chars)')
217
+ logger.debug(f'First 100 characters of README: {readme_content[:100]}...')
218
+
219
+ # Extract outputs from README content
220
+ outputs = extract_outputs_from_readme(readme_content)
221
+ if outputs:
222
+ logger.info(f'Extracted {len(outputs)} outputs from README')
223
+ details['outputs'] = outputs
224
+ else:
225
+ logger.info('No outputs found in README')
226
+
227
+ # Trim if too large
228
+ if len(readme_content) > 8000:
229
+ logger.debug(
230
+ f'README content exceeds 8000 characters ({len(readme_content)}), truncating...'
231
+ )
232
+ readme_content = readme_content[:8000] + '...\n[README truncated due to length]'
233
+ logger.debug('README content truncated')
234
+
235
+ details['readme_content'] = readme_content
236
+ else:
237
+ logger.warning('No README content found through any method')
238
+
239
+ # Add version details if available
240
+ if version_details:
241
+ logger.info('Adding version details to response')
242
+ logger.debug(f'Version details: {version_details}')
243
+ details['version_details'] = version_details
244
+
245
+ return details
246
+
247
+ except Exception as e:
248
+ logger.error(f'Error fetching module details: {e}')
249
+ # Add stack trace for debugging
250
+ logger.debug(f'Stack trace: {traceback.format_exc()}')
251
+ return {}
252
+
253
+
254
+ async def get_specific_module_info(module_info: Dict[str, str]) -> Optional[ModuleSearchResult]:
255
+ """Get detailed information about a specific module.
256
+
257
+ Args:
258
+ module_info: Dictionary with namespace, name, and provider of the module
259
+
260
+ Returns:
261
+ ModuleSearchResult object with module details or None if module not found
262
+ """
263
+ namespace = module_info['namespace']
264
+ name = module_info['name']
265
+ provider = module_info['provider']
266
+
267
+ try:
268
+ # First, check if the module exists
269
+ details_url = f'https://registry.terraform.io/v1/modules/{namespace}/{name}/{provider}'
270
+ response = requests.get(details_url)
271
+
272
+ if response.status_code != 200:
273
+ logger.warning(
274
+ f'Module {namespace}/{name}/{provider} not found (status code: {response.status_code})'
275
+ )
276
+ return None
277
+
278
+ module_data = response.json()
279
+
280
+ # Get the description and clean it
281
+ description = module_data.get('description', 'No description available')
282
+ cleaned_description = clean_description(description)
283
+
284
+ # Create the basic result
285
+ result = ModuleSearchResult(
286
+ name=name,
287
+ namespace=namespace,
288
+ provider=provider,
289
+ version=module_data.get('latest_version', 'unknown'),
290
+ url=f'https://registry.terraform.io/modules/{namespace}/{name}/{provider}',
291
+ description=cleaned_description,
292
+ )
293
+
294
+ # Get detailed information including README
295
+ details = await get_module_details(namespace, name, provider)
296
+
297
+ if details:
298
+ # Update the version if we got a better one from the details
299
+ if 'latest_version' in details:
300
+ result.version = details['latest_version']
301
+
302
+ # Add version details if available
303
+ if 'version_details' in details:
304
+ result.version_details = details['version_details']
305
+
306
+ # Get README content
307
+ if 'readme_content' in details and details['readme_content']:
308
+ result.readme_content = details['readme_content']
309
+
310
+ # Get input and output counts if available
311
+ if 'root' in details and 'inputs' in details['root']:
312
+ result.input_count = len(details['root']['inputs'])
313
+
314
+ if 'root' in details and 'outputs' in details['root']:
315
+ result.output_count = len(details['root']['outputs'])
316
+
317
+ # Add submodules if available
318
+ if 'submodules' in details and details['submodules']:
319
+ submodules = [
320
+ SubmoduleInfo(**submodule_data) for submodule_data in details['submodules']
321
+ ]
322
+ result.submodules = submodules
323
+
324
+ # Add variables information if available
325
+ if 'variables' in details and details['variables']:
326
+ from awslabs.terraform_mcp_server.models import TerraformVariable
327
+
328
+ variables = [TerraformVariable(**var_data) for var_data in details['variables']]
329
+ result.variables = variables
330
+
331
+ # Add variables.tf content if available
332
+ if 'variables_content' in details and details['variables_content']:
333
+ result.variables_content = details['variables_content']
334
+
335
+ # Add outputs from README if available
336
+ if 'outputs' in details and details['outputs']:
337
+ from awslabs.terraform_mcp_server.models import TerraformOutput
338
+
339
+ outputs = [
340
+ TerraformOutput(name=output['name'], description=output.get('description'))
341
+ for output in details['outputs']
342
+ ]
343
+ result.outputs = outputs
344
+ # Update output_count if not already set
345
+ if result.output_count is None:
346
+ result.output_count = len(outputs)
347
+
348
+ return result
349
+
350
+ except Exception as e:
351
+ logger.error(f'Error getting info for module {namespace}/{name}/{provider}: {e}')
352
+ return None
353
+
354
+
355
+ async def search_specific_aws_ia_modules_impl(query: str) -> List[ModuleSearchResult]:
356
+ """Search for specific AWS-IA Terraform modules.
357
+
358
+ This tool checks for information about four specific AWS-IA modules:
359
+ - aws-ia/bedrock/aws - Amazon Bedrock module for generative AI applications
360
+ - aws-ia/opensearch-serverless/aws - OpenSearch Serverless collection for vector search
361
+ - aws-ia/sagemaker-endpoint/aws - SageMaker endpoint deployment module
362
+ - aws-ia/serverless-streamlit-app/aws - Serverless Streamlit application deployment
363
+
364
+ It returns detailed information about these modules, including their README content,
365
+ variables.tf content, and submodules when available.
366
+
367
+ The search is performed across module names, descriptions, README content, and variable
368
+ definitions. This allows you to find modules based on their functionality or specific
369
+ configuration options.
370
+
371
+ The implementation fetches module information directly from the Terraform Registry API
372
+ and GitHub repositories to ensure the most up-to-date information. Results include
373
+ comprehensive details about each module's structure, configuration options, and usage examples.
374
+
375
+ Examples:
376
+ - To get information about all four modules:
377
+ search_specific_aws_ia_modules_impl(query='')
378
+
379
+ - To find modules related to Bedrock:
380
+ search_specific_aws_ia_modules_impl(query='bedrock')
381
+
382
+ - To find modules related to vector search:
383
+ search_specific_aws_ia_modules_impl(query='vector search')
384
+
385
+ - To find modules with specific configuration options:
386
+ search_specific_aws_ia_modules_impl(query='endpoint_name')
387
+
388
+ Parameters:
389
+ query: Optional search term to filter modules (empty returns all four modules)
390
+
391
+ Returns:
392
+ A list of matching modules with their details, including:
393
+ - Basic module information (name, namespace, version)
394
+ - Module documentation (README content)
395
+ - Input and output parameter counts
396
+ - Variables from variables.tf with descriptions and default values
397
+ - Submodules information
398
+ - Version details and release information
399
+ """
400
+ logger.info(f"Searching for specific AWS-IA modules with query: '{query}'")
401
+
402
+ tasks = []
403
+
404
+ # Create tasks for fetching module information
405
+ for module_info in SPECIFIC_MODULES:
406
+ tasks.append(get_specific_module_info(module_info))
407
+
408
+ # Run all tasks concurrently
409
+ module_results = await asyncio.gather(*tasks)
410
+
411
+ # Filter out None results (modules not found)
412
+ module_results = [result for result in module_results if result is not None]
413
+
414
+ # If query is provided, filter results
415
+ if query and query.strip():
416
+ query_terms = query.lower().split()
417
+ filtered_results = []
418
+
419
+ for result in module_results:
420
+ # Check if any query term is in the module name, description, readme, or variables
421
+ matches = False
422
+
423
+ # Build search text from module details and variables
424
+ search_text = (
425
+ f'{result.name} {result.description} {result.readme_content or ""}'.lower()
426
+ )
427
+
428
+ # Add variables information to search text if available
429
+ if result.variables:
430
+ for var in result.variables:
431
+ var_text = f'{var.name} {var.type or ""} {var.description or ""}'
432
+ search_text += f' {var_text.lower()}'
433
+
434
+ # Add variables.tf content to search text if available
435
+ if result.variables_content:
436
+ search_text += f' {result.variables_content.lower()}'
437
+
438
+ # Add outputs information to search text if available
439
+ if result.outputs:
440
+ for output in result.outputs:
441
+ output_text = f'{output.name} {output.description or ""}'
442
+ search_text += f' {output_text.lower()}'
443
+
444
+ for term in query_terms:
445
+ if term in search_text:
446
+ matches = True
447
+ break
448
+
449
+ if matches:
450
+ filtered_results.append(result)
451
+
452
+ logger.info(
453
+ f"Found {len(filtered_results)} modules matching query '{query}' out of {len(module_results)} total modules"
454
+ )
455
+ return filtered_results
456
+ else:
457
+ logger.info(f'Returning all {len(module_results)} specific modules (no query filter)')
458
+ return module_results