awslabs.terraform-mcp-server 0.0.2__py3-none-any.whl → 0.0.7__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.
- awslabs/terraform_mcp_server/impl/tools/__init__.py +2 -0
- awslabs/terraform_mcp_server/impl/tools/search_user_provided_module.py +346 -0
- awslabs/terraform_mcp_server/models/__init__.py +4 -0
- awslabs/terraform_mcp_server/models/models.py +44 -0
- awslabs/terraform_mcp_server/server.py +52 -1
- awslabs/terraform_mcp_server/static/MCP_INSTRUCTIONS.md +7 -0
- awslabs/terraform_mcp_server/static/TERRAFORM_WORKFLOW_GUIDE.md +6 -0
- {awslabs_terraform_mcp_server-0.0.2.dist-info → awslabs_terraform_mcp_server-0.0.7.dist-info}/METADATA +7 -1
- {awslabs_terraform_mcp_server-0.0.2.dist-info → awslabs_terraform_mcp_server-0.0.7.dist-info}/RECORD +11 -14
- awslabs/terraform_mcp_server/tests/__init__.py +0 -1
- awslabs/terraform_mcp_server/tests/run_tests.sh +0 -35
- awslabs/terraform_mcp_server/tests/test_parameter_annotations.py +0 -207
- awslabs/terraform_mcp_server/tests/test_tool_implementations.py +0 -309
- {awslabs_terraform_mcp_server-0.0.2.dist-info → awslabs_terraform_mcp_server-0.0.7.dist-info}/WHEEL +0 -0
- {awslabs_terraform_mcp_server-0.0.2.dist-info → awslabs_terraform_mcp_server-0.0.7.dist-info}/entry_points.txt +0 -0
|
@@ -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
|
|
@@ -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
|
{awslabs_terraform_mcp_server-0.0.2.dist-info → awslabs_terraform_mcp_server-0.0.7.dist-info}/RECORD
RENAMED
|
@@ -1,32 +1,29 @@
|
|
|
1
1
|
awslabs/__init__.py,sha256=4zfFn3N0BkvQmMTAIvV_QAbKp6GWzrwaUN17YeRoChM,115
|
|
2
2
|
awslabs/terraform_mcp_server/__init__.py,sha256=a-zIkwClerA84_XGykBggO4w5kf8f85EapnWnbAH01c,58
|
|
3
|
-
awslabs/terraform_mcp_server/server.py,sha256=
|
|
3
|
+
awslabs/terraform_mcp_server/server.py,sha256=ZyX_w3oath8l2T3tH63EyH5J_l93LZGCxW2HCeL4t2s,15787
|
|
4
4
|
awslabs/terraform_mcp_server/impl/resources/__init__.py,sha256=bxqHGDtuwWq8w-21lT5GzOuxBqkmnUvW6cnSA36ve3A,388
|
|
5
5
|
awslabs/terraform_mcp_server/impl/resources/terraform_aws_provider_resources_listing.py,sha256=t_guGQ44yvip9u2cONN2cWc3fXk6HyQGPu5DDP5YEkI,1973
|
|
6
6
|
awslabs/terraform_mcp_server/impl/resources/terraform_awscc_provider_resources_listing.py,sha256=RoBSe-p5OyLYWNYa7aGx2f4NWyjap1wNrj8F4o8_PSo,2060
|
|
7
|
-
awslabs/terraform_mcp_server/impl/tools/__init__.py,sha256=
|
|
7
|
+
awslabs/terraform_mcp_server/impl/tools/__init__.py,sha256=4D7D6cYF98qj4lltiO5UOHNFRl8Gfg7Zv9n-wRlOj3o,715
|
|
8
8
|
awslabs/terraform_mcp_server/impl/tools/execute_terraform_command.py,sha256=_fPxC2wT23oWa9XaAoUEV1j5lK7Xpi7MqBAq85fLNdU,8507
|
|
9
9
|
awslabs/terraform_mcp_server/impl/tools/run_checkov_scan.py,sha256=oS6qgUPPti7lpa2VNsUwPbswiq2hgpe49fggVUI_PfM,13435
|
|
10
10
|
awslabs/terraform_mcp_server/impl/tools/search_aws_provider_docs.py,sha256=w7NX4oQwsCeVQPj8YZzvXvXcox8drzZimW3mRL8Kd84,29049
|
|
11
11
|
awslabs/terraform_mcp_server/impl/tools/search_awscc_provider_docs.py,sha256=fY8eRQ1j6oB4XyfAoUC6mTUXNkoFaY0vsmMhV6w0NR8,27297
|
|
12
12
|
awslabs/terraform_mcp_server/impl/tools/search_specific_aws_ia_modules.py,sha256=Kny5iharA1Wj-9tnhoPHntu4qM9n-2mC2dB6TXwhqfk,20148
|
|
13
|
+
awslabs/terraform_mcp_server/impl/tools/search_user_provided_module.py,sha256=6d_QHnDwpGicMsmQmpfs0hzTtf-XAVXZBNUsBnkfxtw,14491
|
|
13
14
|
awslabs/terraform_mcp_server/impl/tools/utils.py,sha256=GB1OuhYgrg00AzKfhgaUpQwbVBK5D56GQLcm60NHd1c,20500
|
|
14
|
-
awslabs/terraform_mcp_server/models/__init__.py,sha256
|
|
15
|
-
awslabs/terraform_mcp_server/models/models.py,sha256=
|
|
15
|
+
awslabs/terraform_mcp_server/models/__init__.py,sha256=-kCRp9lGMz0-SXv0XvXP48_dVCeMbQIJqkxLTqXlmhk,801
|
|
16
|
+
awslabs/terraform_mcp_server/models/models.py,sha256=oQutL78FdbXXQgekXjp8Dhotq-A-wBJLylQAHRMyNs0,12614
|
|
16
17
|
awslabs/terraform_mcp_server/scripts/generate_aws_provider_resources.py,sha256=v8t4UL8KwIoqVYz25Xd8wiJJ2cwKmHvxaF0_ZpUI0b4,56714
|
|
17
18
|
awslabs/terraform_mcp_server/scripts/generate_awscc_provider_resources.py,sha256=FN2QOYefeEcyy-JXl1v3hLEz2YSBNRJB_9i182DdWiA,48722
|
|
18
19
|
awslabs/terraform_mcp_server/scripts/scrape_aws_terraform_best_practices.py,sha256=t_eFbciBwZXdK6pNkkcp4X7VPR4vXyXkmE_YLhm6Xr4,3783
|
|
19
20
|
awslabs/terraform_mcp_server/static/AWSCC_PROVIDER_RESOURCES.md,sha256=I_vu3dWXzd9Pxcd7tkdxFQs97wtuunyoXdJCAkCUXnE,440270
|
|
20
21
|
awslabs/terraform_mcp_server/static/AWS_PROVIDER_RESOURCES.md,sha256=OMboscC0Ov6O3U3K1uqrzRzx18nq_5AsJzITc5-CA8Y,303030
|
|
21
22
|
awslabs/terraform_mcp_server/static/AWS_TERRAFORM_BEST_PRACTICES.md,sha256=cftJ9y2nZ0kMendoV6WBlQFsNw-QnGnmF6dR88eoYdA,87665
|
|
22
|
-
awslabs/terraform_mcp_server/static/MCP_INSTRUCTIONS.md,sha256=
|
|
23
|
-
awslabs/terraform_mcp_server/static/TERRAFORM_WORKFLOW_GUIDE.md,sha256=
|
|
23
|
+
awslabs/terraform_mcp_server/static/MCP_INSTRUCTIONS.md,sha256=P82tv9KJJxhoG5UFKaG16nQS5a0ud6Fcvg_BUypz-z0,6445
|
|
24
|
+
awslabs/terraform_mcp_server/static/TERRAFORM_WORKFLOW_GUIDE.md,sha256=1RrSF7NH0BNVnQTZFwPhuaWsxElmCcwvl07iLOnO2gw,9768
|
|
24
25
|
awslabs/terraform_mcp_server/static/__init__.py,sha256=J5JGKYybg48XyBi2hepC101RDNFBxSXZS5YGvj0tql8,549
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
awslabs_terraform_mcp_server-0.0.2.dist-info/METADATA,sha256=wNYoFnJY-jLzLtJuCwGHQ8kxrIUC9uEp8D4BvYWxQiY,4344
|
|
30
|
-
awslabs_terraform_mcp_server-0.0.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
31
|
-
awslabs_terraform_mcp_server-0.0.2.dist-info/entry_points.txt,sha256=jCTPQeUJ74jpIDcYwVHQCk-y0n0ehRFFerh_Qm4ZU1c,90
|
|
32
|
-
awslabs_terraform_mcp_server-0.0.2.dist-info/RECORD,,
|
|
26
|
+
awslabs_terraform_mcp_server-0.0.7.dist-info/METADATA,sha256=FelC966-qfJFl-RXXsRb-pNp8oHC1e9WUNqjtDIs4no,4633
|
|
27
|
+
awslabs_terraform_mcp_server-0.0.7.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
28
|
+
awslabs_terraform_mcp_server-0.0.7.dist-info/entry_points.txt,sha256=jCTPQeUJ74jpIDcYwVHQCk-y0n0ehRFFerh_Qm4ZU1c,90
|
|
29
|
+
awslabs_terraform_mcp_server-0.0.7.dist-info/RECORD,,
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
"""Test package for terraform_mcp_server."""
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# Script to run the Terraform MCP server tests
|
|
3
|
-
|
|
4
|
-
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
|
|
5
|
-
PROJECT_ROOT="$SCRIPT_DIR/../../.."
|
|
6
|
-
|
|
7
|
-
# Set PYTHONPATH to include the project root
|
|
8
|
-
export PYTHONPATH="$PROJECT_ROOT:$PYTHONPATH"
|
|
9
|
-
|
|
10
|
-
# Function to run a test module
|
|
11
|
-
run_test() {
|
|
12
|
-
echo "Running $1..."
|
|
13
|
-
cd "$PROJECT_ROOT"
|
|
14
|
-
python -m awslabs.terraform_mcp_server.tests.$1
|
|
15
|
-
echo "Test completed: $1"
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
# Get the test name from the first argument, default to all tests
|
|
19
|
-
TEST_NAME=$1
|
|
20
|
-
if [ -z "$TEST_NAME" ]; then
|
|
21
|
-
echo "=== Running All Tests ==="
|
|
22
|
-
run_test "test_parameter_annotations"
|
|
23
|
-
run_test "test_tool_implementations"
|
|
24
|
-
elif [ "$TEST_NAME" == "params" ]; then
|
|
25
|
-
run_test "test_parameter_annotations"
|
|
26
|
-
elif [ "$TEST_NAME" == "tools" ]; then
|
|
27
|
-
run_test "test_tool_implementations"
|
|
28
|
-
else
|
|
29
|
-
echo "Unknown test: $TEST_NAME"
|
|
30
|
-
echo "Usage: $0 [params|tools]"
|
|
31
|
-
echo " params - Run parameter annotation tests"
|
|
32
|
-
echo " tools - Run tool implementation tests"
|
|
33
|
-
echo " (no args) - Run all tests"
|
|
34
|
-
exit 1
|
|
35
|
-
fi
|
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
"""Test script for verifying parameter annotations in MCP tools."""
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
import sys
|
|
5
|
-
from awslabs.terraform_mcp_server.server import mcp
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
# Add project root to path to allow importing the server
|
|
10
|
-
project_root = str(Path(__file__).parent.parent.parent.parent)
|
|
11
|
-
if project_root not in sys.path:
|
|
12
|
-
sys.path.insert(0, project_root)
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def print_tool_parameters():
|
|
16
|
-
"""Print the parameters for each tool after annotations are added."""
|
|
17
|
-
tool_names = [
|
|
18
|
-
'SearchAwsProviderDocs',
|
|
19
|
-
'ExecuteTerraformCommand',
|
|
20
|
-
'SearchAwsccProviderDocs',
|
|
21
|
-
'SearchSpecificAwsIaModules',
|
|
22
|
-
'RunCheckovScan',
|
|
23
|
-
]
|
|
24
|
-
|
|
25
|
-
print('\n=== Current Tool Parameter Schemas ===\n')
|
|
26
|
-
for tool_name in tool_names:
|
|
27
|
-
try:
|
|
28
|
-
tool = mcp._tool_manager.get_tool(tool_name)
|
|
29
|
-
if tool is None:
|
|
30
|
-
print(f'Tool {tool_name} not found')
|
|
31
|
-
continue
|
|
32
|
-
|
|
33
|
-
if not hasattr(tool, 'parameters') or tool.parameters is None:
|
|
34
|
-
print(f'Tool {tool_name} has no parameters schema')
|
|
35
|
-
continue
|
|
36
|
-
|
|
37
|
-
print(f'=== {tool_name} Parameters Schema ===')
|
|
38
|
-
print(json.dumps(tool.parameters, indent=2))
|
|
39
|
-
print('\n')
|
|
40
|
-
except Exception as e:
|
|
41
|
-
print(f'Error getting tool {tool_name}: {e}')
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def add_parameter_annotations():
|
|
45
|
-
"""Add parameter annotations to the MCP tools."""
|
|
46
|
-
print('Adding parameter annotations to MCP tools...\n')
|
|
47
|
-
|
|
48
|
-
# Add parameter descriptions for SearchAwsProviderDocs
|
|
49
|
-
search_tool = mcp._tool_manager.get_tool('SearchAwsProviderDocs')
|
|
50
|
-
if (
|
|
51
|
-
search_tool is not None
|
|
52
|
-
and hasattr(search_tool, 'parameters')
|
|
53
|
-
and search_tool.parameters is not None
|
|
54
|
-
):
|
|
55
|
-
if (
|
|
56
|
-
'properties' in search_tool.parameters
|
|
57
|
-
and 'asset_name' in search_tool.parameters['properties']
|
|
58
|
-
):
|
|
59
|
-
search_tool.parameters['properties']['asset_name']['description'] = (
|
|
60
|
-
'Name of the AWS service (asset) to look for (e.g., "aws_s3_bucket", "aws_lambda_function")'
|
|
61
|
-
)
|
|
62
|
-
if (
|
|
63
|
-
'properties' in search_tool.parameters
|
|
64
|
-
and 'asset_type' in search_tool.parameters['properties']
|
|
65
|
-
):
|
|
66
|
-
search_tool.parameters['properties']['asset_type']['description'] = (
|
|
67
|
-
"Type of documentation to search - 'resource', 'data_source', or 'both' (default)"
|
|
68
|
-
)
|
|
69
|
-
|
|
70
|
-
# Add parameter descriptions for SearchAwsccProviderDocs
|
|
71
|
-
awscc_docs_tool = mcp._tool_manager.get_tool('SearchAwsccProviderDocs')
|
|
72
|
-
if (
|
|
73
|
-
awscc_docs_tool is not None
|
|
74
|
-
and hasattr(awscc_docs_tool, 'parameters')
|
|
75
|
-
and awscc_docs_tool.parameters is not None
|
|
76
|
-
):
|
|
77
|
-
if (
|
|
78
|
-
'properties' in awscc_docs_tool.parameters
|
|
79
|
-
and 'asset_name' in awscc_docs_tool.parameters['properties']
|
|
80
|
-
):
|
|
81
|
-
awscc_docs_tool.parameters['properties']['asset_name']['description'] = (
|
|
82
|
-
'Name of the AWSCC service (asset) to look for (e.g., awscc_s3_bucket, awscc_lambda_function)'
|
|
83
|
-
)
|
|
84
|
-
if (
|
|
85
|
-
'properties' in awscc_docs_tool.parameters
|
|
86
|
-
and 'asset_type' in awscc_docs_tool.parameters['properties']
|
|
87
|
-
):
|
|
88
|
-
awscc_docs_tool.parameters['properties']['asset_type']['description'] = (
|
|
89
|
-
"Type of documentation to search - 'resource', 'data_source', or 'both' (default)"
|
|
90
|
-
)
|
|
91
|
-
|
|
92
|
-
# Add parameter descriptions for SearchSpecificAwsIaModules
|
|
93
|
-
modules_tool = mcp._tool_manager.get_tool('SearchSpecificAwsIaModules')
|
|
94
|
-
if (
|
|
95
|
-
modules_tool is not None
|
|
96
|
-
and hasattr(modules_tool, 'parameters')
|
|
97
|
-
and modules_tool.parameters is not None
|
|
98
|
-
):
|
|
99
|
-
if (
|
|
100
|
-
'properties' in modules_tool.parameters
|
|
101
|
-
and 'query' in modules_tool.parameters['properties']
|
|
102
|
-
):
|
|
103
|
-
modules_tool.parameters['properties']['query']['description'] = (
|
|
104
|
-
'Optional search term to filter modules (empty returns all four modules)'
|
|
105
|
-
)
|
|
106
|
-
|
|
107
|
-
# Add parameter descriptions for ExecuteTerraformCommand
|
|
108
|
-
terraform_tool = mcp._tool_manager.get_tool('ExecuteTerraformCommand')
|
|
109
|
-
if (
|
|
110
|
-
terraform_tool is not None
|
|
111
|
-
and hasattr(terraform_tool, 'parameters')
|
|
112
|
-
and terraform_tool.parameters is not None
|
|
113
|
-
):
|
|
114
|
-
if (
|
|
115
|
-
'properties' in terraform_tool.parameters
|
|
116
|
-
and 'request' in terraform_tool.parameters['properties']
|
|
117
|
-
):
|
|
118
|
-
terraform_tool.parameters['properties']['request']['description'] = (
|
|
119
|
-
'Details about the Terraform command to execute'
|
|
120
|
-
)
|
|
121
|
-
|
|
122
|
-
# Since request is a complex object with nested properties, update its schema
|
|
123
|
-
if (
|
|
124
|
-
'properties' in terraform_tool.parameters['properties']['request']
|
|
125
|
-
and 'properties'
|
|
126
|
-
in terraform_tool.parameters['properties']['request']['properties']
|
|
127
|
-
):
|
|
128
|
-
props = terraform_tool.parameters['properties']['request']['properties']
|
|
129
|
-
if 'command' in props:
|
|
130
|
-
props['command']['description'] = (
|
|
131
|
-
'Terraform command to execute (init, plan, validate, apply, destroy)'
|
|
132
|
-
)
|
|
133
|
-
if 'working_directory' in props:
|
|
134
|
-
props['working_directory']['description'] = (
|
|
135
|
-
'Directory containing Terraform files'
|
|
136
|
-
)
|
|
137
|
-
if 'variables' in props:
|
|
138
|
-
props['variables']['description'] = 'Terraform variables to pass'
|
|
139
|
-
if 'aws_region' in props:
|
|
140
|
-
props['aws_region']['description'] = 'AWS region to use'
|
|
141
|
-
if 'strip_ansi' in props:
|
|
142
|
-
props['strip_ansi']['description'] = (
|
|
143
|
-
'Whether to strip ANSI color codes from output'
|
|
144
|
-
)
|
|
145
|
-
|
|
146
|
-
# Add parameter descriptions for RunCheckovScan
|
|
147
|
-
checkov_scan_tool = mcp._tool_manager.get_tool('RunCheckovScan')
|
|
148
|
-
if (
|
|
149
|
-
checkov_scan_tool is not None
|
|
150
|
-
and hasattr(checkov_scan_tool, 'parameters')
|
|
151
|
-
and checkov_scan_tool.parameters is not None
|
|
152
|
-
):
|
|
153
|
-
if (
|
|
154
|
-
'properties' in checkov_scan_tool.parameters
|
|
155
|
-
and 'request' in checkov_scan_tool.parameters['properties']
|
|
156
|
-
):
|
|
157
|
-
checkov_scan_tool.parameters['properties']['request']['description'] = (
|
|
158
|
-
'Details about the Checkov scan to execute'
|
|
159
|
-
)
|
|
160
|
-
|
|
161
|
-
# Since request is a complex object with nested properties, update its schema
|
|
162
|
-
if (
|
|
163
|
-
'properties' in checkov_scan_tool.parameters['properties']['request']
|
|
164
|
-
and 'properties'
|
|
165
|
-
in checkov_scan_tool.parameters['properties']['request']['properties']
|
|
166
|
-
):
|
|
167
|
-
props = checkov_scan_tool.parameters['properties']['request']['properties']
|
|
168
|
-
if 'working_directory' in props:
|
|
169
|
-
props['working_directory']['description'] = (
|
|
170
|
-
'Directory containing Terraform files to scan'
|
|
171
|
-
)
|
|
172
|
-
if 'framework' in props:
|
|
173
|
-
props['framework']['description'] = (
|
|
174
|
-
'Framework to scan (terraform, cloudformation, etc.)'
|
|
175
|
-
)
|
|
176
|
-
if 'check_ids' in props:
|
|
177
|
-
props['check_ids']['description'] = (
|
|
178
|
-
'Optional list of specific check IDs to run'
|
|
179
|
-
)
|
|
180
|
-
if 'skip_check_ids' in props:
|
|
181
|
-
props['skip_check_ids']['description'] = 'Optional list of check IDs to skip'
|
|
182
|
-
if 'output_format' in props:
|
|
183
|
-
props['output_format']['description'] = (
|
|
184
|
-
'Format for scan results (default: json)'
|
|
185
|
-
)
|
|
186
|
-
|
|
187
|
-
print('Parameter annotations added successfully.\n')
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
def main():
|
|
191
|
-
"""Run the parameter annotation test."""
|
|
192
|
-
print('=== Terraform MCP Parameter Annotation Test ===\n')
|
|
193
|
-
|
|
194
|
-
# Print original parameter schemas
|
|
195
|
-
print('Original parameter schemas:')
|
|
196
|
-
print_tool_parameters()
|
|
197
|
-
|
|
198
|
-
# Add parameter annotations
|
|
199
|
-
add_parameter_annotations()
|
|
200
|
-
|
|
201
|
-
# Print updated parameter schemas
|
|
202
|
-
print('Updated parameter schemas:')
|
|
203
|
-
print_tool_parameters()
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
if __name__ == '__main__':
|
|
207
|
-
main()
|
|
@@ -1,309 +0,0 @@
|
|
|
1
|
-
"""Test script for Terraform MCP server implementation functions."""
|
|
2
|
-
|
|
3
|
-
import asyncio
|
|
4
|
-
import json
|
|
5
|
-
import sys
|
|
6
|
-
from awslabs.terraform_mcp_server.impl.tools.search_aws_provider_docs import (
|
|
7
|
-
search_aws_provider_docs_impl,
|
|
8
|
-
)
|
|
9
|
-
from awslabs.terraform_mcp_server.impl.tools.search_awscc_provider_docs import (
|
|
10
|
-
search_awscc_provider_docs_impl,
|
|
11
|
-
)
|
|
12
|
-
from awslabs.terraform_mcp_server.impl.tools.search_specific_aws_ia_modules import (
|
|
13
|
-
search_specific_aws_ia_modules_impl,
|
|
14
|
-
)
|
|
15
|
-
from loguru import logger
|
|
16
|
-
from typing import Any
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
# Configure logger for enhanced diagnostics with stacktraces
|
|
20
|
-
logger.configure(
|
|
21
|
-
handlers=[
|
|
22
|
-
{
|
|
23
|
-
'sink': sys.stderr,
|
|
24
|
-
'backtrace': True,
|
|
25
|
-
'diagnose': True,
|
|
26
|
-
'format': '<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | <level>{level: <8}</level> | <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - <level>{message}</level>',
|
|
27
|
-
}
|
|
28
|
-
]
|
|
29
|
-
)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def print_aws_provider_results(results):
|
|
33
|
-
"""Print formatted results data using the provided logger.
|
|
34
|
-
|
|
35
|
-
Args:
|
|
36
|
-
results: List of result objects containing asset information
|
|
37
|
-
logger: Logger object to use for output
|
|
38
|
-
"""
|
|
39
|
-
logger.info(f'Found {len(results)} results')
|
|
40
|
-
|
|
41
|
-
for i, result in enumerate(results):
|
|
42
|
-
logger.info(f'\nResult {i + 1}:')
|
|
43
|
-
logger.info(f' Asset Name: {result.asset_name}')
|
|
44
|
-
logger.info(f' Asset Type: {result.asset_type}')
|
|
45
|
-
logger.info(f' URL: {result.url}')
|
|
46
|
-
|
|
47
|
-
# Handle description
|
|
48
|
-
if result.description:
|
|
49
|
-
description_preview = (
|
|
50
|
-
result.description[:50] + '...'
|
|
51
|
-
if len(result.description) > 50
|
|
52
|
-
else result.description
|
|
53
|
-
)
|
|
54
|
-
logger.info(f' Description: {description_preview}')
|
|
55
|
-
else:
|
|
56
|
-
logger.info(' No description')
|
|
57
|
-
|
|
58
|
-
# Handle example usage
|
|
59
|
-
if result.example_usage:
|
|
60
|
-
logger.info(f' Example Usage: {len(result.example_usage)} found')
|
|
61
|
-
|
|
62
|
-
# Handle arguments
|
|
63
|
-
if result.arguments:
|
|
64
|
-
logger.info(f' Arguments: {len(result.arguments)} found')
|
|
65
|
-
|
|
66
|
-
# Handle attributes
|
|
67
|
-
if result.attributes:
|
|
68
|
-
logger.info(f' Attributes: {len(result.attributes)} found')
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
def print_awscc_provider_results(results):
|
|
72
|
-
"""Print formatted results data using the provided logger.
|
|
73
|
-
|
|
74
|
-
Args:
|
|
75
|
-
results: List of result objects containing asset information
|
|
76
|
-
logger: Logger object to use for output
|
|
77
|
-
"""
|
|
78
|
-
logger.info(f'Found {len(results)} results')
|
|
79
|
-
|
|
80
|
-
for i, result in enumerate(results):
|
|
81
|
-
logger.info(f'\nResult {i + 1}:')
|
|
82
|
-
logger.info(f' Asset Name: {result.asset_name}')
|
|
83
|
-
logger.info(f' Asset Type: {result.asset_type}')
|
|
84
|
-
logger.info(f' URL: {result.url}')
|
|
85
|
-
|
|
86
|
-
# Handle description
|
|
87
|
-
if result.description:
|
|
88
|
-
description_preview = (
|
|
89
|
-
result.description[:50] + '...'
|
|
90
|
-
if len(result.description) > 50
|
|
91
|
-
else result.description
|
|
92
|
-
)
|
|
93
|
-
logger.info(f' Description: {description_preview}')
|
|
94
|
-
else:
|
|
95
|
-
logger.info(' No description')
|
|
96
|
-
|
|
97
|
-
# Handle example usage
|
|
98
|
-
if result.example_usage:
|
|
99
|
-
logger.info(f' Example Usage: {len(result.example_usage)} found')
|
|
100
|
-
|
|
101
|
-
# Handle schema arguments
|
|
102
|
-
if result.schema_arguments:
|
|
103
|
-
logger.info(f' Schema arguments: {len(result.schema_arguments)} found')
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
async def test_search_aws_provider_docs():
|
|
107
|
-
"""Test the AWS provider docs search function."""
|
|
108
|
-
logger.info('=== Testing search_aws_provider_docs_impl ===')
|
|
109
|
-
|
|
110
|
-
# Test case 1: Common resource with just 1 example snippet
|
|
111
|
-
logger.info('**********---Test case 1: Searching for aws_s3_bucket as a resource---**********')
|
|
112
|
-
results = await search_aws_provider_docs_impl('aws_s3_bucket', 'resource')
|
|
113
|
-
print_aws_provider_results(results)
|
|
114
|
-
|
|
115
|
-
# Test case 2: Common resource with multiple example snippets
|
|
116
|
-
logger.info(
|
|
117
|
-
'**********---Test case 2: Searching for aws_api_gateway_rest_api as a resource---**********'
|
|
118
|
-
)
|
|
119
|
-
results = await search_aws_provider_docs_impl('api_gateway_rest_api', 'resource')
|
|
120
|
-
print_aws_provider_results(results)
|
|
121
|
-
|
|
122
|
-
# Test case 3: Common resource with multiple example snippets and multiple arguments in subsections
|
|
123
|
-
logger.info(
|
|
124
|
-
'**********---Test case 3: Searching for aws_lambda_function as a resource---**********'
|
|
125
|
-
)
|
|
126
|
-
results = await search_aws_provider_docs_impl('aws_lambda_function', 'resource')
|
|
127
|
-
print_aws_provider_results(results)
|
|
128
|
-
|
|
129
|
-
# Test case 4: Specifying data source as asset type
|
|
130
|
-
logger.info(
|
|
131
|
-
'**********---Test case 4: Searching for aws_lambda_function as a data source ---**********'
|
|
132
|
-
)
|
|
133
|
-
results = await search_aws_provider_docs_impl('aws_lambda_function', 'data_source')
|
|
134
|
-
print_aws_provider_results(results)
|
|
135
|
-
|
|
136
|
-
# Test case 5: Searching for both kinds
|
|
137
|
-
logger.info('**********---Test case 5: Searching for aws_dynamodb_table as both ---**********')
|
|
138
|
-
results = await search_aws_provider_docs_impl('aws_dynamodb_table', 'both')
|
|
139
|
-
print_aws_provider_results(results)
|
|
140
|
-
|
|
141
|
-
# Test case 6: Non-existent resource
|
|
142
|
-
logger.info('**********---Test case 6: Searching for non-existent resource---**********')
|
|
143
|
-
results = await search_aws_provider_docs_impl('aws_nonexistent_resource')
|
|
144
|
-
print_aws_provider_results(results)
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
async def test_search_awscc_provider_docs():
|
|
148
|
-
"""Test the AWSCC provider docs search function."""
|
|
149
|
-
logger.info('\n=== Testing search_awscc_provider_docs_impl ===')
|
|
150
|
-
|
|
151
|
-
# Test case 1: Common resource
|
|
152
|
-
logger.info(
|
|
153
|
-
'**********---Test case 1: Searching for awscc_apigateway_api_key as a resource---**********'
|
|
154
|
-
)
|
|
155
|
-
results = await search_awscc_provider_docs_impl('awscc_apigateway_api_key', 'resource')
|
|
156
|
-
print_awscc_provider_results(results)
|
|
157
|
-
|
|
158
|
-
# Test case 2: Resource with attribute
|
|
159
|
-
logger.info(
|
|
160
|
-
'**********---Test case 2: Searching for awscc_apigateway_api_key as a data source---**********'
|
|
161
|
-
)
|
|
162
|
-
results = await search_awscc_provider_docs_impl('awscc_apigateway_api_key', 'data_source')
|
|
163
|
-
print_awscc_provider_results(results)
|
|
164
|
-
|
|
165
|
-
# Test case 3: lambda_function resource
|
|
166
|
-
logger.info(
|
|
167
|
-
'**********---Test case 7: Searching for lambda_function as a resource---**********'
|
|
168
|
-
)
|
|
169
|
-
results = await search_awscc_provider_docs_impl('lambda_function', 'resource')
|
|
170
|
-
print_awscc_provider_results(results)
|
|
171
|
-
|
|
172
|
-
# Test case 4: Searching for both kinds
|
|
173
|
-
logger.info(
|
|
174
|
-
'**********---Test case 4: Searching for lambda_function as both kinds---**********'
|
|
175
|
-
)
|
|
176
|
-
results = await search_awscc_provider_docs_impl('awscc_lambda_function', 'both')
|
|
177
|
-
print_awscc_provider_results(results)
|
|
178
|
-
|
|
179
|
-
# Test case 5: Non-existent resource
|
|
180
|
-
logger.info('**********---Test case 5: Searching for non-existent resource---**********')
|
|
181
|
-
results = await search_awscc_provider_docs_impl('awscc_nonexistent_resource')
|
|
182
|
-
print_awscc_provider_results(results)
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
async def test_search_specific_aws_ia_modules():
|
|
186
|
-
"""Test the AWS IA modules search function."""
|
|
187
|
-
logger.info('\n=== Testing search_specific_aws_ia_modules_impl ===')
|
|
188
|
-
|
|
189
|
-
# Test case 1: Search all modules
|
|
190
|
-
logger.info('Test case 1: Searching all AWS IA modules')
|
|
191
|
-
results = await search_specific_aws_ia_modules_impl('')
|
|
192
|
-
|
|
193
|
-
logger.info(f'Found {len(results)} modules')
|
|
194
|
-
for i, result in enumerate(results):
|
|
195
|
-
logger.info(f'\nModule {i + 1}:')
|
|
196
|
-
logger.info(f' Name: {result.name}')
|
|
197
|
-
logger.info(f' Namespace: {result.namespace}')
|
|
198
|
-
logger.info(
|
|
199
|
-
f' Description: {result.description[:100]}...'
|
|
200
|
-
if result.description
|
|
201
|
-
else ' No description'
|
|
202
|
-
)
|
|
203
|
-
logger.info(f' URL: {result.url}')
|
|
204
|
-
|
|
205
|
-
# Test case 2: Search with query
|
|
206
|
-
logger.info("\nTest case 2: Searching for 'bedrock' modules")
|
|
207
|
-
results = await search_specific_aws_ia_modules_impl('bedrock')
|
|
208
|
-
|
|
209
|
-
logger.info(f'Found {len(results)} modules')
|
|
210
|
-
for i, result in enumerate(results):
|
|
211
|
-
logger.info(f'\nModule {i + 1}:')
|
|
212
|
-
logger.info(f' Name: {result.name}')
|
|
213
|
-
logger.info(f' Namespace: {result.namespace}')
|
|
214
|
-
logger.info(
|
|
215
|
-
f' Description: {result.description[:100]}...'
|
|
216
|
-
if result.description
|
|
217
|
-
else ' No description'
|
|
218
|
-
)
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
async def test_execute_terraform_command():
|
|
222
|
-
"""Test the Terraform command execution function.
|
|
223
|
-
|
|
224
|
-
Note: This test requires a valid Terraform configuration in a temporary directory.
|
|
225
|
-
Skip this test if you don't have a valid Terraform configuration to test with.
|
|
226
|
-
"""
|
|
227
|
-
logger.info('\n=== Testing execute_terraform_command_impl ===')
|
|
228
|
-
logger.info('Skipping actual execution as it requires a valid Terraform configuration.')
|
|
229
|
-
logger.info('To test this function, you would need to:')
|
|
230
|
-
logger.info('1. Create a temporary directory with valid Terraform files')
|
|
231
|
-
logger.info('2. Run terraform init, plan, etc. on those files')
|
|
232
|
-
|
|
233
|
-
# Example of how you would call it (commented out)
|
|
234
|
-
"""
|
|
235
|
-
request = TerraformExecutionRequest(
|
|
236
|
-
command="validate",
|
|
237
|
-
working_directory="/path/to/terraform/config",
|
|
238
|
-
variables={"environment": "test"},
|
|
239
|
-
aws_region="us-west-2",
|
|
240
|
-
strip_ansi=True
|
|
241
|
-
)
|
|
242
|
-
|
|
243
|
-
result = await execute_terraform_command_impl(request)
|
|
244
|
-
logger.info(f"Command: {result.command}")
|
|
245
|
-
logger.info(f"Status: {result.status}")
|
|
246
|
-
logger.info(f"Return Code: {result.return_code}")
|
|
247
|
-
if result.stdout:
|
|
248
|
-
logger.info(f"Stdout: {result.stdout[:100]}...")
|
|
249
|
-
if result.stderr:
|
|
250
|
-
logger.info(f"Stderr: {result.stderr[:100]}...")
|
|
251
|
-
"""
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
async def test_run_checkov_scan():
|
|
255
|
-
"""Test the Checkov scan function.
|
|
256
|
-
|
|
257
|
-
Note: This test requires a valid Terraform configuration in a temporary directory.
|
|
258
|
-
Skip this test if you don't have a valid Terraform configuration to test with.
|
|
259
|
-
"""
|
|
260
|
-
logger.info('\n=== Testing run_checkov_scan_impl ===')
|
|
261
|
-
logger.info('Skipping actual execution as it requires a valid Terraform configuration.')
|
|
262
|
-
logger.info('To test this function, you would need to:')
|
|
263
|
-
logger.info('1. Create a temporary directory with valid Terraform files')
|
|
264
|
-
logger.info('2. Run Checkov on those files')
|
|
265
|
-
|
|
266
|
-
# Example of how you would call it (commented out)
|
|
267
|
-
"""
|
|
268
|
-
request = CheckovScanRequest(
|
|
269
|
-
working_directory="/path/to/terraform/config",
|
|
270
|
-
framework="terraform",
|
|
271
|
-
output_format="json"
|
|
272
|
-
)
|
|
273
|
-
|
|
274
|
-
result = await run_checkov_scan_impl(request)
|
|
275
|
-
logger.info(f"Status: {result.status}")
|
|
276
|
-
logger.info(f"Return Code: {result.return_code}")
|
|
277
|
-
logger.info(f"Found {len(result.vulnerabilities)} vulnerabilities")
|
|
278
|
-
for i, vuln in enumerate(result.vulnerabilities[:3]): # Show first 3 only
|
|
279
|
-
logger.info(f"\nVulnerability {i+1}:")
|
|
280
|
-
logger.info(f" ID: {vuln.id}")
|
|
281
|
-
logger.info(f" Resource: {vuln.resource}")
|
|
282
|
-
logger.info(f" Description: {vuln.description[:100]}..." if vuln.description else " No description")
|
|
283
|
-
"""
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
def format_json(obj: Any) -> str:
|
|
287
|
-
"""Format an object as pretty JSON."""
|
|
288
|
-
if hasattr(obj, 'model_dump'):
|
|
289
|
-
# For Pydantic v2
|
|
290
|
-
data = obj.model_dump()
|
|
291
|
-
elif hasattr(obj, 'dict'):
|
|
292
|
-
# For Pydantic v1
|
|
293
|
-
data = obj.dict()
|
|
294
|
-
else:
|
|
295
|
-
data = obj
|
|
296
|
-
return json.dumps(data, indent=2, default=str)
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
async def main():
|
|
300
|
-
"""Run all tests."""
|
|
301
|
-
try:
|
|
302
|
-
await test_search_aws_provider_docs()
|
|
303
|
-
await test_search_awscc_provider_docs()
|
|
304
|
-
except Exception as e:
|
|
305
|
-
logger.exception(f'Error running tests: {e}')
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
if __name__ == '__main__':
|
|
309
|
-
asyncio.run(main())
|
{awslabs_terraform_mcp_server-0.0.2.dist-info → awslabs_terraform_mcp_server-0.0.7.dist-info}/WHEEL
RENAMED
|
File without changes
|
|
File without changes
|