awslabs.terraform-mcp-server 0.0.1__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.

Potentially problematic release.


This version of awslabs.terraform-mcp-server might be problematic. Click here for more details.

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