awslabs.cdk-mcp-server 0.1.1__py3-none-any.whl → 0.1.3__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 (32) hide show
  1. awslabs/cdk_mcp_server/core/resources.py +104 -15
  2. awslabs/cdk_mcp_server/core/server.py +4 -3
  3. awslabs/cdk_mcp_server/core/tools.py +6 -1
  4. awslabs/cdk_mcp_server/data/genai_cdk_loader.py +508 -349
  5. {awslabs_cdk_mcp_server-0.1.1.dist-info → awslabs_cdk_mcp_server-0.1.3.dist-info}/METADATA +24 -1
  6. awslabs_cdk_mcp_server-0.1.3.dist-info/RECORD +33 -0
  7. awslabs/cdk_mcp_server/static/genai_cdk/bedrock/agent/actiongroups.md +0 -137
  8. awslabs/cdk_mcp_server/static/genai_cdk/bedrock/agent/alias.md +0 -39
  9. awslabs/cdk_mcp_server/static/genai_cdk/bedrock/agent/collaboration.md +0 -91
  10. awslabs/cdk_mcp_server/static/genai_cdk/bedrock/agent/creation.md +0 -149
  11. awslabs/cdk_mcp_server/static/genai_cdk/bedrock/agent/custom_orchestration.md +0 -74
  12. awslabs/cdk_mcp_server/static/genai_cdk/bedrock/agent/overview.md +0 -78
  13. awslabs/cdk_mcp_server/static/genai_cdk/bedrock/agent/prompt_override.md +0 -70
  14. awslabs/cdk_mcp_server/static/genai_cdk/bedrock/bedrockguardrails.md +0 -188
  15. awslabs/cdk_mcp_server/static/genai_cdk/bedrock/knowledgebases/chunking.md +0 -137
  16. awslabs/cdk_mcp_server/static/genai_cdk/bedrock/knowledgebases/datasources.md +0 -225
  17. awslabs/cdk_mcp_server/static/genai_cdk/bedrock/knowledgebases/kendra.md +0 -81
  18. awslabs/cdk_mcp_server/static/genai_cdk/bedrock/knowledgebases/overview.md +0 -116
  19. awslabs/cdk_mcp_server/static/genai_cdk/bedrock/knowledgebases/parsing.md +0 -36
  20. awslabs/cdk_mcp_server/static/genai_cdk/bedrock/knowledgebases/transformation.md +0 -30
  21. awslabs/cdk_mcp_server/static/genai_cdk/bedrock/knowledgebases/vector/aurora.md +0 -185
  22. awslabs/cdk_mcp_server/static/genai_cdk/bedrock/knowledgebases/vector/creation.md +0 -80
  23. awslabs/cdk_mcp_server/static/genai_cdk/bedrock/knowledgebases/vector/opensearch.md +0 -56
  24. awslabs/cdk_mcp_server/static/genai_cdk/bedrock/knowledgebases/vector/pinecone.md +0 -66
  25. awslabs/cdk_mcp_server/static/genai_cdk/bedrock/profiles.md +0 -153
  26. awslabs/cdk_mcp_server/static/genai_cdk/opensearch-vectorindex/overview.md +0 -135
  27. awslabs/cdk_mcp_server/static/genai_cdk/opensearchserverless/overview.md +0 -17
  28. awslabs_cdk_mcp_server-0.1.1.dist-info/RECORD +0 -54
  29. {awslabs_cdk_mcp_server-0.1.1.dist-info → awslabs_cdk_mcp_server-0.1.3.dist-info}/WHEEL +0 -0
  30. {awslabs_cdk_mcp_server-0.1.1.dist-info → awslabs_cdk_mcp_server-0.1.3.dist-info}/entry_points.txt +0 -0
  31. {awslabs_cdk_mcp_server-0.1.1.dist-info → awslabs_cdk_mcp_server-0.1.3.dist-info}/licenses/LICENSE +0 -0
  32. {awslabs_cdk_mcp_server-0.1.1.dist-info → awslabs_cdk_mcp_server-0.1.3.dist-info}/licenses/NOTICE +0 -0
@@ -9,429 +9,588 @@
9
9
  # OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions
10
10
  # and limitations under the License.
11
11
 
12
- """GenAI CDK constructs static content loader."""
12
+ """GitHub-based GenAI CDK constructs content loader."""
13
13
 
14
+ import httpx
14
15
  import logging
15
- import os
16
- from awslabs.cdk_mcp_server.data.construct_descriptions import get_construct_descriptions
17
- from enum import Enum
16
+ import re
17
+ from datetime import datetime, timedelta
18
18
  from typing import Any, Dict, List, Optional
19
19
 
20
20
 
21
21
  # Set up logging
22
22
  logger = logging.getLogger(__name__)
23
23
 
24
+ # Constants
25
+ GITHUB_API_URL = 'https://api.github.com'
26
+ GITHUB_RAW_CONTENT_URL = 'https://raw.githubusercontent.com'
27
+ REPO_OWNER = 'awslabs'
28
+ REPO_NAME = 'generative-ai-cdk-constructs'
29
+ BASE_PATH = 'src/cdk-lib'
30
+ CACHE_TTL = timedelta(hours=24) # Cache for 24 hours
24
31
 
25
- class ConstructType(str, Enum):
26
- """GenAI CDK construct types."""
32
+ # Simple caches
33
+ _readme_cache = {} # Cache for README.md content, keyed by path
34
+ _sections_cache = {} # Cache for extracted sections, keyed by path
35
+ _constructs_cache = {} # Cache for constructs list
36
+ _last_constructs_fetch = None # Last time constructs were fetched
27
37
 
28
- BEDROCK = 'bedrock'
29
- OPENSEARCH_SERVERLESS = 'opensearchserverless'
30
- OPENSEARCH_VECTOR_INDEX = 'opensearch-vectorindex'
31
38
 
32
-
33
- def get_construct_types() -> List[str]:
34
- """Get a list of available construct types."""
35
- return [ct.value for ct in ConstructType]
36
-
37
-
38
- def get_construct_map() -> Dict[str, str]:
39
- """Get a dictionary mapping construct types to their descriptions."""
40
- return {
41
- 'bedrock': 'Amazon Bedrock constructs for agents, knowledge bases, and more',
42
- 'opensearchserverless': 'Amazon OpenSearch Serverless constructs for vector search',
43
- 'opensearch-vectorindex': 'Amazon OpenSearch vector index constructs',
44
- }
45
-
46
-
47
- def get_genai_cdk_overview(construct_type: str = '') -> str:
48
- """Get an overview of GenAI CDK constructs.
39
+ async def fetch_readme(
40
+ construct_type: str, construct_name: Optional[str] = None
41
+ ) -> Dict[str, Any]:
42
+ """Fetch README.md content directly from GitHub.
49
43
 
50
44
  Args:
51
- construct_type: Optional construct type to get overview for.
52
- If empty, returns the best practices.
45
+ construct_type: Top-level directory (e.g., 'bedrock')
46
+ construct_name: Optional subdirectory (e.g., 'agents')
53
47
 
54
48
  Returns:
55
- The overview content as a string.
49
+ Dictionary with README content and metadata
56
50
  """
57
- # Normalize construct type
58
- construct_type = construct_type.lower()
59
-
60
- # Validate construct type
61
- if construct_type not in get_construct_types():
62
- construct_list = '\n'.join([f'- {t}: {desc}' for t, desc in get_construct_map().items()])
63
- return f"# GenAI CDK Constructs\n\nConstruct type '{construct_type}' not found. Available types:\n\n{construct_list}"
64
-
65
- # Get overview file
66
- file_path = os.path.join(
67
- os.path.dirname(os.path.dirname(__file__)), # Fix path to use parent directory
68
- 'static',
69
- 'genai_cdk',
70
- construct_type,
71
- 'overview.md',
51
+ # Build the path
52
+ path_parts = [construct_type]
53
+ if construct_name:
54
+ path_parts.append(construct_name)
55
+
56
+ path = '/'.join(path_parts)
57
+ cache_key = f'{construct_type}/{construct_name}' if construct_name else construct_type
58
+
59
+ # Check cache first
60
+ if (
61
+ cache_key in _readme_cache
62
+ and datetime.now() - _readme_cache[cache_key]['timestamp'] < CACHE_TTL
63
+ ):
64
+ logger.debug(f'Using cached README for {path}')
65
+ return _readme_cache[cache_key]['data']
66
+
67
+ # Fetch from GitHub
68
+ readme_url = (
69
+ f'{GITHUB_RAW_CONTENT_URL}/{REPO_OWNER}/{REPO_NAME}/main/{BASE_PATH}/{path}/README.md'
72
70
  )
71
+ logger.info(f'Fetching README from {readme_url}')
72
+
73
73
  try:
74
- with open(file_path, 'r', encoding='utf-8') as f:
75
- return f.read()
76
- except FileNotFoundError:
77
- return f"Error: Overview file for '{construct_type}' not found."
74
+ async with httpx.AsyncClient() as client:
75
+ response = await client.get(readme_url)
76
+
77
+ if response.status_code != 200:
78
+ logger.warning(f'Failed to fetch README for {path}: HTTP {response.status_code}')
79
+ return {
80
+ 'error': f'Failed to fetch README for {path}: HTTP {response.status_code}',
81
+ 'status_code': response.status_code,
82
+ }
83
+
84
+ content = response.text
85
+
86
+ # Update cache
87
+ result = {
88
+ 'content': content,
89
+ 'path': path,
90
+ 'url': readme_url,
91
+ 'status': 'success',
92
+ }
78
93
 
94
+ _readme_cache[cache_key] = {
95
+ 'timestamp': datetime.now(),
96
+ 'data': result,
97
+ }
79
98
 
80
- def list_available_sections(construct_type: str, construct_name: str) -> List[str]:
81
- """List available sections for a specific construct.
99
+ return result
100
+ except Exception as e:
101
+ logger.error(f'Error fetching README for {path}: {str(e)}')
102
+ return {
103
+ 'error': f'Error fetching README: {str(e)}',
104
+ 'status': 'error',
105
+ }
82
106
 
83
- Args:
84
- construct_type: The construct type (e.g., 'bedrock')
85
- construct_name: The name of the construct (e.g., 'agent', 'knowledgebases')
86
107
 
87
- Returns:
88
- List of available sections.
108
+ def extract_sections(content: str) -> Dict[str, str]:
109
+ """Extract sections from README.md content based on level 2 headings (##) only.
110
+
111
+ Returns a dictionary mapping section names to their content.
112
+ Uses URL encoding for section names to handle special characters.
89
113
  """
90
- sections = []
91
- base_path = os.path.join(
92
- os.path.dirname(os.path.dirname(__file__)), # Fix path to use parent directory
93
- 'static',
94
- 'genai_cdk',
95
- construct_type,
96
- construct_name,
97
- )
114
+ # Find all level 2 headings (## Heading)
115
+ headings = re.finditer(r'^##\s+(.+?)$', content, re.MULTILINE)
98
116
 
99
- if not os.path.exists(base_path):
100
- return sections
117
+ sections = {}
118
+ section_starts = []
101
119
 
102
- # Walk through the directory structure
103
- for root, dirs, files in os.walk(base_path):
104
- rel_path = os.path.relpath(root, base_path)
120
+ # Import here to avoid circular imports
105
121
 
106
- for file in files:
107
- if file.endswith('.md') and file != 'overview.md':
108
- section_name = file[:-3] # Remove .md extension
122
+ # Collect all level 2 headings with their positions
123
+ for match in headings:
124
+ heading_text = match.group(1).strip()
125
+ # Store URL-safe version for proper matching later
126
+ section_starts.append((match.start(), heading_text))
109
127
 
110
- # For files in the base directory
111
- if rel_path == '.':
112
- sections.append(section_name)
113
- else:
114
- # For files in subdirectories
115
- if rel_path != '.':
116
- section_path = os.path.join(rel_path, section_name)
117
- # Replace backslashes with forward slashes for consistency
118
- section_path = section_path.replace('\\', '/')
119
- sections.append(section_path)
128
+ # Sort by position
129
+ section_starts.sort()
130
+
131
+ # Extract content between headings
132
+ for i, (start_pos, heading) in enumerate(section_starts):
133
+ # Find the end of this section (start of next level 2 heading or end of file)
134
+ end_pos = section_starts[i + 1][0] if i < len(section_starts) - 1 else len(content)
135
+
136
+ # Extract the section content including the heading
137
+ section_content = content[start_pos:end_pos].strip()
138
+
139
+ # Use the heading text as the key
140
+ sections[heading] = section_content
120
141
 
121
142
  return sections
122
143
 
123
144
 
124
- def get_genai_cdk_construct_section(construct_type: str, construct_name: str, section: str) -> str:
125
- """Get a specific section of documentation for a GenAI CDK construct.
145
+ async def get_section(
146
+ construct_type: str, construct_name: str, section_name: str
147
+ ) -> Dict[str, Any]:
148
+ """Get a specific section from a README.md file.
126
149
 
127
150
  Args:
128
- construct_type: The construct type (e.g., 'bedrock')
129
- construct_name: The name of the construct (e.g., 'agent', 'knowledgebases')
130
- section: The section name (e.g., 'actiongroups', 'vector/opensearch')
151
+ construct_type: Top-level directory (e.g., 'bedrock')
152
+ construct_name: Subdirectory (e.g., 'agents')
153
+ section_name: Name of the section to extract
131
154
 
132
155
  Returns:
133
- The section documentation as a string.
156
+ Dictionary with section content and metadata
134
157
  """
135
- # Normalize inputs
136
- construct_type = construct_type.lower()
137
- construct_name_lower = construct_name.lower()
138
-
139
- # Special handling for Agent_* and Knowledgebases_* constructs
140
- if construct_name_lower.startswith('agent_'):
141
- # Convert Agent_actiongroups to agent/actiongroups
142
- construct_name_lower = 'agent'
143
- section = construct_name_lower.split('_', 1)[1]
144
- elif construct_name_lower.startswith('knowledgebases_'):
145
- # Convert Knowledgebases_vector_opensearch to knowledgebases/vector/opensearch
146
- parts = construct_name_lower.split('_', 1)
147
- if len(parts) > 1:
148
- construct_name_lower = parts[0]
149
- # Handle nested paths with underscores (e.g., vector_opensearch -> vector/opensearch)
150
- section_parts = parts[1].split('_')
151
- if len(section_parts) > 1 and section_parts[0] == 'vector':
152
- # Special case for vector/* sections which are in a nested directory
153
- section = f'vector/{section_parts[1]}'
154
- else:
155
- section = parts[1]
156
-
157
- # Validate construct type
158
- if construct_type not in get_construct_types():
159
- return f"Error: Construct type '{construct_type}' not found."
160
-
161
- # Handle nested sections (e.g., vector/opensearch)
162
- if '/' in section:
163
- section_parts = section.split('/')
164
- file_path = (
165
- os.path.join(
166
- os.path.dirname(os.path.dirname(__file__)), # Fix path to use parent directory
167
- 'static',
168
- 'genai_cdk',
169
- construct_type,
170
- construct_name_lower,
171
- *section_parts,
172
- )
173
- + '.md'
174
- )
175
- else:
176
- # Regular section
177
- file_path = os.path.join(
178
- os.path.dirname(os.path.dirname(__file__)), # Fix path to use parent directory
179
- 'static',
180
- 'genai_cdk',
181
- construct_type,
182
- construct_name_lower,
183
- f'{section}.md',
184
- )
158
+ # Build cache key
159
+ path = f'{construct_type}/{construct_name}'
160
+ cache_key = path
161
+
162
+ # Check if sections are already cached
163
+ if (
164
+ cache_key in _sections_cache
165
+ and datetime.now() - _sections_cache[cache_key]['timestamp'] < CACHE_TTL
166
+ ):
167
+ sections = _sections_cache[cache_key]['data']
168
+
169
+ # Find the section (case-insensitive)
170
+ for heading, content in sections.items():
171
+ if heading.lower() == section_name.lower():
172
+ return {
173
+ 'content': content,
174
+ 'section': heading,
175
+ 'path': path,
176
+ 'status': 'success',
177
+ }
178
+
179
+ # Section not found in cache
180
+ return {
181
+ 'error': f"Section '{section_name}' not found in {path}",
182
+ 'status': 'not_found',
183
+ }
184
+
185
+ # Fetch the README
186
+ readme_result = await fetch_readme(construct_type, construct_name)
187
+
188
+ if 'error' in readme_result:
189
+ # Return error result with consistent path
190
+ return {
191
+ 'error': readme_result['error'],
192
+ 'path': path,
193
+ 'status': 'error',
194
+ }
195
+
196
+ # Extract sections
197
+ sections = extract_sections(readme_result['content'])
198
+
199
+ # Cache the sections
200
+ _sections_cache[cache_key] = {
201
+ 'timestamp': datetime.now(),
202
+ 'data': sections,
203
+ }
185
204
 
186
- try:
187
- with open(file_path, 'r', encoding='utf-8') as f:
188
- return f.read()
189
- except FileNotFoundError:
190
- return (
191
- f"Error: Section '{section}' for '{construct_name}' in '{construct_type}' not found."
192
- )
205
+ # Find the section using URL decoding and case-insensitive comparison
206
+ import urllib.parse
207
+
208
+ decoded_section_name = urllib.parse.unquote(section_name)
209
+ logger.info(f"Looking for section '{decoded_section_name}' in {path}")
210
+ logger.info(f'Available sections: {", ".join(sections.keys())}')
211
+
212
+ # First try direct match after decoding
213
+ for heading, content in sections.items():
214
+ if heading.lower() == decoded_section_name.lower():
215
+ return {
216
+ 'content': content,
217
+ 'section': heading,
218
+ 'path': path,
219
+ 'status': 'success',
220
+ }
221
+
222
+ # Section not found
223
+ logger.warning(f"Section '{section_name}' not found in {path}")
224
+ return {
225
+ 'error': f"Section '{section_name}' not found in {path}",
226
+ 'status': 'not_found',
227
+ }
193
228
 
194
229
 
195
- def get_genai_cdk_construct(construct_type: str, construct_name: str) -> str:
196
- """Get documentation for a specific GenAI CDK construct.
230
+ async def list_sections(construct_type: str, construct_name: str) -> Dict[str, Any]:
231
+ """List available sections in a README.md file.
197
232
 
198
233
  Args:
199
- construct_type: The construct type (e.g., 'bedrock')
200
- construct_name: The name of the construct (e.g., 'Agent')
234
+ construct_type: Top-level directory (e.g., 'bedrock')
235
+ construct_name: Subdirectory (e.g., 'agents')
201
236
 
202
237
  Returns:
203
- The construct documentation as a string.
238
+ Dictionary with list of sections and metadata
204
239
  """
205
- # Normalize inputs
206
- construct_type = construct_type.lower()
207
- construct_name_lower = construct_name.lower()
208
-
209
- # Special handling for Agent_* and Knowledgebases_* constructs
210
- if construct_name_lower.startswith('agent_'):
211
- # For Agent_actiongroups, redirect to agent/actiongroups section
212
- parent = 'agent'
213
- child = construct_name_lower.split('_', 1)[1]
214
- return get_genai_cdk_construct_section(construct_type, parent, child)
215
- elif construct_name_lower.startswith('knowledgebases_'):
216
- # For Knowledgebases_vector_opensearch, redirect to knowledgebases/vector/opensearch section
217
- parts = construct_name_lower.split('_', 1)
218
- if len(parts) > 1:
219
- parent = parts[0]
220
- # Handle nested paths with underscores (e.g., vector_opensearch -> vector/opensearch)
221
- section_parts = parts[1].split('_')
222
- if len(section_parts) > 1 and section_parts[0] == 'vector':
223
- # Special case for vector/* sections which are in a nested directory
224
- child = f'vector/{section_parts[1]}'
225
- else:
226
- child = parts[1]
227
- return get_genai_cdk_construct_section(construct_type, parent, child)
228
-
229
- # Special handling for agent and knowledgebases
230
- if construct_name_lower in ['agent', 'knowledgebases']:
231
- # For these special cases, return an overview or index of available sections
232
- base_path = os.path.join(
233
- os.path.dirname(os.path.dirname(__file__)), # Fix path to use parent directory
234
- 'static',
235
- 'genai_cdk',
236
- construct_type,
237
- construct_name_lower,
238
- )
239
-
240
- # Check if directory exists
241
- if not os.path.exists(base_path):
242
- return f"Error: Documentation for '{construct_name}' in '{construct_type}' not found."
243
-
244
- # List files in directory
245
- sections = []
246
- for file_name in os.listdir(base_path):
247
- if file_name.endswith('.md') and file_name != 'overview.md':
248
- sections.append(file_name[:-3]) # Remove .md extension
249
-
250
- # Also check subdirectories
251
- for root, dirs, files in os.walk(base_path):
252
- if root != base_path: # Skip the base directory
253
- rel_path = os.path.relpath(root, base_path)
254
- for file_name in files:
255
- if file_name.endswith('.md'):
256
- section_path = os.path.join(rel_path, file_name[:-3])
257
- section_path = section_path.replace('\\', '/')
258
- sections.append(section_path)
259
-
260
- result = f'# {construct_name.capitalize()} Documentation\n\n'
261
- result += 'This documentation is split into sections for easier consumption.\n\n'
262
- result += '## Available Sections\n\n'
263
-
264
- for section in sorted(sections):
265
- result += f'- [{section}](genai-cdk-constructs://{construct_type}/{construct_name_lower}/{section})\n'
266
-
267
- return result
268
-
269
- # Special handling for key constructs
270
- key_construct_mapping = {
271
- 'agent': 'agent',
272
- 'agents': 'agent',
273
- 'knowledgebase': 'knowledgebases',
274
- 'knowledgebases': 'knowledgebases',
275
- 'knowledge-base': 'knowledgebases',
276
- 'knowledge-bases': 'knowledgebases',
277
- 'agentactiongroup': 'agent/actiongroups',
278
- 'action-group': 'agent/actiongroups',
279
- 'actiongroup': 'agent/actiongroups',
280
- 'agentalias': 'agent/alias',
281
- 'guardrail': 'bedrockguardrails',
282
- 'guardrails': 'bedrockguardrails',
283
- 'bedrock-guardrails': 'bedrockguardrails',
240
+ # Build cache key
241
+ path = f'{construct_type}/{construct_name}'
242
+ cache_key = path
243
+
244
+ # Check if sections are already cached
245
+ if (
246
+ cache_key in _sections_cache
247
+ and datetime.now() - _sections_cache[cache_key]['timestamp'] < CACHE_TTL
248
+ ):
249
+ sections = _sections_cache[cache_key]['data']
250
+ return {
251
+ 'sections': list(sections.keys()),
252
+ 'path': path,
253
+ 'status': 'success',
254
+ }
255
+
256
+ # Fetch the README
257
+ readme_result = await fetch_readme(construct_type, construct_name)
258
+
259
+ if 'error' in readme_result:
260
+ # Return empty sections on error, but maintain successful status
261
+ return {
262
+ 'sections': [],
263
+ 'path': path,
264
+ 'status': 'success',
265
+ }
266
+
267
+ # Extract sections
268
+ sections = extract_sections(readme_result['content'])
269
+
270
+ # Cache the sections
271
+ _sections_cache[cache_key] = {
272
+ 'timestamp': datetime.now(),
273
+ 'data': sections,
284
274
  }
285
275
 
286
- # Normalize construct name
287
- if construct_name_lower in key_construct_mapping:
288
- mapped_name = key_construct_mapping[construct_name_lower]
289
- if '/' in mapped_name:
290
- # Handle redirects to sections
291
- parent, section = mapped_name.split('/', 1)
292
- return get_genai_cdk_construct_section(construct_type, parent, section)
293
- else:
294
- construct_name_lower = mapped_name
295
-
296
- # Validate construct type
297
- if construct_type not in get_construct_types():
298
- construct_list = '\n'.join([f'- {t}: {desc}' for t, desc in get_construct_map().items()])
299
- return f"# GenAI CDK Constructs\n\nConstruct type '{construct_type}' not found. Available types:\n\n{construct_list}"
300
-
301
- # Get construct file (flat structure)
302
- file_path = os.path.join(
303
- os.path.dirname(os.path.dirname(__file__)), # Fix path to use parent directory
304
- 'static',
305
- 'genai_cdk',
306
- construct_type,
307
- f'{construct_name_lower}.md',
308
- )
309
- try:
310
- with open(file_path, 'r', encoding='utf-8') as f:
311
- return f.read()
312
- except FileNotFoundError:
313
- # Try to see if this is a directory with an overview.md file
314
- overview_path = os.path.join(
315
- os.path.dirname(os.path.dirname(__file__)),
316
- 'static',
317
- 'genai_cdk',
318
- construct_type,
319
- construct_name_lower,
320
- 'overview.md',
321
- )
322
- try:
323
- with open(overview_path, 'r', encoding='utf-8') as f:
324
- return f.read()
325
- except FileNotFoundError:
326
- return f"Error: Documentation for '{construct_name}' in '{construct_type}' not found."
276
+ return {
277
+ 'sections': list(sections.keys()),
278
+ 'path': path,
279
+ 'status': 'success',
280
+ }
327
281
 
328
282
 
329
- def list_available_constructs(construct_type: Optional[str] = None) -> List[Dict[str, Any]]:
330
- """List available constructs.
283
+ async def get_construct_overview(construct_type: str) -> Dict[str, Any]:
284
+ """Get overview documentation for a construct type.
331
285
 
332
286
  Args:
333
- construct_type: Optional construct type to filter by.
287
+ construct_type: Top-level directory (e.g., 'bedrock')
334
288
 
335
289
  Returns:
336
- List of constructs with name, type, and description.
290
+ Dictionary with README content for the construct type
337
291
  """
338
- constructs = []
292
+ return await fetch_readme(construct_type)
339
293
 
340
- # Determine which construct types to search
341
- if construct_type is not None:
342
- construct_types = [construct_type.lower()]
343
- else:
344
- construct_types = get_construct_types()
345
294
 
346
- # For each construct type, list files in the directory
347
- for ct in construct_types:
348
- if ct not in get_construct_types():
349
- continue
295
+ async def fetch_bedrock_subdirectories() -> List[Dict[str, Any]]:
296
+ """Fetch subdirectories specifically for the bedrock directory.
350
297
 
351
- # Get directory path - fix path to use parent directory
352
- dir_path = os.path.join(
353
- os.path.dirname(os.path.dirname(__file__)), 'static', 'genai_cdk', ct
354
- )
298
+ Returns:
299
+ List of subdirectory information
300
+ """
301
+ try:
302
+ async with httpx.AsyncClient() as client:
303
+ response = await client.get(
304
+ f'{GITHUB_API_URL}/repos/{REPO_OWNER}/{REPO_NAME}/contents/{BASE_PATH}/bedrock',
305
+ headers={'Accept': 'application/vnd.github.v3+json'},
306
+ )
355
307
 
356
- # Skip if directory doesn't exist
357
- if not os.path.exists(dir_path):
358
- continue
308
+ if response.status_code != 200:
309
+ logger.warning(
310
+ f'Failed to fetch bedrock subdirectories: HTTP {response.status_code}'
311
+ )
312
+ return []
313
+
314
+ contents = response.json()
315
+
316
+ # Filter directories only
317
+ subdirs = []
318
+ for item in contents:
319
+ if item['type'] == 'dir':
320
+ subdir_name = item['name']
321
+
322
+ # Get README for this subdirectory if available
323
+ readme_result = await fetch_readme('bedrock', subdir_name)
324
+
325
+ # Default values
326
+ title = subdir_name
327
+ description = f'Bedrock {subdir_name.capitalize()} constructs'
328
+
329
+ # Extract better title/description if README exists
330
+ if 'error' not in readme_result:
331
+ readme_content = readme_result['content']
332
+
333
+ # Use a safer approach to extract title - find first # heading
334
+ lines = readme_content.split('\n')
335
+ for line in lines:
336
+ if line.startswith('# '):
337
+ title = line.replace('# ', '').strip()
338
+ break
339
+
340
+ # Extract description from content after first heading and before next heading
341
+ # or stability banner
342
+ description_text = ''
343
+ capture_description = False
344
+ for line in lines:
345
+ if line.startswith('# '):
346
+ capture_description = True
347
+ continue
348
+ if capture_description and (
349
+ line.startswith('#') or line.startswith('<!--BEGIN')
350
+ ):
351
+ break
352
+ if capture_description and line.strip():
353
+ description_text += line.strip() + ' '
354
+
355
+ if description_text:
356
+ # Clean up and truncate description
357
+ description_text = description_text.strip()
358
+ # Take first sentence or up to 150 chars
359
+ description = description_text.split('.')[0][:150]
360
+ if len(description) < len(description_text):
361
+ description += '...'
362
+
363
+ subdirs.append(
364
+ {
365
+ 'name': title,
366
+ 'path': f'bedrock/{subdir_name}',
367
+ 'url': item['html_url'],
368
+ 'description': description,
369
+ }
370
+ )
359
371
 
360
- # Process files in the main directory
361
- process_directory_files(dir_path, ct, constructs)
372
+ return subdirs
373
+ except Exception as e:
374
+ logger.error(f'Error fetching bedrock subdirectories: {str(e)}')
375
+ return []
362
376
 
363
- # Process subdirectories recursively
364
- for root, dirs, files in os.walk(dir_path):
365
- # Skip the main directory as it's already processed
366
- if root == dir_path:
367
- continue
368
377
 
369
- # Get the relative path from the main directory
370
- rel_path = os.path.relpath(root, dir_path)
371
- # Use the relative path as the parent
372
- process_directory_files(root, ct, constructs, parent=rel_path.replace(os.sep, '_'))
378
+ async def fetch_repo_structure() -> Dict[str, Any]:
379
+ """Fetch repository structure from GitHub API.
373
380
 
374
- return constructs
381
+ Returns:
382
+ Dictionary with repository structure information
383
+ """
384
+ global _constructs_cache, _last_constructs_fetch
375
385
 
386
+ # Check if we've fetched recently
387
+ if _last_constructs_fetch and datetime.now() - _last_constructs_fetch < CACHE_TTL:
388
+ logger.debug('Using cached repo structure')
389
+ return _constructs_cache
390
+
391
+ try:
392
+ # Fetch top-level directories
393
+ async with httpx.AsyncClient() as client:
394
+ response = await client.get(
395
+ f'{GITHUB_API_URL}/repos/{REPO_OWNER}/{REPO_NAME}/contents/{BASE_PATH}',
396
+ headers={'Accept': 'application/vnd.github.v3+json'},
397
+ )
376
398
 
377
- def process_directory_files(
378
- dir_path: str,
379
- construct_type: str,
380
- constructs: List[Dict[str, Any]],
381
- parent: Optional[str] = None,
382
- ):
383
- """Process files in a directory and add them to the constructs list.
399
+ if response.status_code != 200:
400
+ logger.warning(f'Failed to fetch repo structure: HTTP {response.status_code}')
401
+ return {'error': 'Failed to fetch repository structure'}
402
+
403
+ contents = response.json()
404
+
405
+ # Filter directories only
406
+ directories = [item for item in contents if item['type'] == 'dir']
407
+
408
+ # For each directory, get its README.md if available
409
+ construct_types = {}
410
+ for dir_info in directories:
411
+ dir_name = dir_info['name']
412
+
413
+ # Initialize default values first
414
+ title = dir_name
415
+ description = f'AWS {dir_name.capitalize()} constructs'
416
+
417
+ # Then fetch and potentially override with better data
418
+ readme_result = await fetch_readme(dir_name)
419
+ if 'error' not in readme_result:
420
+ # Try to extract title and description from README content using markdown parsing
421
+ readme_content = readme_result['content']
422
+
423
+ # Use a safer approach to extract title - find first # heading
424
+ lines = readme_content.split('\n')
425
+ for line in lines:
426
+ if line.startswith('# '):
427
+ title = line.replace('# ', '').strip()
428
+ break
429
+
430
+ # Extract description from content after first heading and before next heading
431
+ # or stability banner
432
+ description_text = ''
433
+ capture_description = False
434
+ for line in lines:
435
+ if line.startswith('# '):
436
+ capture_description = True
437
+ continue
438
+ if capture_description and (
439
+ line.startswith('#') or line.startswith('<!--BEGIN')
440
+ ):
441
+ break
442
+ if capture_description and line.strip():
443
+ description_text += line.strip() + ' '
444
+
445
+ if description_text:
446
+ # Clean up and truncate description
447
+ description_text = description_text.strip()
448
+ # Take first sentence or up to 150 chars
449
+ description = description_text.split('.')[0][:150]
450
+ if len(description) < len(description_text):
451
+ description += '...'
452
+
453
+ # Store in construct types
454
+ construct_types[dir_name] = {
455
+ 'name': title,
456
+ 'description': description,
457
+ 'path': dir_info['path'],
458
+ 'url': dir_info['html_url'],
459
+ }
460
+
461
+ # Special case for bedrock: fetch its subdirectories
462
+ if 'bedrock' in construct_types:
463
+ bedrock_subdirs = await fetch_bedrock_subdirectories()
464
+ if bedrock_subdirs:
465
+ construct_types['bedrock']['subdirectories'] = bedrock_subdirs
466
+
467
+ # Update cache
468
+ _constructs_cache = {'construct_types': construct_types}
469
+ _last_constructs_fetch = datetime.now()
470
+
471
+ return _constructs_cache
472
+ except Exception as e:
473
+ logger.error(f'Error fetching repo structure: {str(e)}')
474
+ return {'error': f'Error fetching repository structure: {str(e)}'}
475
+
476
+
477
+ async def list_available_constructs(construct_type: Optional[str] = None) -> List[Dict[str, Any]]:
478
+ """List available constructs from GitHub.
384
479
 
385
480
  Args:
386
- dir_path: Path to the directory
387
- construct_type: Type of construct
388
- constructs: List to add constructs to
389
- parent: Optional parent directory name
481
+ construct_type: Optional construct type to filter by
482
+
483
+ Returns:
484
+ List of constructs with name, type, and description
390
485
  """
391
- # List files in directory
392
- for file_name in os.listdir(dir_path):
393
- # Skip overview file, directories, and non-markdown files
394
- if (
395
- file_name == 'overview.md'
396
- or not file_name.endswith('.md')
397
- or os.path.isdir(os.path.join(dir_path, file_name))
398
- ):
399
- continue
486
+ # Get repository structure
487
+ repo_structure = await fetch_repo_structure()
400
488
 
401
- # Extract construct name from file name
402
- base_name = file_name[:-3]
489
+ if 'error' in repo_structure:
490
+ logger.error(f'Error in list_available_constructs: {repo_structure["error"]}')
491
+ return []
403
492
 
404
- # Format the construct name
405
- if parent:
406
- construct_name = f'{parent}_{base_name}'
407
- else:
408
- construct_name = base_name
493
+ construct_types = repo_structure.get('construct_types', {})
409
494
 
410
- display_name = construct_name.capitalize()
495
+ # Get available types
496
+ available_types = list(construct_types.keys())
411
497
 
412
- # Define file_path here, before it's used
413
- file_path = os.path.join(dir_path, file_name)
498
+ # If construct type is provided, filter by it
499
+ if construct_type:
500
+ if construct_type not in available_types:
501
+ logger.warning(
502
+ f"Construct type '{construct_type}' not found. Available types: {', '.join(available_types)}"
503
+ )
504
+ return []
505
+ filter_types = [construct_type]
506
+ else:
507
+ filter_types = available_types
414
508
 
415
- # Get description from fixed mapping or use default
416
- descriptions = get_construct_descriptions()
417
- description = descriptions.get(display_name, '')
509
+ # Prepare result list
510
+ constructs = []
418
511
 
419
- # If no fixed description, fall back to current behavior
420
- if not description:
421
- try:
422
- with open(file_path, 'r', encoding='utf-8') as f:
423
- first_line = f.readline().strip()
424
- description = (
425
- first_line[1:].strip() if first_line.startswith('#') else display_name
426
- )
427
- except Exception:
428
- description = f'A {construct_type} construct.'
512
+ # For each construct type
513
+ for ct in filter_types:
514
+ # Get README for this construct type
515
+ readme_result = await fetch_readme(ct)
429
516
 
430
- # Add to list
517
+ if 'error' in readme_result:
518
+ continue
519
+
520
+ # Extract sections from README
521
+ sections = extract_sections(readme_result['content'])
522
+
523
+ # Add construct types as top-level constructs
431
524
  constructs.append(
432
525
  {
433
- 'name': display_name,
434
- 'type': construct_type,
435
- 'description': description,
526
+ 'name': ct.capitalize(),
527
+ 'type': ct,
528
+ 'description': construct_types[ct]['description'],
436
529
  }
437
530
  )
531
+
532
+ # Add sections as constructs
533
+ for section_name in sections:
534
+ # Build a construct name from section
535
+ name_parts = [part.capitalize() for part in section_name.split()]
536
+ if len(name_parts) > 1:
537
+ construct_name = f'{name_parts[0]}{"".join(name_parts[1:])}'
538
+ else:
539
+ construct_name = name_parts[0]
540
+
541
+ # Build description from the first line of the section
542
+ section_content = sections[section_name]
543
+ first_line = section_content.split('\n')[0].strip('# ')
544
+ description = first_line
545
+
546
+ # Add to constructs list
547
+ constructs.append(
548
+ {
549
+ 'name': construct_name,
550
+ 'type': ct,
551
+ 'description': description,
552
+ }
553
+ )
554
+
555
+ # Add bedrock subdirectories as constructs
556
+ if ct == 'bedrock' and 'subdirectories' in construct_types[ct]:
557
+ for subdir in construct_types[ct]['subdirectories']:
558
+ # Add the subdirectory as a construct
559
+ subdir_name = subdir['name']
560
+ constructs.append(
561
+ {
562
+ 'name': f'{subdir_name}',
563
+ 'type': 'bedrock',
564
+ 'description': subdir['description'],
565
+ }
566
+ )
567
+
568
+ # Also fetch README for this subdirectory to extract sections
569
+ subdir_raw_name = subdir['path'].split('/')[-1] # Get the raw name from path
570
+ subdir_readme = await fetch_readme('bedrock', subdir_raw_name)
571
+ if 'error' not in subdir_readme:
572
+ subdir_sections = extract_sections(subdir_readme['content'])
573
+
574
+ # Add sections from subdirectory README
575
+ for section_name in subdir_sections:
576
+ # Similar logic to build construct name and description
577
+ name_parts = [part.capitalize() for part in section_name.split()]
578
+ if len(name_parts) > 1:
579
+ section_construct_name = f'{name_parts[0]}{"".join(name_parts[1:])}'
580
+ else:
581
+ section_construct_name = name_parts[0]
582
+
583
+ section_content = subdir_sections[section_name]
584
+ first_line = section_content.split('\n')[0].strip('# ')
585
+ description = first_line
586
+
587
+ # Add to constructs list with special naming to indicate subdirectory
588
+ constructs.append(
589
+ {
590
+ 'name': f'{subdir_name}{section_construct_name}',
591
+ 'type': 'bedrock',
592
+ 'description': description,
593
+ }
594
+ )
595
+
596
+ return constructs