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,349 @@
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
+ """Implementation of user provided module from the Terraform registry search tool."""
15
+
16
+ import re
17
+ import requests
18
+ import traceback
19
+ from awslabs.terraform_mcp_server.impl.tools.utils import (
20
+ clean_description,
21
+ extract_outputs_from_readme,
22
+ get_github_release_details,
23
+ get_variables_tf,
24
+ )
25
+ from awslabs.terraform_mcp_server.models import (
26
+ SearchUserProvidedModuleRequest,
27
+ SearchUserProvidedModuleResult,
28
+ TerraformOutput,
29
+ TerraformVariable,
30
+ )
31
+ from loguru import logger
32
+ from typing import Any, Dict, Optional, Tuple
33
+ from urllib.parse import urlparse
34
+
35
+
36
+ async def search_user_provided_module_impl(
37
+ request: SearchUserProvidedModuleRequest,
38
+ ) -> SearchUserProvidedModuleResult:
39
+ """Analyze a Terraform module from the registry.
40
+
41
+ This tool takes a Terraform registry module URL and analyzes its input variables,
42
+ output variables, README, and other details to provide comprehensive information
43
+ about the module.
44
+
45
+ Parameters:
46
+ request: Details about the Terraform module to analyze
47
+
48
+ Returns:
49
+ A SearchUserProvidedModuleResult object containing module information
50
+ """
51
+ logger.info(f'Analyzing Terraform module: {request.module_url}')
52
+
53
+ try:
54
+ # Parse the module URL to extract namespace, name, and provider
55
+ module_parts = parse_module_url(request.module_url)
56
+ if not module_parts:
57
+ return SearchUserProvidedModuleResult(
58
+ status='error',
59
+ module_name='unknown',
60
+ module_url=request.module_url,
61
+ module_version='unknown',
62
+ module_description='',
63
+ variables=[],
64
+ outputs=[],
65
+ readme_content=None,
66
+ error_message=f'Invalid module URL format: {request.module_url}. Expected format: [namespace]/[name]/[provider] or registry.terraform.io/[namespace]/[name]/[provider]',
67
+ )
68
+
69
+ namespace, name, provider = module_parts
70
+
71
+ # Fetch module details from Terraform Registry
72
+ module_details = await get_module_details(namespace, name, provider, request.version)
73
+ if not module_details:
74
+ return SearchUserProvidedModuleResult(
75
+ status='error',
76
+ module_name=name,
77
+ module_url=request.module_url,
78
+ module_version=request.version or 'latest',
79
+ module_description='',
80
+ variables=[],
81
+ outputs=[],
82
+ readme_content=None,
83
+ error_message=f'Failed to fetch module details from Terraform Registry: {request.module_url}',
84
+ )
85
+
86
+ # Extract module information
87
+ module_version = module_details.get('version', request.version or 'latest')
88
+ module_description = clean_description(module_details.get('description', ''))
89
+ readme_content = module_details.get('readme_content', '')
90
+
91
+ # Get variables and outputs
92
+ variables = []
93
+ outputs = []
94
+
95
+ # Extract variables from module details
96
+ if 'variables' in module_details and module_details['variables']:
97
+ variables = [TerraformVariable(**var_data) for var_data in module_details['variables']]
98
+ elif 'root' in module_details and 'inputs' in module_details['root']:
99
+ # Extract from registry API format
100
+ for var_name, var_data in module_details['root']['inputs'].items():
101
+ variables.append(
102
+ TerraformVariable(
103
+ name=var_name,
104
+ type=var_data.get('type', ''),
105
+ description=var_data.get('description', ''),
106
+ default=var_data.get('default'),
107
+ required=var_data.get('required', True),
108
+ )
109
+ )
110
+
111
+ # Extract outputs from module details
112
+ if 'outputs' in module_details and module_details['outputs']:
113
+ outputs = [
114
+ TerraformOutput(name=output['name'], description=output.get('description', ''))
115
+ for output in module_details['outputs']
116
+ ]
117
+ elif 'root' in module_details and 'outputs' in module_details['root']:
118
+ # Extract from registry API format
119
+ for output_name, output_data in module_details['root']['outputs'].items():
120
+ outputs.append(
121
+ TerraformOutput(
122
+ name=output_name,
123
+ description=output_data.get('description', ''),
124
+ )
125
+ )
126
+ elif readme_content:
127
+ # Try to extract outputs from README
128
+ extracted_outputs = extract_outputs_from_readme(readme_content)
129
+ if extracted_outputs:
130
+ outputs = [
131
+ TerraformOutput(name=output['name'], description=output.get('description', ''))
132
+ for output in extracted_outputs
133
+ ]
134
+
135
+ # Create the result
136
+ result = SearchUserProvidedModuleResult(
137
+ status='success',
138
+ module_name=name,
139
+ module_url=request.module_url,
140
+ module_version=module_version,
141
+ module_description=module_description,
142
+ variables=variables,
143
+ outputs=outputs,
144
+ readme_content=readme_content,
145
+ error_message=None,
146
+ )
147
+
148
+ return result
149
+
150
+ except Exception as e:
151
+ logger.error(f'Error analyzing Terraform module: {e}')
152
+ logger.debug(f'Stack trace: {traceback.format_exc()}')
153
+ return SearchUserProvidedModuleResult(
154
+ status='error',
155
+ module_name=request.module_url.split('/')[-2]
156
+ if '/' in request.module_url
157
+ else 'unknown',
158
+ module_url=request.module_url,
159
+ module_version=request.version or 'latest',
160
+ module_description='',
161
+ variables=[],
162
+ outputs=[],
163
+ readme_content=None,
164
+ error_message=f'Error analyzing Terraform module: {str(e)}',
165
+ )
166
+
167
+
168
+ def parse_module_url(module_url: str) -> Optional[Tuple[str, str, str]]:
169
+ """Parse a Terraform module URL to extract namespace, name, and provider.
170
+
171
+ Args:
172
+ module_url: The module URL or identifier (e.g., "hashicorp/consul/aws" or "registry.terraform.io/hashicorp/consul/aws")
173
+
174
+ Returns:
175
+ Tuple containing (namespace, name, provider) or None if invalid format
176
+ """
177
+ # First, handle registry.terraform.io URLs (with or without scheme)
178
+ parsed_url = None
179
+
180
+ # If URL has a scheme (http://, https://)
181
+ if '://' in module_url:
182
+ parsed_url = urlparse(module_url)
183
+ # For URLs without scheme, add a dummy scheme to enable proper URL parsing
184
+ else:
185
+ parsed_url = urlparse(f'https://{module_url}')
186
+
187
+ # Check if this is a registry.terraform.io URL
188
+ if parsed_url.netloc == 'registry.terraform.io':
189
+ # Extract path and remove leading slash
190
+ path = parsed_url.path.lstrip('/')
191
+ parts = path.split('/')
192
+ else:
193
+ # Simple module path format (namespace/name/provider)
194
+ parts = module_url.split('/')
195
+
196
+ # Ensure we have at least namespace/name/provider
197
+ if len(parts) < 3:
198
+ return None
199
+
200
+ namespace = parts[0]
201
+ name = parts[1]
202
+ provider = parts[2]
203
+
204
+ return namespace, name, provider
205
+
206
+
207
+ async def get_module_details(
208
+ namespace: str, name: str, provider: str, version: Optional[str] = None
209
+ ) -> Dict[str, Any]:
210
+ """Fetch detailed information about a Terraform module from the registry.
211
+
212
+ Args:
213
+ namespace: The module namespace (e.g., hashicorp)
214
+ name: The module name (e.g., consul)
215
+ provider: The provider (e.g., aws)
216
+ version: Optional specific version to fetch
217
+
218
+ Returns:
219
+ Dictionary containing module details
220
+ """
221
+ logger.info(f'Fetching details for module {namespace}/{name}/{provider}')
222
+
223
+ try:
224
+ # Get basic module info via API
225
+ details_url = f'https://registry.terraform.io/v1/modules/{namespace}/{name}/{provider}'
226
+ if version:
227
+ details_url += f'/{version}'
228
+
229
+ logger.debug(f'Making API request to: {details_url}')
230
+
231
+ response = requests.get(details_url)
232
+ response.raise_for_status()
233
+
234
+ details = response.json()
235
+ logger.debug(
236
+ f'Received module details. Status code: {response.status_code}, Content size: {len(response.text)} bytes'
237
+ )
238
+
239
+ # Get the version
240
+ module_version = version or details.get('version', '')
241
+ if not module_version and 'latest' in details and 'version' in details['latest']:
242
+ module_version = details['latest']['version']
243
+
244
+ # Try to get README content and version details
245
+ readme_content = None
246
+ version_details = None
247
+
248
+ # APPROACH 1: Try to see if the registry API provides README content directly
249
+ logger.debug('Checking for README content in API response')
250
+ if 'readme' in details and details['readme']:
251
+ readme_content = details['readme']
252
+ logger.info(
253
+ f'Found README content directly in API response: {len(readme_content)} chars'
254
+ )
255
+
256
+ # APPROACH 2: Try using the GitHub repo URL for README content and version details
257
+ if 'source' in details:
258
+ source_url = details.get('source')
259
+ # Validate GitHub URL using regex
260
+ if isinstance(source_url, str) and re.match(r'https://github.com/', source_url):
261
+ logger.info(f'Found GitHub source URL: {source_url}')
262
+
263
+ # Extract GitHub owner and repo
264
+ github_parts = re.match(r'https://github.com/([^/]+)/([^/]+)', source_url)
265
+ if github_parts:
266
+ owner, repo = github_parts.groups()
267
+ logger.info(f'Extracted GitHub repo: {owner}/{repo}')
268
+
269
+ # Get version details from GitHub
270
+ github_version_info = await get_github_release_details(owner, repo)
271
+ version_details = github_version_info['details']
272
+ version_from_github = github_version_info['version']
273
+
274
+ if version_from_github:
275
+ logger.info(f'Found version from GitHub: {version_from_github}')
276
+ if not module_version:
277
+ module_version = version_from_github
278
+
279
+ # Get variables.tf content and parsed variables
280
+ variables_content, variables = await get_variables_tf(owner, repo, 'main')
281
+ if variables_content and variables:
282
+ logger.info(f'Found variables.tf with {len(variables)} variables')
283
+ details['variables_content'] = variables_content
284
+ details['variables'] = [var.dict() for var in variables]
285
+ else:
286
+ # Try master branch as fallback
287
+ variables_content, variables = await get_variables_tf(
288
+ owner, repo, 'master'
289
+ )
290
+ if variables_content and variables:
291
+ logger.info(
292
+ f'Found variables.tf in master branch with {len(variables)} variables'
293
+ )
294
+ details['variables_content'] = variables_content
295
+ details['variables'] = [var.dict() for var in variables]
296
+
297
+ # If README content not already found, try fetching it from GitHub
298
+ if not readme_content:
299
+ logger.debug(f'Fetching README from GitHub source: {source_url}')
300
+
301
+ # Try main branch first, then fall back to master if needed
302
+ for branch in ['main', 'master']:
303
+ raw_readme_url = f'https://raw.githubusercontent.com/{owner}/{repo}/{branch}/README.md'
304
+ logger.debug(f'Trying to fetch README from: {raw_readme_url}')
305
+
306
+ readme_response = requests.get(raw_readme_url)
307
+ if readme_response.status_code == 200:
308
+ readme_content = readme_response.text
309
+ logger.info(
310
+ f'Successfully fetched README from GitHub ({branch}): {len(readme_content)} chars'
311
+ )
312
+ break
313
+
314
+ # Add readme_content to details if available
315
+ if readme_content:
316
+ logger.info(f'Successfully extracted README content ({len(readme_content)} chars)')
317
+
318
+ # Extract outputs from README content
319
+ outputs = extract_outputs_from_readme(readme_content)
320
+ if outputs:
321
+ logger.info(f'Extracted {len(outputs)} outputs from README')
322
+ details['outputs'] = outputs
323
+
324
+ # Trim if too large
325
+ if len(readme_content) > 8000:
326
+ logger.debug(
327
+ f'README content exceeds 8000 characters ({len(readme_content)}), truncating...'
328
+ )
329
+ readme_content = readme_content[:8000] + '...\n[README truncated due to length]'
330
+ logger.debug('README content truncated')
331
+
332
+ details['readme_content'] = readme_content
333
+ else:
334
+ logger.warning('No README content found through any method')
335
+
336
+ # Add version details if available
337
+ if version_details:
338
+ logger.info('Adding version details to response')
339
+ details['version_details'] = version_details
340
+
341
+ # Add version to details
342
+ details['version'] = module_version
343
+
344
+ return details
345
+
346
+ except Exception as e:
347
+ logger.error(f'Error fetching module details: {e}')
348
+ logger.debug(f'Stack trace: {traceback.format_exc()}')
349
+ return {}