awslabs.terraform-mcp-server 1.0.14__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. awslabs/__init__.py +17 -0
  2. awslabs/terraform_mcp_server/__init__.py +17 -0
  3. awslabs/terraform_mcp_server/impl/resources/__init__.py +25 -0
  4. awslabs/terraform_mcp_server/impl/resources/terraform_aws_provider_resources_listing.py +66 -0
  5. awslabs/terraform_mcp_server/impl/resources/terraform_awscc_provider_resources_listing.py +69 -0
  6. awslabs/terraform_mcp_server/impl/tools/__init__.py +33 -0
  7. awslabs/terraform_mcp_server/impl/tools/execute_terraform_command.py +223 -0
  8. awslabs/terraform_mcp_server/impl/tools/execute_terragrunt_command.py +320 -0
  9. awslabs/terraform_mcp_server/impl/tools/run_checkov_scan.py +376 -0
  10. awslabs/terraform_mcp_server/impl/tools/search_aws_provider_docs.py +691 -0
  11. awslabs/terraform_mcp_server/impl/tools/search_awscc_provider_docs.py +641 -0
  12. awslabs/terraform_mcp_server/impl/tools/search_specific_aws_ia_modules.py +458 -0
  13. awslabs/terraform_mcp_server/impl/tools/search_user_provided_module.py +349 -0
  14. awslabs/terraform_mcp_server/impl/tools/utils.py +572 -0
  15. awslabs/terraform_mcp_server/models/__init__.py +49 -0
  16. awslabs/terraform_mcp_server/models/models.py +381 -0
  17. awslabs/terraform_mcp_server/scripts/generate_aws_provider_resources.py +1240 -0
  18. awslabs/terraform_mcp_server/scripts/generate_awscc_provider_resources.py +1039 -0
  19. awslabs/terraform_mcp_server/scripts/scrape_aws_terraform_best_practices.py +143 -0
  20. awslabs/terraform_mcp_server/server.py +440 -0
  21. awslabs/terraform_mcp_server/static/AWSCC_PROVIDER_RESOURCES.md +3125 -0
  22. awslabs/terraform_mcp_server/static/AWS_PROVIDER_RESOURCES.md +3833 -0
  23. awslabs/terraform_mcp_server/static/AWS_TERRAFORM_BEST_PRACTICES.md +2523 -0
  24. awslabs/terraform_mcp_server/static/MCP_INSTRUCTIONS.md +142 -0
  25. awslabs/terraform_mcp_server/static/TERRAFORM_WORKFLOW_GUIDE.md +330 -0
  26. awslabs/terraform_mcp_server/static/__init__.py +38 -0
  27. awslabs_terraform_mcp_server-1.0.14.dist-info/METADATA +166 -0
  28. awslabs_terraform_mcp_server-1.0.14.dist-info/RECORD +30 -0
  29. awslabs_terraform_mcp_server-1.0.14.dist-info/WHEEL +4 -0
  30. awslabs_terraform_mcp_server-1.0.14.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,641 @@
1
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Implementation of AWSCC provider documentation search tool."""
16
+
17
+ import re
18
+ import requests
19
+ import sys
20
+ import time
21
+ from awslabs.terraform_mcp_server.models import TerraformAWSCCProviderDocsResult
22
+ from loguru import logger
23
+ from pathlib import Path
24
+ from typing import Any, Dict, List, Literal, Optional, Tuple, cast
25
+
26
+
27
+ # Configure logger for enhanced diagnostics with stacktraces
28
+ logger.configure(
29
+ handlers=[
30
+ {
31
+ 'sink': sys.stderr,
32
+ 'backtrace': True,
33
+ 'diagnose': True,
34
+ '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>',
35
+ }
36
+ ]
37
+ )
38
+
39
+ # Path to the static markdown file
40
+ STATIC_RESOURCES_PATH = (
41
+ Path(__file__).parent.parent.parent / 'static' / 'AWSCC_PROVIDER_RESOURCES.md'
42
+ )
43
+
44
+ # Base URLs for AWSCC provider documentation
45
+ AWSCC_DOCS_BASE_URL = 'https://registry.terraform.io/providers/hashicorp/awscc/latest/docs'
46
+ GITHUB_RAW_BASE_URL = (
47
+ 'https://raw.githubusercontent.com/hashicorp/terraform-provider-awscc/main/docs'
48
+ )
49
+
50
+ # Simple in-memory cache
51
+ _GITHUB_DOC_CACHE = {}
52
+
53
+
54
+ def resource_to_github_path(
55
+ asset_name: str, asset_type: str = 'resource', correlation_id: str = ''
56
+ ) -> Tuple[str, str]:
57
+ """Convert AWSCC resource type to GitHub documentation file path.
58
+
59
+ Args:
60
+ asset_name: The name of the asset to search (e.g., 'awscc_s3_bucket')
61
+ asset_type: Type of asset to search for - 'resource' or 'data_source'
62
+ correlation_id: Identifier for tracking this request in logs
63
+
64
+ Returns:
65
+ A tuple of (path, url) for the GitHub documentation file
66
+ """
67
+ # Validate input parameters
68
+ if not isinstance(asset_name, str) or not asset_name:
69
+ logger.error(f'[{correlation_id}] Invalid asset_name: {asset_name}')
70
+ raise ValueError('asset_name must be a non-empty string')
71
+
72
+ # Sanitize asset_name to prevent path traversal and URL manipulation
73
+ # Only allow alphanumeric characters, underscores, and hyphens
74
+ sanitized_name = asset_name
75
+ if not re.match(r'^[a-zA-Z0-9_-]+$', sanitized_name.replace('awscc_', '')):
76
+ logger.error(f'[{correlation_id}] Invalid characters in asset_name: {asset_name}')
77
+ raise ValueError('asset_name contains invalid characters')
78
+
79
+ # Validate asset_type
80
+ valid_asset_types = ['resource', 'data_source', 'both']
81
+ if asset_type not in valid_asset_types:
82
+ logger.error(f'[{correlation_id}] Invalid asset_type: {asset_type}')
83
+ raise ValueError(f'asset_type must be one of {valid_asset_types}')
84
+
85
+ # Remove the 'awscc_' prefix if present
86
+ if sanitized_name.startswith('awscc_'):
87
+ resource_name = sanitized_name[6:]
88
+ logger.trace(f"[{correlation_id}] Removed 'awscc_' prefix: {resource_name}")
89
+ else:
90
+ resource_name = sanitized_name
91
+ logger.trace(f"[{correlation_id}] No 'awscc_' prefix to remove: {resource_name}")
92
+
93
+ # Determine document type based on asset_type parameter
94
+ if asset_type == 'data_source':
95
+ doc_type = 'data-sources' # data sources
96
+ elif asset_type == 'resource':
97
+ doc_type = 'resources' # resources
98
+ else:
99
+ # For "both" or any other value, determine based on name pattern
100
+ # Data sources typically have 'data' in the name or follow other patterns
101
+ is_data_source = 'data' in sanitized_name.lower()
102
+ doc_type = 'data-sources' if is_data_source else 'resources'
103
+
104
+ # Create the file path for the markdown documentation
105
+ file_path = f'{doc_type}/{resource_name}.md'
106
+ logger.trace(f'[{correlation_id}] Constructed GitHub file path: {file_path}')
107
+
108
+ # Create the full URL to the raw GitHub content
109
+ github_url = f'{GITHUB_RAW_BASE_URL}/{file_path}'
110
+ logger.trace(f'[{correlation_id}] GitHub raw URL: {github_url}')
111
+
112
+ return file_path, github_url
113
+
114
+
115
+ def fetch_github_documentation(
116
+ asset_name: str, asset_type: str, cache_enabled: bool, correlation_id: str = ''
117
+ ) -> Optional[Dict[str, Any]]:
118
+ """Fetch documentation from GitHub for a specific resource type.
119
+
120
+ Args:
121
+ asset_name: The asset name (e.g., 'awscc_s3_bucket')
122
+ asset_type: Either 'resource' or 'data_source'
123
+ cache_enabled: Whether local cache is enabled or not
124
+ correlation_id: Identifier for tracking this request in logs
125
+
126
+ Returns:
127
+ Dictionary with markdown content and metadata, or None if not found
128
+ """
129
+ start_time = time.time()
130
+ logger.info(f"[{correlation_id}] Fetching documentation from GitHub for '{asset_name}'")
131
+
132
+ # Create a cache key that includes both asset_name and asset_type
133
+ # Use a hash function to ensure the cache key is safe
134
+ cache_key = f'{asset_name}_{asset_type}'
135
+
136
+ # Check cache first
137
+ if cache_enabled:
138
+ if cache_key in _GITHUB_DOC_CACHE:
139
+ logger.info(
140
+ f"[{correlation_id}] Using cached documentation for '{asset_name}' (asset_type: {asset_type})"
141
+ )
142
+ return _GITHUB_DOC_CACHE[cache_key]
143
+
144
+ try:
145
+ # Convert resource type to GitHub path and URL
146
+ # This will validate and sanitize the input
147
+ try:
148
+ _, github_url = resource_to_github_path(asset_name, asset_type, correlation_id)
149
+ except ValueError as e:
150
+ logger.error(f'[{correlation_id}] Invalid input parameters: {str(e)}')
151
+ return None
152
+
153
+ # Validate the constructed URL to ensure it points to the expected domain
154
+ if not github_url.startswith(GITHUB_RAW_BASE_URL):
155
+ logger.error(f'[{correlation_id}] Invalid GitHub URL constructed: {github_url}')
156
+ return None
157
+
158
+ # Fetch the markdown content from GitHub
159
+ logger.info(f'[{correlation_id}] Fetching from GitHub: {github_url}')
160
+ response = requests.get(github_url, timeout=10)
161
+
162
+ if response.status_code != 200:
163
+ logger.warning(
164
+ f'[{correlation_id}] GitHub request failed: HTTP {response.status_code}'
165
+ )
166
+ return None
167
+
168
+ markdown_content = response.text
169
+ content_length = len(markdown_content)
170
+ logger.debug(f'[{correlation_id}] Received markdown content: {content_length} bytes')
171
+
172
+ if content_length > 0:
173
+ preview_length = min(200, content_length)
174
+ logger.trace(
175
+ f'[{correlation_id}] Markdown preview: {markdown_content[:preview_length]}...'
176
+ )
177
+
178
+ # Parse the markdown content
179
+ result = parse_markdown_documentation(
180
+ markdown_content, asset_name, github_url, correlation_id
181
+ )
182
+
183
+ # Cache the result with the composite key
184
+ if cache_enabled:
185
+ _GITHUB_DOC_CACHE[cache_key] = result
186
+
187
+ fetch_time = time.time() - start_time
188
+ logger.info(f'[{correlation_id}] GitHub documentation fetched in {fetch_time:.2f} seconds')
189
+ return result
190
+
191
+ except requests.exceptions.Timeout as e:
192
+ logger.warning(f'[{correlation_id}] Timeout error fetching from GitHub: {str(e)}')
193
+ return None
194
+ except requests.exceptions.RequestException as e:
195
+ logger.warning(f'[{correlation_id}] Request error fetching from GitHub: {str(e)}')
196
+ return None
197
+ except Exception as e:
198
+ logger.error(
199
+ f'[{correlation_id}] Unexpected error fetching from GitHub: {type(e).__name__}: {str(e)}'
200
+ )
201
+ # Don't log the full stack trace to avoid information disclosure
202
+ return None
203
+
204
+
205
+ def parse_markdown_documentation(
206
+ content: str,
207
+ asset_name: str,
208
+ url: str,
209
+ correlation_id: str = '',
210
+ ) -> Dict[str, Any]:
211
+ """Parse markdown documentation content for a resource.
212
+
213
+ Args:
214
+ content: The markdown content
215
+ asset_name: The asset name
216
+ url: The source URL for this documentation
217
+ correlation_id: Identifier for tracking this request in logs
218
+
219
+ Returns:
220
+ Dictionary with parsed documentation details
221
+ """
222
+ start_time = time.time()
223
+ logger.debug(f"[{correlation_id}] Parsing markdown documentation for '{asset_name}'")
224
+
225
+ try:
226
+ # Find the title (typically the first heading)
227
+ title_match = re.search(r'^#\s+(.*?)$', content, re.MULTILINE)
228
+ if title_match:
229
+ title = title_match.group(1).strip()
230
+ logger.debug(f"[{correlation_id}] Found title: '{title}'")
231
+ else:
232
+ title = f'AWS {asset_name}'
233
+ logger.debug(f"[{correlation_id}] No title found, using default: '{title}'")
234
+
235
+ # Find the main resource description section (all content after resource title before next heading)
236
+ description = ''
237
+ resource_heading_pattern = re.compile(
238
+ rf'# {re.escape(asset_name)}\s+\(Resource\)\s*(.*?)(?=\n#|\Z)', re.DOTALL
239
+ )
240
+ resource_match = resource_heading_pattern.search(content)
241
+
242
+ if resource_match:
243
+ # Extract the description text and clean it up
244
+ description = resource_match.group(1).strip()
245
+ logger.debug(
246
+ f"[{correlation_id}] Found resource description section: '{description[:100]}...'"
247
+ )
248
+ else:
249
+ # Fall back to the description found on the starting markdown table of each github markdown page
250
+ desc_match = re.search(r'description:\s*\|-\n(.*?)\n---', content, re.MULTILINE)
251
+ if desc_match:
252
+ description = desc_match.group(1).strip()
253
+ logger.debug(
254
+ f"[{correlation_id}] Using fallback description: '{description[:100]}...'"
255
+ )
256
+ else:
257
+ description = f'Documentation for AWSCC {asset_name}'
258
+ logger.debug(f'[{correlation_id}] No description found, using default')
259
+
260
+ # Find all example snippets
261
+ example_snippets = []
262
+
263
+ # First try to extract from the Example Usage section
264
+ example_section_match = re.search(r'## Example Usage\n([\s\S]*?)(?=\n## |\Z)', content)
265
+
266
+ if example_section_match:
267
+ # logger.debug(f"example_section_match: {example_section_match.group()}")
268
+ example_section = example_section_match.group(1).strip()
269
+ logger.debug(
270
+ f'[{correlation_id}] Found Example Usage section ({len(example_section)} chars)'
271
+ )
272
+
273
+ # Find all subheadings in the Example Usage section with a more robust pattern
274
+ subheading_list = list(
275
+ re.finditer(r'### (.*?)[\r\n]+(.*?)(?=###|\Z)', example_section, re.DOTALL)
276
+ )
277
+ logger.debug(
278
+ f'[{correlation_id}] Found {len(subheading_list)} subheadings in Example Usage section'
279
+ )
280
+ subheading_found = False
281
+
282
+ # Check if there are any subheadings
283
+ for match in subheading_list:
284
+ # logger.info(f"subheading match: {match.group()}")
285
+ subheading_found = True
286
+ title = match.group(1).strip()
287
+ subcontent = match.group(2).strip()
288
+
289
+ logger.debug(
290
+ f"[{correlation_id}] Found subheading '{title}' with {len(subcontent)} chars content"
291
+ )
292
+
293
+ # Find code blocks in this subsection - pattern to match terraform code blocks
294
+ code_match = re.search(r'```(?:terraform|hcl)?\s*(.*?)```', subcontent, re.DOTALL)
295
+ if code_match:
296
+ code_snippet = code_match.group(1).strip()
297
+ example_snippets.append({'title': title, 'code': code_snippet})
298
+ logger.debug(
299
+ f"[{correlation_id}] Added example snippet for '{title}' ({len(code_snippet)} chars)"
300
+ )
301
+
302
+ # If no subheadings were found, look for direct code blocks under Example Usage
303
+ if not subheading_found:
304
+ logger.debug(
305
+ f'[{correlation_id}] No subheadings found, looking for direct code blocks'
306
+ )
307
+ # Improved pattern for code blocks
308
+ code_blocks = re.finditer(
309
+ r'```(?:terraform|hcl)?\s*(.*?)```', example_section, re.DOTALL
310
+ )
311
+ code_found = False
312
+
313
+ for code_match in code_blocks:
314
+ code_found = True
315
+ code_snippet = code_match.group(1).strip()
316
+ example_snippets.append({'title': 'Example Usage', 'code': code_snippet})
317
+ logger.debug(
318
+ f'[{correlation_id}] Added direct example snippet ({len(code_snippet)} chars)'
319
+ )
320
+
321
+ if not code_found:
322
+ logger.debug(
323
+ f'[{correlation_id}] No code blocks found in Example Usage section'
324
+ )
325
+ else:
326
+ logger.debug(f'[{correlation_id}] No Example Usage section found')
327
+
328
+ if example_snippets:
329
+ logger.info(f'[{correlation_id}] Found {len(example_snippets)} example snippets')
330
+ else:
331
+ logger.debug(f'[{correlation_id}] No example snippets found')
332
+
333
+ # Extract Schema section
334
+ schema_arguments = []
335
+ schema_section_match = re.search(r'## Schema\n([\s\S]*?)(?=\n## |\Z)', content)
336
+ if schema_section_match:
337
+ schema_section = schema_section_match.group(1).strip()
338
+ logger.debug(f'[{correlation_id}] Found Schema section ({len(schema_section)} chars)')
339
+
340
+ # DO NOT Look for schema arguments directly under the main Schema section
341
+ # args_under_main_section_match = re.search(r'(.*?)(?=\n###|\n##|$)', schema_section, re.DOTALL)
342
+ # if args_under_main_section_match:
343
+ # args_under_main_section = args_under_main_section_match.group(1).strip()
344
+ # logger.debug(
345
+ # f'[{correlation_id}] Found arguments directly under the Schema section ({len(args_under_main_section)} chars)'
346
+ # )
347
+
348
+ # # Find arguments in this subsection
349
+ # arg_matches = re.finditer(
350
+ # r'-\s+`([^`]+)`\s+(.*?)(?=\n-\s+`|$)',
351
+ # args_under_main_section,
352
+ # re.DOTALL,
353
+ # )
354
+ # arg_list = list(arg_matches)
355
+ # logger.debug(
356
+ # f'[{correlation_id}] Found {len(arg_list)} arguments directly under the Argument Reference section'
357
+ # )
358
+
359
+ # for match in arg_list:
360
+ # arg_name = match.group(1).strip()
361
+ # arg_desc = match.group(2).strip() if match.group(2) else None
362
+ # # Do not add arguments that do not have a description
363
+ # if arg_name is not None and arg_desc is not None:
364
+ # schema_arguments.append({'name': arg_name, 'description': arg_desc, 'schema_section': "main"})
365
+ # logger.debug(
366
+ # f"[{correlation_id}] Added argument '{arg_name}': '{arg_desc[:50]}...' (truncated)"
367
+ # )
368
+
369
+ # Now, Find all subheadings in the Argument Reference section with a more robust pattern
370
+ subheading_list = list(
371
+ re.finditer(r'### (.*?)[\r\n]+(.*?)(?=###|\Z)', schema_section, re.DOTALL)
372
+ )
373
+ logger.debug(
374
+ f'[{correlation_id}] Found {len(subheading_list)} subheadings in Argument Reference section'
375
+ )
376
+ subheading_found = False
377
+
378
+ # Check if there are any subheadings
379
+ for match in subheading_list:
380
+ subheading_found = True
381
+ title = match.group(1).strip()
382
+ subcontent = match.group(2).strip()
383
+ logger.debug(
384
+ f"[{correlation_id}] Found subheading '{title}' with {len(subcontent)} chars content"
385
+ )
386
+
387
+ # Find arguments in this subsection
388
+ arg_matches = re.finditer(
389
+ r'-\s+`([^`]+)`\s+(.*?)(?=\n-\s+`|$)',
390
+ subcontent,
391
+ re.DOTALL,
392
+ )
393
+ arg_list = list(arg_matches)
394
+ logger.debug(
395
+ f'[{correlation_id}] Found {len(arg_list)} arguments in subheading {title}'
396
+ )
397
+
398
+ for match in arg_list:
399
+ arg_name = match.group(1).strip()
400
+ arg_desc = match.group(2).strip() if match.group(2) else None
401
+ # Do not add arguments that do not have a description
402
+ if arg_name is not None and arg_desc is not None:
403
+ schema_arguments.append(
404
+ {'name': arg_name, 'description': arg_desc, 'argument_section': title}
405
+ )
406
+ else:
407
+ logger.debug(
408
+ f"[{correlation_id}] Added argument '{arg_name}': '{arg_desc[:50] if arg_desc else 'No description found'}...' (truncated)"
409
+ )
410
+
411
+ schema_arguments = schema_arguments if schema_arguments else None
412
+ if schema_arguments:
413
+ logger.info(
414
+ f'[{correlation_id}] Found {len(schema_arguments)} arguments across all sections'
415
+ )
416
+ else:
417
+ logger.debug(f'[{correlation_id}] No Schema section found')
418
+
419
+ # Return the parsed information
420
+ parse_time = time.time() - start_time
421
+ logger.debug(f'[{correlation_id}] Markdown parsing completed in {parse_time:.2f} seconds')
422
+
423
+ return {
424
+ 'title': title,
425
+ 'description': description,
426
+ 'example_snippets': example_snippets if example_snippets else None,
427
+ 'url': url,
428
+ 'schema_arguments': schema_arguments,
429
+ }
430
+
431
+ except Exception as e:
432
+ logger.exception(f'[{correlation_id}] Error parsing markdown content')
433
+ logger.error(f'[{correlation_id}] Error type: {type(e).__name__}, message: {str(e)}')
434
+
435
+ # Return partial info if available
436
+ return {
437
+ 'title': f'AWSCC {asset_name}',
438
+ 'description': f'Documentation for AWSCC {asset_name} (Error parsing details: {str(e)})',
439
+ 'url': url,
440
+ 'example_snippets': None,
441
+ 'schema_arguments': None,
442
+ }
443
+
444
+
445
+ async def search_awscc_provider_docs_impl(
446
+ asset_name: str, asset_type: str = 'resource', cache_enabled: bool = False
447
+ ) -> List[TerraformAWSCCProviderDocsResult]:
448
+ """Search AWSCC provider documentation for resources and data sources.
449
+
450
+ This tool searches the Terraform AWSCC provider documentation for information about
451
+ specific assets, which can either be resources or data sources. It retrieves comprehensive details including
452
+ descriptions, example code snippets, and schema information.
453
+
454
+ The AWSCC provider is based on the AWS Cloud Control API and provides a more consistent interface to AWS resources compared to the standard AWS provider.
455
+
456
+ The implementation fetches documentation directly from the official Terraform AWSCC provider
457
+ GitHub repository to ensure the most up-to-date information. Results are cached for
458
+ improved performance on subsequent queries.
459
+
460
+ The tool retrieves comprehensive details including descriptions, example code snippets,
461
+ and schema information (required, optional, and read-only attributes). It also handles
462
+ nested schema structures for complex attributes.
463
+
464
+ The tool will automatically handle prefixes - you can search for either 'awscc_s3_bucket' or 's3_bucket'.
465
+
466
+ Examples:
467
+ - To get documentation for an S3 bucket resource:
468
+ search_awscc_provider_docs_impl(resource_type='awscc_s3_bucket')
469
+
470
+ - To find information about a specific attribute:
471
+ search_awscc_provider_docs_impl(resource_type='awscc_lambda_function', attribute='code')
472
+
473
+ - Without the prefix:
474
+ search_awscc_provider_docs_impl(resource_type='ec2_instance')
475
+
476
+ Parameters:
477
+ asset_name: Name of the AWSCC Provider resource or data source to look for (e.g., 'awscc_s3_bucket', 'awscc_lambda_function')
478
+ asset_type: Type of documentation to search - 'resource' (default), 'data_source', or 'both'. Some resources and data sources share the same name
479
+
480
+ Returns:
481
+ A list of matching documentation entries with details including:
482
+ - Resource name and description
483
+ - URL to the official documentation
484
+ - Example code snippets
485
+ - Schema information (required, optional, read-only, and nested structures attributes)
486
+ """
487
+ start_time = time.time()
488
+ correlation_id = f'search-{int(start_time * 1000)}'
489
+ logger.info(f"[{correlation_id}] Starting AWSCC provider docs search for '{asset_name}'")
490
+
491
+ # Validate input parameters
492
+ if not isinstance(asset_name, str) or not asset_name:
493
+ logger.error(f'[{correlation_id}] Invalid asset_name parameter: {asset_name}')
494
+ return [
495
+ TerraformAWSCCProviderDocsResult(
496
+ asset_name='Error',
497
+ asset_type=cast(Literal['both', 'resource', 'data_source'], asset_type),
498
+ description='Invalid asset_name parameter. Must be a non-empty string.',
499
+ url=None,
500
+ example_usage=None,
501
+ schema_arguments=None,
502
+ )
503
+ ]
504
+
505
+ # Validate asset_type
506
+ valid_asset_types = ['resource', 'data_source', 'both']
507
+ if asset_type not in valid_asset_types:
508
+ logger.error(f'[{correlation_id}] Invalid asset_type parameter: {asset_type}')
509
+ return [
510
+ TerraformAWSCCProviderDocsResult(
511
+ asset_name='Error',
512
+ asset_type=cast(Literal['both', 'resource', 'data_source'], 'resource'),
513
+ description=f'Invalid asset_type parameter. Must be one of {valid_asset_types}.',
514
+ url=None,
515
+ example_usage=None,
516
+ schema_arguments=None,
517
+ )
518
+ ]
519
+
520
+ search_term = asset_name.lower()
521
+
522
+ try:
523
+ # Try fetching from GitHub
524
+ logger.info(f'[{correlation_id}] Fetching from GitHub')
525
+
526
+ results = []
527
+
528
+ # If asset_type is "both", try both resource and data source paths
529
+ if asset_type == 'both':
530
+ logger.info(f'[{correlation_id}] Searching for both resources and data sources')
531
+
532
+ # First try as a resource
533
+ github_result = fetch_github_documentation(
534
+ search_term, 'resource', cache_enabled, correlation_id
535
+ )
536
+ if github_result:
537
+ logger.info(f'[{correlation_id}] Found documentation as a resource')
538
+ # Create result object
539
+ description = github_result['description']
540
+
541
+ result = TerraformAWSCCProviderDocsResult(
542
+ asset_name=asset_name,
543
+ asset_type='resource',
544
+ description=description,
545
+ url=github_result['url'],
546
+ example_usage=github_result.get('example_snippets'),
547
+ schema_arguments=github_result.get('schema_arguments'),
548
+ )
549
+ results.append(result)
550
+
551
+ # Then try as a data source
552
+ data_result = fetch_github_documentation(
553
+ search_term, 'data_source', cache_enabled, correlation_id
554
+ )
555
+ if data_result:
556
+ logger.info(f'[{correlation_id}] Found documentation as a data source')
557
+ # Create result object
558
+ description = data_result['description']
559
+
560
+ result = TerraformAWSCCProviderDocsResult(
561
+ asset_name=asset_name,
562
+ asset_type='data_source',
563
+ description=description,
564
+ url=data_result['url'],
565
+ example_usage=data_result.get('example_snippets'),
566
+ schema_arguments=data_result.get('schema_arguments'),
567
+ )
568
+ results.append(result)
569
+
570
+ if results:
571
+ logger.info(f'[{correlation_id}] Found {len(results)} documentation entries')
572
+ end_time = time.time()
573
+ logger.info(
574
+ f'[{correlation_id}] Search completed in {end_time - start_time:.2f} seconds (GitHub source)'
575
+ )
576
+ return results
577
+ else:
578
+ # Search for either resource or data source based on asset_type parameter
579
+ github_result = fetch_github_documentation(
580
+ search_term, asset_type, cache_enabled, correlation_id
581
+ )
582
+ if github_result:
583
+ logger.info(f'[{correlation_id}] Successfully found GitHub documentation')
584
+
585
+ # Create result object
586
+ description = github_result['description']
587
+ result = TerraformAWSCCProviderDocsResult(
588
+ asset_name=asset_name,
589
+ asset_type=cast(Literal['both', 'resource', 'data_source'], asset_type),
590
+ description=description,
591
+ url=github_result['url'],
592
+ example_usage=github_result.get('example_snippets'),
593
+ schema_arguments=github_result.get('schema_arguments'),
594
+ )
595
+
596
+ end_time = time.time()
597
+ logger.info(
598
+ f'[{correlation_id}] Search completed in {end_time - start_time:.2f} seconds (GitHub source)'
599
+ )
600
+ return [result]
601
+
602
+ # If GitHub approach fails, return a "not found" result
603
+ logger.warning(f"[{correlation_id}] Documentation not found on GitHub for '{search_term}'")
604
+
605
+ # Return a "not found" result
606
+ logger.warning(f'[{correlation_id}] No documentation found for asset {asset_name}')
607
+ end_time = time.time()
608
+ logger.info(
609
+ f'[{correlation_id}] Search completed in {end_time - start_time:.2f} seconds (no results)'
610
+ )
611
+ return [
612
+ TerraformAWSCCProviderDocsResult(
613
+ asset_name='Not found',
614
+ asset_type=cast(Literal['both', 'resource', 'data_source'], asset_type),
615
+ description=f"No documentation found for resource type '{asset_name}'.",
616
+ url=None,
617
+ example_usage=None,
618
+ schema_arguments=None,
619
+ )
620
+ ]
621
+
622
+ except Exception as e:
623
+ logger.error(
624
+ f'[{correlation_id}] Error searching AWSCC provider docs: {type(e).__name__}: {str(e)}'
625
+ )
626
+ # Don't log the full stack trace to avoid information disclosure
627
+
628
+ end_time = time.time()
629
+ logger.info(f'[{correlation_id}] Search failed in {end_time - start_time:.2f} seconds')
630
+
631
+ # Return a generic error message without exposing internal details
632
+ return [
633
+ TerraformAWSCCProviderDocsResult(
634
+ asset_name='Error',
635
+ asset_type=cast(Literal['both', 'resource', 'data_source'], asset_type),
636
+ description='Failed to search AWSCC provider documentation. Please check your input and try again.',
637
+ url=None,
638
+ example_usage=None,
639
+ schema_arguments=None,
640
+ )
641
+ ]