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.
- awslabs/__init__.py +17 -0
- awslabs/terraform_mcp_server/__init__.py +17 -0
- awslabs/terraform_mcp_server/impl/resources/__init__.py +25 -0
- awslabs/terraform_mcp_server/impl/resources/terraform_aws_provider_resources_listing.py +66 -0
- awslabs/terraform_mcp_server/impl/resources/terraform_awscc_provider_resources_listing.py +69 -0
- awslabs/terraform_mcp_server/impl/tools/__init__.py +33 -0
- awslabs/terraform_mcp_server/impl/tools/execute_terraform_command.py +223 -0
- awslabs/terraform_mcp_server/impl/tools/execute_terragrunt_command.py +320 -0
- awslabs/terraform_mcp_server/impl/tools/run_checkov_scan.py +376 -0
- awslabs/terraform_mcp_server/impl/tools/search_aws_provider_docs.py +691 -0
- awslabs/terraform_mcp_server/impl/tools/search_awscc_provider_docs.py +641 -0
- awslabs/terraform_mcp_server/impl/tools/search_specific_aws_ia_modules.py +458 -0
- awslabs/terraform_mcp_server/impl/tools/search_user_provided_module.py +349 -0
- awslabs/terraform_mcp_server/impl/tools/utils.py +572 -0
- awslabs/terraform_mcp_server/models/__init__.py +49 -0
- awslabs/terraform_mcp_server/models/models.py +381 -0
- awslabs/terraform_mcp_server/scripts/generate_aws_provider_resources.py +1240 -0
- awslabs/terraform_mcp_server/scripts/generate_awscc_provider_resources.py +1039 -0
- awslabs/terraform_mcp_server/scripts/scrape_aws_terraform_best_practices.py +143 -0
- awslabs/terraform_mcp_server/server.py +440 -0
- awslabs/terraform_mcp_server/static/AWSCC_PROVIDER_RESOURCES.md +3125 -0
- awslabs/terraform_mcp_server/static/AWS_PROVIDER_RESOURCES.md +3833 -0
- awslabs/terraform_mcp_server/static/AWS_TERRAFORM_BEST_PRACTICES.md +2523 -0
- awslabs/terraform_mcp_server/static/MCP_INSTRUCTIONS.md +142 -0
- awslabs/terraform_mcp_server/static/TERRAFORM_WORKFLOW_GUIDE.md +330 -0
- awslabs/terraform_mcp_server/static/__init__.py +38 -0
- awslabs_terraform_mcp_server-1.0.14.dist-info/METADATA +166 -0
- awslabs_terraform_mcp_server-1.0.14.dist-info/RECORD +30 -0
- awslabs_terraform_mcp_server-1.0.14.dist-info/WHEEL +4 -0
- 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 {}
|