awslabs.terraform-mcp-server 0.0.6__tar.gz → 0.0.7__tar.gz
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.
- {awslabs_terraform_mcp_server-0.0.6 → awslabs_terraform_mcp_server-0.0.7}/PKG-INFO +7 -1
- {awslabs_terraform_mcp_server-0.0.6 → awslabs_terraform_mcp_server-0.0.7}/README.md +6 -0
- {awslabs_terraform_mcp_server-0.0.6 → awslabs_terraform_mcp_server-0.0.7}/awslabs/terraform_mcp_server/impl/tools/__init__.py +2 -0
- awslabs_terraform_mcp_server-0.0.7/awslabs/terraform_mcp_server/impl/tools/search_user_provided_module.py +346 -0
- {awslabs_terraform_mcp_server-0.0.6 → awslabs_terraform_mcp_server-0.0.7}/awslabs/terraform_mcp_server/models/__init__.py +4 -0
- {awslabs_terraform_mcp_server-0.0.6 → awslabs_terraform_mcp_server-0.0.7}/awslabs/terraform_mcp_server/models/models.py +44 -0
- {awslabs_terraform_mcp_server-0.0.6 → awslabs_terraform_mcp_server-0.0.7}/awslabs/terraform_mcp_server/server.py +52 -1
- {awslabs_terraform_mcp_server-0.0.6 → awslabs_terraform_mcp_server-0.0.7}/awslabs/terraform_mcp_server/static/MCP_INSTRUCTIONS.md +7 -0
- {awslabs_terraform_mcp_server-0.0.6 → awslabs_terraform_mcp_server-0.0.7}/awslabs/terraform_mcp_server/static/TERRAFORM_WORKFLOW_GUIDE.md +6 -0
- {awslabs_terraform_mcp_server-0.0.6 → awslabs_terraform_mcp_server-0.0.7}/pyproject.toml +1 -1
- {awslabs_terraform_mcp_server-0.0.6 → awslabs_terraform_mcp_server-0.0.7}/tests/README.md +16 -0
- awslabs_terraform_mcp_server-0.0.7/tests/test_search_user_provided_module.py +965 -0
- {awslabs_terraform_mcp_server-0.0.6 → awslabs_terraform_mcp_server-0.0.7}/tests/test_server.py +292 -0
- {awslabs_terraform_mcp_server-0.0.6 → awslabs_terraform_mcp_server-0.0.7}/uv.lock +1 -1
- {awslabs_terraform_mcp_server-0.0.6 → awslabs_terraform_mcp_server-0.0.7}/.gitignore +0 -0
- {awslabs_terraform_mcp_server-0.0.6 → awslabs_terraform_mcp_server-0.0.7}/.pre-commit-config.yaml +0 -0
- {awslabs_terraform_mcp_server-0.0.6 → awslabs_terraform_mcp_server-0.0.7}/.python-version +0 -0
- {awslabs_terraform_mcp_server-0.0.6 → awslabs_terraform_mcp_server-0.0.7}/CHANGELOG.md +0 -0
- {awslabs_terraform_mcp_server-0.0.6 → awslabs_terraform_mcp_server-0.0.7}/awslabs/__init__.py +0 -0
- {awslabs_terraform_mcp_server-0.0.6 → awslabs_terraform_mcp_server-0.0.7}/awslabs/terraform_mcp_server/__init__.py +0 -0
- {awslabs_terraform_mcp_server-0.0.6 → awslabs_terraform_mcp_server-0.0.7}/awslabs/terraform_mcp_server/impl/resources/__init__.py +0 -0
- {awslabs_terraform_mcp_server-0.0.6 → awslabs_terraform_mcp_server-0.0.7}/awslabs/terraform_mcp_server/impl/resources/terraform_aws_provider_resources_listing.py +0 -0
- {awslabs_terraform_mcp_server-0.0.6 → awslabs_terraform_mcp_server-0.0.7}/awslabs/terraform_mcp_server/impl/resources/terraform_awscc_provider_resources_listing.py +0 -0
- {awslabs_terraform_mcp_server-0.0.6 → awslabs_terraform_mcp_server-0.0.7}/awslabs/terraform_mcp_server/impl/tools/execute_terraform_command.py +0 -0
- {awslabs_terraform_mcp_server-0.0.6 → awslabs_terraform_mcp_server-0.0.7}/awslabs/terraform_mcp_server/impl/tools/run_checkov_scan.py +0 -0
- {awslabs_terraform_mcp_server-0.0.6 → awslabs_terraform_mcp_server-0.0.7}/awslabs/terraform_mcp_server/impl/tools/search_aws_provider_docs.py +0 -0
- {awslabs_terraform_mcp_server-0.0.6 → awslabs_terraform_mcp_server-0.0.7}/awslabs/terraform_mcp_server/impl/tools/search_awscc_provider_docs.py +0 -0
- {awslabs_terraform_mcp_server-0.0.6 → awslabs_terraform_mcp_server-0.0.7}/awslabs/terraform_mcp_server/impl/tools/search_specific_aws_ia_modules.py +0 -0
- {awslabs_terraform_mcp_server-0.0.6 → awslabs_terraform_mcp_server-0.0.7}/awslabs/terraform_mcp_server/impl/tools/utils.py +0 -0
- {awslabs_terraform_mcp_server-0.0.6 → awslabs_terraform_mcp_server-0.0.7}/awslabs/terraform_mcp_server/scripts/generate_aws_provider_resources.py +0 -0
- {awslabs_terraform_mcp_server-0.0.6 → awslabs_terraform_mcp_server-0.0.7}/awslabs/terraform_mcp_server/scripts/generate_awscc_provider_resources.py +0 -0
- {awslabs_terraform_mcp_server-0.0.6 → awslabs_terraform_mcp_server-0.0.7}/awslabs/terraform_mcp_server/scripts/scrape_aws_terraform_best_practices.py +0 -0
- {awslabs_terraform_mcp_server-0.0.6 → awslabs_terraform_mcp_server-0.0.7}/awslabs/terraform_mcp_server/static/AWSCC_PROVIDER_RESOURCES.md +0 -0
- {awslabs_terraform_mcp_server-0.0.6 → awslabs_terraform_mcp_server-0.0.7}/awslabs/terraform_mcp_server/static/AWS_PROVIDER_RESOURCES.md +0 -0
- {awslabs_terraform_mcp_server-0.0.6 → awslabs_terraform_mcp_server-0.0.7}/awslabs/terraform_mcp_server/static/AWS_TERRAFORM_BEST_PRACTICES.md +0 -0
- {awslabs_terraform_mcp_server-0.0.6 → awslabs_terraform_mcp_server-0.0.7}/awslabs/terraform_mcp_server/static/__init__.py +0 -0
- {awslabs_terraform_mcp_server-0.0.6 → awslabs_terraform_mcp_server-0.0.7}/run_tests.sh +0 -0
- {awslabs_terraform_mcp_server-0.0.6 → awslabs_terraform_mcp_server-0.0.7}/tests/.gitignore +0 -0
- {awslabs_terraform_mcp_server-0.0.6 → awslabs_terraform_mcp_server-0.0.7}/tests/__init__.py +0 -0
- {awslabs_terraform_mcp_server-0.0.6 → awslabs_terraform_mcp_server-0.0.7}/tests/conftest.py +0 -0
- {awslabs_terraform_mcp_server-0.0.6 → awslabs_terraform_mcp_server-0.0.7}/tests/test_command_impl.py +0 -0
- {awslabs_terraform_mcp_server-0.0.6 → awslabs_terraform_mcp_server-0.0.7}/tests/test_execute_terraform_command.py +0 -0
- {awslabs_terraform_mcp_server-0.0.6 → awslabs_terraform_mcp_server-0.0.7}/tests/test_models.py +0 -0
- {awslabs_terraform_mcp_server-0.0.6 → awslabs_terraform_mcp_server-0.0.7}/tests/test_parameter_annotations.py +0 -0
- {awslabs_terraform_mcp_server-0.0.6 → awslabs_terraform_mcp_server-0.0.7}/tests/test_resources.py +0 -0
- {awslabs_terraform_mcp_server-0.0.6 → awslabs_terraform_mcp_server-0.0.7}/tests/test_run_checkov_scan.py +0 -0
- {awslabs_terraform_mcp_server-0.0.6 → awslabs_terraform_mcp_server-0.0.7}/tests/test_tool_implementations.py +0 -0
- {awslabs_terraform_mcp_server-0.0.6 → awslabs_terraform_mcp_server-0.0.7}/tests/test_utils.py +0 -0
- {awslabs_terraform_mcp_server-0.0.6 → awslabs_terraform_mcp_server-0.0.7}/tests/test_utils_additional.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: awslabs.terraform-mcp-server
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.7
|
|
4
4
|
Summary: An AWS Labs Model Context Protocol (MCP) server for terraform
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Requires-Dist: beautifulsoup4>=4.12.0
|
|
@@ -45,6 +45,12 @@ MCP server for Terraform on AWS best practices, infrastructure as code patterns,
|
|
|
45
45
|
- SageMaker endpoint deployment for ML model hosting
|
|
46
46
|
- Serverless Streamlit application deployment for AI interfaces
|
|
47
47
|
|
|
48
|
+
- **Terraform Registry Module Analysis** - Analyze Terraform Registry modules
|
|
49
|
+
- Search for modules by URL or identifier
|
|
50
|
+
- Extract input variables, output variables, and README content
|
|
51
|
+
- Understand module usage and configuration options
|
|
52
|
+
- Analyze module structure and dependencies
|
|
53
|
+
|
|
48
54
|
- **Terraform Workflow Execution** - Run Terraform commands directly
|
|
49
55
|
- Initialize, plan, validate, apply, and destroy operations
|
|
50
56
|
- Pass variables and specify AWS regions
|
|
@@ -30,6 +30,12 @@ MCP server for Terraform on AWS best practices, infrastructure as code patterns,
|
|
|
30
30
|
- SageMaker endpoint deployment for ML model hosting
|
|
31
31
|
- Serverless Streamlit application deployment for AI interfaces
|
|
32
32
|
|
|
33
|
+
- **Terraform Registry Module Analysis** - Analyze Terraform Registry modules
|
|
34
|
+
- Search for modules by URL or identifier
|
|
35
|
+
- Extract input variables, output variables, and README content
|
|
36
|
+
- Understand module usage and configuration options
|
|
37
|
+
- Analyze module structure and dependencies
|
|
38
|
+
|
|
33
39
|
- **Terraform Workflow Execution** - Run Terraform commands directly
|
|
34
40
|
- Initialize, plan, validate, apply, and destroy operations
|
|
35
41
|
- Pass variables and specify AWS regions
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Tool implementations for the terraform MCP server."""
|
|
2
2
|
|
|
3
|
+
from .search_user_provided_module import search_user_provided_module_impl
|
|
3
4
|
from .execute_terraform_command import execute_terraform_command_impl
|
|
4
5
|
from .search_aws_provider_docs import search_aws_provider_docs_impl
|
|
5
6
|
from .search_awscc_provider_docs import search_awscc_provider_docs_impl
|
|
@@ -7,6 +8,7 @@ from .search_specific_aws_ia_modules import search_specific_aws_ia_modules_impl
|
|
|
7
8
|
from .run_checkov_scan import run_checkov_scan_impl
|
|
8
9
|
|
|
9
10
|
__all__ = [
|
|
11
|
+
'search_user_provided_module_impl',
|
|
10
12
|
'execute_terraform_command_impl',
|
|
11
13
|
'search_aws_provider_docs_impl',
|
|
12
14
|
'search_awscc_provider_docs_impl',
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance
|
|
4
|
+
# with the License. A copy of the License is located at
|
|
5
|
+
#
|
|
6
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
7
|
+
#
|
|
8
|
+
# or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES
|
|
9
|
+
# OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions
|
|
10
|
+
# and limitations under the License.
|
|
11
|
+
"""Implementation of user provided module from the Terraform registry search tool."""
|
|
12
|
+
|
|
13
|
+
import re
|
|
14
|
+
import requests
|
|
15
|
+
import traceback
|
|
16
|
+
from awslabs.terraform_mcp_server.impl.tools.utils import (
|
|
17
|
+
clean_description,
|
|
18
|
+
extract_outputs_from_readme,
|
|
19
|
+
get_github_release_details,
|
|
20
|
+
get_variables_tf,
|
|
21
|
+
)
|
|
22
|
+
from awslabs.terraform_mcp_server.models import (
|
|
23
|
+
SearchUserProvidedModuleRequest,
|
|
24
|
+
SearchUserProvidedModuleResult,
|
|
25
|
+
TerraformOutput,
|
|
26
|
+
TerraformVariable,
|
|
27
|
+
)
|
|
28
|
+
from loguru import logger
|
|
29
|
+
from typing import Any, Dict, Optional, Tuple
|
|
30
|
+
from urllib.parse import urlparse
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
async def search_user_provided_module_impl(
|
|
34
|
+
request: SearchUserProvidedModuleRequest,
|
|
35
|
+
) -> SearchUserProvidedModuleResult:
|
|
36
|
+
"""Analyze a Terraform module from the registry.
|
|
37
|
+
|
|
38
|
+
This tool takes a Terraform registry module URL and analyzes its input variables,
|
|
39
|
+
output variables, README, and other details to provide comprehensive information
|
|
40
|
+
about the module.
|
|
41
|
+
|
|
42
|
+
Parameters:
|
|
43
|
+
request: Details about the Terraform module to analyze
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
A SearchUserProvidedModuleResult object containing module information
|
|
47
|
+
"""
|
|
48
|
+
logger.info(f'Analyzing Terraform module: {request.module_url}')
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
# Parse the module URL to extract namespace, name, and provider
|
|
52
|
+
module_parts = parse_module_url(request.module_url)
|
|
53
|
+
if not module_parts:
|
|
54
|
+
return SearchUserProvidedModuleResult(
|
|
55
|
+
status='error',
|
|
56
|
+
module_name='unknown',
|
|
57
|
+
module_url=request.module_url,
|
|
58
|
+
module_version='unknown',
|
|
59
|
+
module_description='',
|
|
60
|
+
variables=[],
|
|
61
|
+
outputs=[],
|
|
62
|
+
readme_content=None,
|
|
63
|
+
error_message=f'Invalid module URL format: {request.module_url}. Expected format: [namespace]/[name]/[provider] or registry.terraform.io/[namespace]/[name]/[provider]',
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
namespace, name, provider = module_parts
|
|
67
|
+
|
|
68
|
+
# Fetch module details from Terraform Registry
|
|
69
|
+
module_details = await get_module_details(namespace, name, provider, request.version)
|
|
70
|
+
if not module_details:
|
|
71
|
+
return SearchUserProvidedModuleResult(
|
|
72
|
+
status='error',
|
|
73
|
+
module_name=name,
|
|
74
|
+
module_url=request.module_url,
|
|
75
|
+
module_version=request.version or 'latest',
|
|
76
|
+
module_description='',
|
|
77
|
+
variables=[],
|
|
78
|
+
outputs=[],
|
|
79
|
+
readme_content=None,
|
|
80
|
+
error_message=f'Failed to fetch module details from Terraform Registry: {request.module_url}',
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# Extract module information
|
|
84
|
+
module_version = module_details.get('version', request.version or 'latest')
|
|
85
|
+
module_description = clean_description(module_details.get('description', ''))
|
|
86
|
+
readme_content = module_details.get('readme_content', '')
|
|
87
|
+
|
|
88
|
+
# Get variables and outputs
|
|
89
|
+
variables = []
|
|
90
|
+
outputs = []
|
|
91
|
+
|
|
92
|
+
# Extract variables from module details
|
|
93
|
+
if 'variables' in module_details and module_details['variables']:
|
|
94
|
+
variables = [TerraformVariable(**var_data) for var_data in module_details['variables']]
|
|
95
|
+
elif 'root' in module_details and 'inputs' in module_details['root']:
|
|
96
|
+
# Extract from registry API format
|
|
97
|
+
for var_name, var_data in module_details['root']['inputs'].items():
|
|
98
|
+
variables.append(
|
|
99
|
+
TerraformVariable(
|
|
100
|
+
name=var_name,
|
|
101
|
+
type=var_data.get('type', ''),
|
|
102
|
+
description=var_data.get('description', ''),
|
|
103
|
+
default=var_data.get('default'),
|
|
104
|
+
required=var_data.get('required', True),
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# Extract outputs from module details
|
|
109
|
+
if 'outputs' in module_details and module_details['outputs']:
|
|
110
|
+
outputs = [
|
|
111
|
+
TerraformOutput(name=output['name'], description=output.get('description', ''))
|
|
112
|
+
for output in module_details['outputs']
|
|
113
|
+
]
|
|
114
|
+
elif 'root' in module_details and 'outputs' in module_details['root']:
|
|
115
|
+
# Extract from registry API format
|
|
116
|
+
for output_name, output_data in module_details['root']['outputs'].items():
|
|
117
|
+
outputs.append(
|
|
118
|
+
TerraformOutput(
|
|
119
|
+
name=output_name,
|
|
120
|
+
description=output_data.get('description', ''),
|
|
121
|
+
)
|
|
122
|
+
)
|
|
123
|
+
elif readme_content:
|
|
124
|
+
# Try to extract outputs from README
|
|
125
|
+
extracted_outputs = extract_outputs_from_readme(readme_content)
|
|
126
|
+
if extracted_outputs:
|
|
127
|
+
outputs = [
|
|
128
|
+
TerraformOutput(name=output['name'], description=output.get('description', ''))
|
|
129
|
+
for output in extracted_outputs
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
# Create the result
|
|
133
|
+
result = SearchUserProvidedModuleResult(
|
|
134
|
+
status='success',
|
|
135
|
+
module_name=name,
|
|
136
|
+
module_url=request.module_url,
|
|
137
|
+
module_version=module_version,
|
|
138
|
+
module_description=module_description,
|
|
139
|
+
variables=variables,
|
|
140
|
+
outputs=outputs,
|
|
141
|
+
readme_content=readme_content,
|
|
142
|
+
error_message=None,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
return result
|
|
146
|
+
|
|
147
|
+
except Exception as e:
|
|
148
|
+
logger.error(f'Error analyzing Terraform module: {e}')
|
|
149
|
+
logger.debug(f'Stack trace: {traceback.format_exc()}')
|
|
150
|
+
return SearchUserProvidedModuleResult(
|
|
151
|
+
status='error',
|
|
152
|
+
module_name=request.module_url.split('/')[-2]
|
|
153
|
+
if '/' in request.module_url
|
|
154
|
+
else 'unknown',
|
|
155
|
+
module_url=request.module_url,
|
|
156
|
+
module_version=request.version or 'latest',
|
|
157
|
+
module_description='',
|
|
158
|
+
variables=[],
|
|
159
|
+
outputs=[],
|
|
160
|
+
readme_content=None,
|
|
161
|
+
error_message=f'Error analyzing Terraform module: {str(e)}',
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def parse_module_url(module_url: str) -> Optional[Tuple[str, str, str]]:
|
|
166
|
+
"""Parse a Terraform module URL to extract namespace, name, and provider.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
module_url: The module URL or identifier (e.g., "hashicorp/consul/aws" or "registry.terraform.io/hashicorp/consul/aws")
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
Tuple containing (namespace, name, provider) or None if invalid format
|
|
173
|
+
"""
|
|
174
|
+
# First, handle registry.terraform.io URLs (with or without scheme)
|
|
175
|
+
parsed_url = None
|
|
176
|
+
|
|
177
|
+
# If URL has a scheme (http://, https://)
|
|
178
|
+
if '://' in module_url:
|
|
179
|
+
parsed_url = urlparse(module_url)
|
|
180
|
+
# For URLs without scheme, add a dummy scheme to enable proper URL parsing
|
|
181
|
+
else:
|
|
182
|
+
parsed_url = urlparse(f'https://{module_url}')
|
|
183
|
+
|
|
184
|
+
# Check if this is a registry.terraform.io URL
|
|
185
|
+
if parsed_url.netloc == 'registry.terraform.io':
|
|
186
|
+
# Extract path and remove leading slash
|
|
187
|
+
path = parsed_url.path.lstrip('/')
|
|
188
|
+
parts = path.split('/')
|
|
189
|
+
else:
|
|
190
|
+
# Simple module path format (namespace/name/provider)
|
|
191
|
+
parts = module_url.split('/')
|
|
192
|
+
|
|
193
|
+
# Ensure we have at least namespace/name/provider
|
|
194
|
+
if len(parts) < 3:
|
|
195
|
+
return None
|
|
196
|
+
|
|
197
|
+
namespace = parts[0]
|
|
198
|
+
name = parts[1]
|
|
199
|
+
provider = parts[2]
|
|
200
|
+
|
|
201
|
+
return namespace, name, provider
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
async def get_module_details(
|
|
205
|
+
namespace: str, name: str, provider: str, version: Optional[str] = None
|
|
206
|
+
) -> Dict[str, Any]:
|
|
207
|
+
"""Fetch detailed information about a Terraform module from the registry.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
namespace: The module namespace (e.g., hashicorp)
|
|
211
|
+
name: The module name (e.g., consul)
|
|
212
|
+
provider: The provider (e.g., aws)
|
|
213
|
+
version: Optional specific version to fetch
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
Dictionary containing module details
|
|
217
|
+
"""
|
|
218
|
+
logger.info(f'Fetching details for module {namespace}/{name}/{provider}')
|
|
219
|
+
|
|
220
|
+
try:
|
|
221
|
+
# Get basic module info via API
|
|
222
|
+
details_url = f'https://registry.terraform.io/v1/modules/{namespace}/{name}/{provider}'
|
|
223
|
+
if version:
|
|
224
|
+
details_url += f'/{version}'
|
|
225
|
+
|
|
226
|
+
logger.debug(f'Making API request to: {details_url}')
|
|
227
|
+
|
|
228
|
+
response = requests.get(details_url)
|
|
229
|
+
response.raise_for_status()
|
|
230
|
+
|
|
231
|
+
details = response.json()
|
|
232
|
+
logger.debug(
|
|
233
|
+
f'Received module details. Status code: {response.status_code}, Content size: {len(response.text)} bytes'
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
# Get the version
|
|
237
|
+
module_version = version or details.get('version', '')
|
|
238
|
+
if not module_version and 'latest' in details and 'version' in details['latest']:
|
|
239
|
+
module_version = details['latest']['version']
|
|
240
|
+
|
|
241
|
+
# Try to get README content and version details
|
|
242
|
+
readme_content = None
|
|
243
|
+
version_details = None
|
|
244
|
+
|
|
245
|
+
# APPROACH 1: Try to see if the registry API provides README content directly
|
|
246
|
+
logger.debug('Checking for README content in API response')
|
|
247
|
+
if 'readme' in details and details['readme']:
|
|
248
|
+
readme_content = details['readme']
|
|
249
|
+
logger.info(
|
|
250
|
+
f'Found README content directly in API response: {len(readme_content)} chars'
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
# APPROACH 2: Try using the GitHub repo URL for README content and version details
|
|
254
|
+
if 'source' in details:
|
|
255
|
+
source_url = details.get('source')
|
|
256
|
+
# Validate GitHub URL using regex
|
|
257
|
+
if isinstance(source_url, str) and re.match(r'https://github.com/', source_url):
|
|
258
|
+
logger.info(f'Found GitHub source URL: {source_url}')
|
|
259
|
+
|
|
260
|
+
# Extract GitHub owner and repo
|
|
261
|
+
github_parts = re.match(r'https://github.com/([^/]+)/([^/]+)', source_url)
|
|
262
|
+
if github_parts:
|
|
263
|
+
owner, repo = github_parts.groups()
|
|
264
|
+
logger.info(f'Extracted GitHub repo: {owner}/{repo}')
|
|
265
|
+
|
|
266
|
+
# Get version details from GitHub
|
|
267
|
+
github_version_info = await get_github_release_details(owner, repo)
|
|
268
|
+
version_details = github_version_info['details']
|
|
269
|
+
version_from_github = github_version_info['version']
|
|
270
|
+
|
|
271
|
+
if version_from_github:
|
|
272
|
+
logger.info(f'Found version from GitHub: {version_from_github}')
|
|
273
|
+
if not module_version:
|
|
274
|
+
module_version = version_from_github
|
|
275
|
+
|
|
276
|
+
# Get variables.tf content and parsed variables
|
|
277
|
+
variables_content, variables = await get_variables_tf(owner, repo, 'main')
|
|
278
|
+
if variables_content and variables:
|
|
279
|
+
logger.info(f'Found variables.tf with {len(variables)} variables')
|
|
280
|
+
details['variables_content'] = variables_content
|
|
281
|
+
details['variables'] = [var.dict() for var in variables]
|
|
282
|
+
else:
|
|
283
|
+
# Try master branch as fallback
|
|
284
|
+
variables_content, variables = await get_variables_tf(
|
|
285
|
+
owner, repo, 'master'
|
|
286
|
+
)
|
|
287
|
+
if variables_content and variables:
|
|
288
|
+
logger.info(
|
|
289
|
+
f'Found variables.tf in master branch with {len(variables)} variables'
|
|
290
|
+
)
|
|
291
|
+
details['variables_content'] = variables_content
|
|
292
|
+
details['variables'] = [var.dict() for var in variables]
|
|
293
|
+
|
|
294
|
+
# If README content not already found, try fetching it from GitHub
|
|
295
|
+
if not readme_content:
|
|
296
|
+
logger.debug(f'Fetching README from GitHub source: {source_url}')
|
|
297
|
+
|
|
298
|
+
# Try main branch first, then fall back to master if needed
|
|
299
|
+
for branch in ['main', 'master']:
|
|
300
|
+
raw_readme_url = f'https://raw.githubusercontent.com/{owner}/{repo}/{branch}/README.md'
|
|
301
|
+
logger.debug(f'Trying to fetch README from: {raw_readme_url}')
|
|
302
|
+
|
|
303
|
+
readme_response = requests.get(raw_readme_url)
|
|
304
|
+
if readme_response.status_code == 200:
|
|
305
|
+
readme_content = readme_response.text
|
|
306
|
+
logger.info(
|
|
307
|
+
f'Successfully fetched README from GitHub ({branch}): {len(readme_content)} chars'
|
|
308
|
+
)
|
|
309
|
+
break
|
|
310
|
+
|
|
311
|
+
# Add readme_content to details if available
|
|
312
|
+
if readme_content:
|
|
313
|
+
logger.info(f'Successfully extracted README content ({len(readme_content)} chars)')
|
|
314
|
+
|
|
315
|
+
# Extract outputs from README content
|
|
316
|
+
outputs = extract_outputs_from_readme(readme_content)
|
|
317
|
+
if outputs:
|
|
318
|
+
logger.info(f'Extracted {len(outputs)} outputs from README')
|
|
319
|
+
details['outputs'] = outputs
|
|
320
|
+
|
|
321
|
+
# Trim if too large
|
|
322
|
+
if len(readme_content) > 8000:
|
|
323
|
+
logger.debug(
|
|
324
|
+
f'README content exceeds 8000 characters ({len(readme_content)}), truncating...'
|
|
325
|
+
)
|
|
326
|
+
readme_content = readme_content[:8000] + '...\n[README truncated due to length]'
|
|
327
|
+
logger.debug('README content truncated')
|
|
328
|
+
|
|
329
|
+
details['readme_content'] = readme_content
|
|
330
|
+
else:
|
|
331
|
+
logger.warning('No README content found through any method')
|
|
332
|
+
|
|
333
|
+
# Add version details if available
|
|
334
|
+
if version_details:
|
|
335
|
+
logger.info('Adding version details to response')
|
|
336
|
+
details['version_details'] = version_details
|
|
337
|
+
|
|
338
|
+
# Add version to details
|
|
339
|
+
details['version'] = module_version
|
|
340
|
+
|
|
341
|
+
return details
|
|
342
|
+
|
|
343
|
+
except Exception as e:
|
|
344
|
+
logger.error(f'Error fetching module details: {e}')
|
|
345
|
+
logger.debug(f'Stack trace: {traceback.format_exc()}')
|
|
346
|
+
return {}
|
|
@@ -10,6 +10,8 @@ from .models import (
|
|
|
10
10
|
CheckovScanResult,
|
|
11
11
|
TerraformVariable,
|
|
12
12
|
TerraformOutput,
|
|
13
|
+
SearchUserProvidedModuleRequest,
|
|
14
|
+
SearchUserProvidedModuleResult,
|
|
13
15
|
)
|
|
14
16
|
|
|
15
17
|
__all__ = [
|
|
@@ -24,4 +26,6 @@ __all__ = [
|
|
|
24
26
|
'CheckovScanResult',
|
|
25
27
|
'TerraformVariable',
|
|
26
28
|
'TerraformOutput',
|
|
29
|
+
'SearchUserProvidedModuleRequest',
|
|
30
|
+
'SearchUserProvidedModuleResult',
|
|
27
31
|
]
|
|
@@ -258,3 +258,47 @@ class CheckovScanResult(BaseModel):
|
|
|
258
258
|
)
|
|
259
259
|
summary: Dict[str, Any] = Field({}, description='Summary of scan results')
|
|
260
260
|
raw_output: Optional[str] = Field(None, description='Raw output from Checkov')
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
class SearchUserProvidedModuleRequest(BaseModel):
|
|
264
|
+
"""Request model for searching user-provided Terraform modules.
|
|
265
|
+
|
|
266
|
+
Attributes:
|
|
267
|
+
module_url: URL of the Terraform module in the registry (e.g., 'hashicorp/consul/aws').
|
|
268
|
+
version: Optional specific version of the module to analyze.
|
|
269
|
+
variables: Optional dictionary of variables to use when analyzing the module.
|
|
270
|
+
"""
|
|
271
|
+
|
|
272
|
+
module_url: str = Field(
|
|
273
|
+
..., description='URL or identifier of the Terraform module (e.g., "hashicorp/consul/aws")'
|
|
274
|
+
)
|
|
275
|
+
version: Optional[str] = Field(None, description='Specific version of the module to analyze')
|
|
276
|
+
variables: Optional[Dict[str, Any]] = Field(
|
|
277
|
+
None, description='Variables to use when analyzing the module'
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
class SearchUserProvidedModuleResult(BaseModel):
|
|
282
|
+
"""Result model for searching user-provided Terraform modules.
|
|
283
|
+
|
|
284
|
+
Attributes:
|
|
285
|
+
status: Execution status (success/error).
|
|
286
|
+
module_name: Name of the analyzed module.
|
|
287
|
+
module_url: URL of the module in the registry.
|
|
288
|
+
module_version: Version of the module that was analyzed.
|
|
289
|
+
module_description: Description of the module.
|
|
290
|
+
variables: List of variables defined by the module.
|
|
291
|
+
outputs: List of outputs provided by the module.
|
|
292
|
+
readme_content: The README content of the module.
|
|
293
|
+
error_message: Optional error message if execution failed.
|
|
294
|
+
"""
|
|
295
|
+
|
|
296
|
+
status: Literal['success', 'error']
|
|
297
|
+
module_name: str
|
|
298
|
+
module_url: str
|
|
299
|
+
module_version: str
|
|
300
|
+
module_description: str
|
|
301
|
+
variables: List[TerraformVariable] = Field([], description='Variables defined by the module')
|
|
302
|
+
outputs: List[TerraformOutput] = Field([], description='Outputs provided by the module')
|
|
303
|
+
readme_content: Optional[str] = Field(None, description='README content of the module')
|
|
304
|
+
error_message: Optional[str] = Field(None, description='Error message if execution failed')
|
|
@@ -12,11 +12,14 @@ from awslabs.terraform_mcp_server.impl.tools import (
|
|
|
12
12
|
search_aws_provider_docs_impl,
|
|
13
13
|
search_awscc_provider_docs_impl,
|
|
14
14
|
search_specific_aws_ia_modules_impl,
|
|
15
|
+
search_user_provided_module_impl,
|
|
15
16
|
)
|
|
16
17
|
from awslabs.terraform_mcp_server.models import (
|
|
17
18
|
CheckovScanRequest,
|
|
18
19
|
CheckovScanResult,
|
|
19
20
|
ModuleSearchResult,
|
|
21
|
+
SearchUserProvidedModuleRequest,
|
|
22
|
+
SearchUserProvidedModuleResult,
|
|
20
23
|
TerraformAWSCCProviderDocsResult,
|
|
21
24
|
TerraformAWSProviderDocsResult,
|
|
22
25
|
TerraformExecutionRequest,
|
|
@@ -29,7 +32,7 @@ from awslabs.terraform_mcp_server.static import (
|
|
|
29
32
|
)
|
|
30
33
|
from mcp.server.fastmcp import FastMCP
|
|
31
34
|
from pydantic import Field
|
|
32
|
-
from typing import Dict, List, Literal, Optional
|
|
35
|
+
from typing import Any, Dict, List, Literal, Optional
|
|
33
36
|
|
|
34
37
|
|
|
35
38
|
mcp = FastMCP(
|
|
@@ -264,6 +267,54 @@ async def run_checkov_scan(
|
|
|
264
267
|
return await run_checkov_scan_impl(request)
|
|
265
268
|
|
|
266
269
|
|
|
270
|
+
@mcp.tool(name='SearchUserProvidedModule')
|
|
271
|
+
async def search_user_provided_module(
|
|
272
|
+
module_url: str = Field(
|
|
273
|
+
..., description='URL or identifier of the Terraform module (e.g., "hashicorp/consul/aws")'
|
|
274
|
+
),
|
|
275
|
+
version: Optional[str] = Field(None, description='Specific version of the module to analyze'),
|
|
276
|
+
variables: Optional[Dict[str, Any]] = Field(
|
|
277
|
+
None, description='Variables to use when analyzing the module'
|
|
278
|
+
),
|
|
279
|
+
) -> SearchUserProvidedModuleResult:
|
|
280
|
+
"""Search for a user-provided Terraform registry module and understand its inputs, outputs, and usage.
|
|
281
|
+
|
|
282
|
+
This tool takes a Terraform registry module URL and analyzes its input variables,
|
|
283
|
+
output variables, README, and other details to provide comprehensive information
|
|
284
|
+
about the module.
|
|
285
|
+
|
|
286
|
+
The module URL should be in the format "namespace/name/provider" (e.g., "hashicorp/consul/aws")
|
|
287
|
+
or "registry.terraform.io/namespace/name/provider".
|
|
288
|
+
|
|
289
|
+
Examples:
|
|
290
|
+
- To search for the HashiCorp Consul module:
|
|
291
|
+
search_user_provided_module(module_url='hashicorp/consul/aws')
|
|
292
|
+
|
|
293
|
+
- To search for a specific version of a module:
|
|
294
|
+
search_user_provided_module(module_url='terraform-aws-modules/vpc/aws', version='3.14.0')
|
|
295
|
+
|
|
296
|
+
- To search for a module with specific variables:
|
|
297
|
+
search_user_provided_module(
|
|
298
|
+
module_url='terraform-aws-modules/eks/aws',
|
|
299
|
+
variables={'cluster_name': 'my-cluster', 'vpc_id': 'vpc-12345'}
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
Parameters:
|
|
303
|
+
module_url: URL or identifier of the Terraform module (e.g., "hashicorp/consul/aws")
|
|
304
|
+
version: Optional specific version of the module to analyze
|
|
305
|
+
variables: Optional dictionary of variables to use when analyzing the module
|
|
306
|
+
|
|
307
|
+
Returns:
|
|
308
|
+
A SearchUserProvidedModuleResult object containing module information
|
|
309
|
+
"""
|
|
310
|
+
request = SearchUserProvidedModuleRequest(
|
|
311
|
+
module_url=module_url,
|
|
312
|
+
version=version,
|
|
313
|
+
variables=variables,
|
|
314
|
+
)
|
|
315
|
+
return await search_user_provided_module_impl(request)
|
|
316
|
+
|
|
317
|
+
|
|
267
318
|
# * Resources
|
|
268
319
|
@mcp.resource(
|
|
269
320
|
name='terraform_development_workflow',
|
|
@@ -70,6 +70,10 @@ When implementing specific AWS resources (only after confirming no suitable AWS-
|
|
|
70
70
|
3. `SearchSpecificAwsIaModules`
|
|
71
71
|
* Use for specialized AI/ML infrastructure needs
|
|
72
72
|
* Returns details for supported AWS-IA modules
|
|
73
|
+
4. `SearchUserProvidedModule`
|
|
74
|
+
* Analyze any Terraform Registry module by URL or identifier
|
|
75
|
+
* Extract input variables, output variables, and README content
|
|
76
|
+
* Understand module usage and configuration options
|
|
73
77
|
|
|
74
78
|
### Command Execution Tools
|
|
75
79
|
|
|
@@ -108,6 +112,9 @@ The AWSCC provider (Cloud Control API-based) offers:
|
|
|
108
112
|
- "Is this VPC configuration secure? Let's scan it with Checkov."
|
|
109
113
|
- "Find documentation for awscc_lambda_function to ensure we're using the preferred provider."
|
|
110
114
|
- "We need a Bedrock implementation for RAG. Let's search for AWS-IA modules that can help."
|
|
115
|
+
- "Use the terraform-aws-modules/vpc/aws module to implement a VPC"
|
|
116
|
+
- "Search for the hashicorp/consul/aws module and explain how to use it"
|
|
117
|
+
- "What variables are required for the terraform-aws-modules/eks/aws module?"
|
|
111
118
|
|
|
112
119
|
## Best Practices
|
|
113
120
|
|
|
@@ -83,6 +83,11 @@ flowchart TD
|
|
|
83
83
|
* FIRST check for specialized AWS-IA modules (`SearchSpecificAwsIaModules` tool)
|
|
84
84
|
* If no suitable module exists, THEN use AWSCC provider resources (`SearchAwsccProviderDocs` tool)
|
|
85
85
|
* ONLY fall back to traditional AWS provider (`SearchAwsProviderDocs` tool) when the above options don't meet requirements
|
|
86
|
+
- When a user provides a specific Terraform Registry module to use:
|
|
87
|
+
* Use the `SearchUserProvidedModule` tool to analyze the module
|
|
88
|
+
* Extract input variables, output variables, and README content
|
|
89
|
+
* Understand module usage and configuration options
|
|
90
|
+
* Provide guidance on how to use the module correctly
|
|
86
91
|
- MCP Resources and tools to consult:
|
|
87
92
|
- Resources
|
|
88
93
|
- *terraform_development_workflow* to consult this guide and to use it to ensure you're following the development workflow correctly
|
|
@@ -91,6 +96,7 @@ flowchart TD
|
|
|
91
96
|
- *terraform_aws_provider_resources_listing* for available AWS resources
|
|
92
97
|
- Tools
|
|
93
98
|
- *SearchSpecificAwsIaModules* tool to check for specialized AWS-IA modules first (Bedrock, OpenSearch Serverless, SageMaker, Streamlit)
|
|
99
|
+
- *SearchUserProvidedModule* tool to analyze any Terraform Registry module provided by the user
|
|
94
100
|
- *SearchAwsccProviderDocs* tool to look up specific Cloud Control API resources
|
|
95
101
|
- *SearchAwsProviderDocs* tool to look up specific resource documentation
|
|
96
102
|
2. Validate Code
|
|
@@ -12,6 +12,7 @@ The tests are organized as follows:
|
|
|
12
12
|
- `test_command_impl.py`: Tests for the Terraform command execution implementation
|
|
13
13
|
- `test_execute_terraform_command.py`: Dedicated tests for the execute_terraform_command implementation
|
|
14
14
|
- `test_run_checkov_scan.py`: Dedicated tests for the run_checkov_scan implementation
|
|
15
|
+
- `test_search_user_provided_module.py`: Dedicated tests for the search_user_provided_module implementation
|
|
15
16
|
- `test_resources.py`: Tests for the resource implementations
|
|
16
17
|
- `test_tool_implementations.py`: Tests for the tool implementations
|
|
17
18
|
- `test_utils.py` and `test_utils_additional.py`: Tests for utility functions
|
|
@@ -157,3 +158,18 @@ Dedicated tests for the run_checkov_scan implementation, including:
|
|
|
157
158
|
- Testing security checks for dangerous patterns
|
|
158
159
|
- Testing CLI output parsing
|
|
159
160
|
- Testing error handling and exception handling
|
|
161
|
+
|
|
162
|
+
### test_search_user_provided_module.py
|
|
163
|
+
|
|
164
|
+
Dedicated tests for the search_user_provided_module implementation, including:
|
|
165
|
+
|
|
166
|
+
- Testing the parse_module_url function for different URL formats
|
|
167
|
+
- Testing the get_module_details function with successful responses
|
|
168
|
+
- Testing the get_module_details function with error responses
|
|
169
|
+
- Testing the search_user_provided_module_impl function with successful responses
|
|
170
|
+
- Testing the search_user_provided_module_impl function with registry prefix in URL
|
|
171
|
+
- Testing the search_user_provided_module_impl function with invalid URL
|
|
172
|
+
- Testing the search_user_provided_module_impl function when module is not found
|
|
173
|
+
- Testing the search_user_provided_module_impl function when an exception occurs
|
|
174
|
+
- Testing the extraction of outputs from README when not available in module details
|
|
175
|
+
- Testing the format_json helper function for serializing objects
|