awslabs.ccapi-mcp-server 1.0.1__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.ccapi-mcp-server might be problematic. Click here for more details.

Files changed (28) hide show
  1. awslabs/__init__.py +16 -0
  2. awslabs/ccapi_mcp_server/__init__.py +17 -0
  3. awslabs/ccapi_mcp_server/aws_client.py +62 -0
  4. awslabs/ccapi_mcp_server/cloud_control_utils.py +120 -0
  5. awslabs/ccapi_mcp_server/context.py +37 -0
  6. awslabs/ccapi_mcp_server/errors.py +67 -0
  7. awslabs/ccapi_mcp_server/iac_generator.py +203 -0
  8. awslabs/ccapi_mcp_server/impl/__init__.py +13 -0
  9. awslabs/ccapi_mcp_server/impl/tools/__init__.py +13 -0
  10. awslabs/ccapi_mcp_server/impl/tools/explanation.py +325 -0
  11. awslabs/ccapi_mcp_server/impl/tools/infrastructure_generation.py +70 -0
  12. awslabs/ccapi_mcp_server/impl/tools/resource_operations.py +367 -0
  13. awslabs/ccapi_mcp_server/impl/tools/security_scanning.py +223 -0
  14. awslabs/ccapi_mcp_server/impl/tools/session_management.py +221 -0
  15. awslabs/ccapi_mcp_server/impl/utils/__init__.py +13 -0
  16. awslabs/ccapi_mcp_server/impl/utils/validation.py +64 -0
  17. awslabs/ccapi_mcp_server/infrastructure_generator.py +160 -0
  18. awslabs/ccapi_mcp_server/models/__init__.py +13 -0
  19. awslabs/ccapi_mcp_server/models/models.py +118 -0
  20. awslabs/ccapi_mcp_server/schema_manager.py +219 -0
  21. awslabs/ccapi_mcp_server/server.py +733 -0
  22. awslabs/ccapi_mcp_server/static/__init__.py +13 -0
  23. awslabs_ccapi_mcp_server-1.0.1.dist-info/METADATA +656 -0
  24. awslabs_ccapi_mcp_server-1.0.1.dist-info/RECORD +28 -0
  25. awslabs_ccapi_mcp_server-1.0.1.dist-info/WHEEL +4 -0
  26. awslabs_ccapi_mcp_server-1.0.1.dist-info/entry_points.txt +2 -0
  27. awslabs_ccapi_mcp_server-1.0.1.dist-info/licenses/LICENSE +175 -0
  28. awslabs_ccapi_mcp_server-1.0.1.dist-info/licenses/NOTICE +2 -0
@@ -0,0 +1,325 @@
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
+ """Explanation functionality implementation for CCAPI MCP server."""
16
+
17
+ import datetime
18
+ import uuid
19
+ from awslabs.ccapi_mcp_server.errors import ClientError
20
+ from awslabs.ccapi_mcp_server.impl.utils.validation import ensure_string
21
+ from awslabs.ccapi_mcp_server.models.models import ExplainRequest
22
+ from typing import Any
23
+
24
+
25
+ def _format_value(value: Any) -> str:
26
+ """Format any value for display."""
27
+ if isinstance(value, str):
28
+ return f'"{value[:100]}"' + ('...' if len(value) > 100 else '')
29
+ elif isinstance(value, (int, float, bool)):
30
+ return str(value)
31
+ elif isinstance(value, dict):
32
+ return f'{{dict with {len(value)} keys}}'
33
+ elif isinstance(value, list):
34
+ return f'[list with {len(value)} items]'
35
+ else:
36
+ return f'{type(value).__name__} object'
37
+
38
+
39
+ def _generate_explanation(
40
+ content: Any, context: str, operation: str, format: str, user_intent: str
41
+ ) -> str:
42
+ """Generate comprehensive explanation for any type of content."""
43
+ content_type = type(content).__name__
44
+
45
+ # Build header
46
+ if context:
47
+ header = (
48
+ f'## {context} - {operation.title()} Operation'
49
+ if operation != 'analyze'
50
+ else f'## {context} Analysis'
51
+ )
52
+ else:
53
+ header = f'## Data Analysis ({content_type})'
54
+
55
+ if user_intent:
56
+ header += f'\n\n**User Intent:** {user_intent}'
57
+
58
+ explanation = header + '\n\n'
59
+
60
+ # Handle different content types
61
+ if isinstance(content, dict):
62
+ # Check if this is security scan data
63
+ if content.get('scan_status') in ['PASSED', 'FAILED']:
64
+ explanation += _explain_security_scan(content)
65
+ else:
66
+ explanation += _explain_dict(content, format)
67
+ elif isinstance(content, list):
68
+ explanation += _explain_list(content, format)
69
+ elif isinstance(content, str):
70
+ explanation += f'**Content:** {content[:500]}{"..." if len(content) > 500 else ""}'
71
+ elif isinstance(content, (int, float, bool)):
72
+ explanation += f'**Value:** {content} ({content_type})'
73
+ else:
74
+ explanation += f'**Content Type:** {content_type}\n**Value:** {str(content)[:500]}'
75
+
76
+ # Add operation-specific notes
77
+ if operation in ['create', 'update', 'delete']:
78
+ explanation += '\n\n**Infrastructure Operation Notes:**'
79
+ explanation += '\n• This operation will modify AWS resources'
80
+ explanation += '\n• Default management tags will be applied for tracking'
81
+ explanation += '\n• Changes will be applied to the specified AWS region'
82
+
83
+ return explanation
84
+
85
+
86
+ def _explain_dict(data: dict, format: str) -> str:
87
+ """Explain dictionary content comprehensively with improved formatting."""
88
+ property_names = [key for key in data.keys() if not key.startswith('_')]
89
+ explanation = f'### 📋 Configuration Summary: {len(property_names)} properties\n'
90
+ explanation += f'**Properties:** {", ".join(f"`{name}`" for name in property_names)}\n\n'
91
+
92
+ for key, value in data.items():
93
+ if key.startswith('_'):
94
+ continue
95
+
96
+ if key == 'Tags' and isinstance(value, list):
97
+ # Special handling for AWS tags
98
+ explanation += f'**🏷️ {key}:** ({len(value)} tags)\n'
99
+ default_tags = []
100
+ user_tags = []
101
+
102
+ for tag in value:
103
+ if isinstance(tag, dict):
104
+ tag_key = tag.get('Key', '')
105
+ tag_value = tag.get('Value', '')
106
+ if tag_key in ['MANAGED_BY', 'MCP_SERVER_SOURCE_CODE', 'MCP_SERVER_VERSION']:
107
+ default_tags.append(f' 🔧 {tag_key}: `{tag_value}` (Default)')
108
+ else:
109
+ user_tags.append(f' ✨ {tag_key}: `{tag_value}`')
110
+
111
+ if user_tags:
112
+ explanation += '\n'.join(user_tags) + '\n'
113
+ if default_tags:
114
+ explanation += '\n'.join(default_tags) + '\n'
115
+
116
+ elif isinstance(value, dict):
117
+ explanation += f'**📄 {key}:** ({len(value)} properties)\n'
118
+ if format == 'detailed':
119
+ for sub_key, sub_value in list(value.items())[:5]:
120
+ if isinstance(sub_value, list) and sub_key == 'Statement':
121
+ # Special handling for policy statements
122
+ explanation += f' • **{sub_key}:** ({len(sub_value)} statements)\n'
123
+ for i, stmt in enumerate(sub_value[:3]):
124
+ if isinstance(stmt, dict):
125
+ sid = stmt.get('Sid', f'Statement {i + 1}')
126
+ effect = stmt.get('Effect', 'Unknown')
127
+ action = stmt.get('Action', 'Unknown')
128
+ principal = stmt.get('Principal', 'Unknown')
129
+ emoji = '✅' if effect == 'Allow' else '❌'
130
+
131
+ # Format principal nicely
132
+ if isinstance(principal, dict):
133
+ if 'AWS' in principal:
134
+ principal_str = f'AWS: {principal["AWS"]}'
135
+ elif 'Service' in principal:
136
+ principal_str = f'Service: {principal["Service"]}'
137
+ else:
138
+ principal_str = str(principal)
139
+ else:
140
+ principal_str = str(principal)
141
+
142
+ # Format action nicely
143
+ if isinstance(action, list):
144
+ action_str = f'{len(action)} actions: {", ".join(action[:3])}'
145
+ if len(action) > 3:
146
+ action_str += f' + {len(action) - 3} more'
147
+ else:
148
+ action_str = str(action)
149
+
150
+ explanation += f' {emoji} **{sid}**: {effect} {action_str} for {principal_str}\n'
151
+ if len(sub_value) > 3:
152
+ explanation += f' ... and {len(sub_value) - 3} more statements\n'
153
+ else:
154
+ explanation += f' • **{sub_key}:** {_format_value(sub_value)}\n'
155
+ if len(value) > 5:
156
+ explanation += f' • ... and {len(value) - 5} more properties\n'
157
+
158
+ elif isinstance(value, list):
159
+ explanation += f'**📝 {key}:** ({len(value)} items)\n'
160
+ if format == 'detailed' and value:
161
+ for i, item in enumerate(value[:3]):
162
+ explanation += f' • **Item {i + 1}:** {_format_value(item)}\n'
163
+ if len(value) > 3:
164
+ explanation += f' • ... and {len(value) - 3} more items\n'
165
+
166
+ else:
167
+ explanation += f'**⚙️ {key}:** `{_format_value(value)}`\n'
168
+
169
+ explanation += '\n'
170
+
171
+ return explanation
172
+
173
+
174
+ def _explain_list(data: list, format: str) -> str:
175
+ """Explain list content comprehensively."""
176
+ explanation = f'**List Summary:** {len(data)} items\n\n'
177
+
178
+ if format == 'detailed':
179
+ for i, item in enumerate(data[:10]): # Limit to first 10
180
+ explanation += f'**Item {i + 1}:** {_format_value(item)}\n'
181
+ if len(data) > 10:
182
+ explanation += f'\n... and {len(data) - 10} more items\n'
183
+ else:
184
+ explanation += f'Items: {[type(item).__name__ for item in data[:5]]}\n'
185
+ if len(data) > 5:
186
+ explanation += f'... and {len(data) - 5} more\n'
187
+
188
+ return explanation
189
+
190
+
191
+ def _explain_security_scan(scan_data: dict) -> str:
192
+ """Format security scan results with emojis and clear structure."""
193
+ explanation = ''
194
+
195
+ failed_checks = scan_data.get('raw_failed_checks', [])
196
+ passed_checks = scan_data.get('raw_passed_checks', [])
197
+ scan_status = scan_data.get('scan_status', 'UNKNOWN')
198
+
199
+ # Status summary
200
+ if scan_status == 'PASSED':
201
+ explanation += '✅ **Security Scan: PASSED**\n\n'
202
+ explanation += f'🛡️ **Passed:** {len(passed_checks)} checks\n'
203
+ else:
204
+ explanation += '❌ **Security Scan: ISSUES FOUND**\n\n'
205
+ explanation += f'✅ **Passed:** {len(passed_checks)} checks\n'
206
+ explanation += f'❌ **Failed:** {len(failed_checks)} checks\n\n'
207
+
208
+ # Failed checks details
209
+ if failed_checks:
210
+ explanation += '### 🚨 Failed Security Checks:\n\n'
211
+ for check in failed_checks:
212
+ check_id = check.get('check_id', 'Unknown')
213
+ check_name = check.get('check_name', 'Unknown check')
214
+ # Try to get description from multiple possible fields
215
+ description = (
216
+ check.get('description')
217
+ or check.get('short_description')
218
+ or check.get('guideline')
219
+ or f'Security check failed: {check_name}'
220
+ )
221
+
222
+ explanation += f'• **{check_id}**: {check_name}\n'
223
+ explanation += f' 📝 **Issue:** {description}\n\n'
224
+
225
+ # Passed checks summary (don't show all details)
226
+ if passed_checks:
227
+ explanation += f'### ✅ Passed Security Checks: {len(passed_checks)}\n\n'
228
+ for check in passed_checks[:3]: # Show first 3
229
+ check_id = check.get('check_id', 'Unknown')
230
+ check_name = check.get('check_name', 'Unknown check')
231
+ explanation += f'• **{check_id}**: {check_name} ✅\n'
232
+
233
+ if len(passed_checks) > 3:
234
+ explanation += f'• ... and {len(passed_checks) - 3} more passed checks\n'
235
+
236
+ return explanation
237
+
238
+
239
+ async def explain_impl(request: ExplainRequest, workflow_store: dict) -> dict:
240
+ """MANDATORY: Explain any data in clear, human-readable format implementation."""
241
+ explained_token = None
242
+ explanation_content = None
243
+
244
+ # Check if we have valid input
245
+ has_generated_code_token = (
246
+ request.generated_code_token
247
+ and isinstance(request.generated_code_token, str)
248
+ and request.generated_code_token.strip()
249
+ )
250
+ has_content = request.content is not None and not hasattr(request.content, 'annotation')
251
+
252
+ if not has_generated_code_token and not has_content:
253
+ raise ClientError("Either 'content' or 'generated_code_token' must be provided")
254
+
255
+ # Handle infrastructure operations with token workflow
256
+ if has_generated_code_token:
257
+ # Infrastructure operation - consume generated_code_token
258
+ if request.generated_code_token not in workflow_store:
259
+ raise ClientError('Invalid generated code token')
260
+
261
+ workflow_data = workflow_store[request.generated_code_token]
262
+ if workflow_data.get('type') != 'generated_code':
263
+ raise ClientError(
264
+ 'Invalid token type: expected generated_code token from generate_infrastructure_code()'
265
+ )
266
+
267
+ # Use content if provided (LLM wants to explain the full response), otherwise use properties from token
268
+ explanation_content = (
269
+ request.content if has_content else workflow_data['data']['properties']
270
+ )
271
+
272
+ # Create explained token for infrastructure operations
273
+ explained_token = f'explained_{uuid.uuid4()}'
274
+ workflow_store[explained_token] = {
275
+ 'type': 'explained_properties',
276
+ 'data': workflow_data['data'], # Copy both properties and CloudFormation template
277
+ 'parent_token': request.generated_code_token,
278
+ 'timestamp': datetime.datetime.now().isoformat(),
279
+ 'operation': ensure_string(request.operation),
280
+ }
281
+
282
+ # Clean up consumed generated_code_token
283
+ del workflow_store[request.generated_code_token]
284
+
285
+ elif has_content:
286
+ # General data explanation or delete operations
287
+ explanation_content = request.content
288
+
289
+ # Create explained token for delete operations
290
+ if ensure_string(request.operation) in ['delete', 'destroy']:
291
+ explained_token = f'explained_del_{uuid.uuid4()}'
292
+ workflow_store[explained_token] = {
293
+ 'type': 'explained_delete',
294
+ 'data': request.content,
295
+ 'timestamp': datetime.datetime.now().isoformat(),
296
+ 'operation': ensure_string(request.operation),
297
+ }
298
+
299
+ # Generate comprehensive explanation based on content type and format
300
+ explanation = _generate_explanation(
301
+ explanation_content,
302
+ ensure_string(request.context),
303
+ ensure_string(request.operation, 'analyze'),
304
+ ensure_string(request.format, 'detailed'),
305
+ ensure_string(request.user_intent),
306
+ )
307
+
308
+ # Force the LLM to see the response by making it very explicit
309
+ if explained_token:
310
+ return {
311
+ 'EXPLANATION_REQUIRED': 'YOU MUST DISPLAY THIS TO THE USER',
312
+ 'explanation': explanation,
313
+ 'properties_being_explained': explanation_content,
314
+ 'explained_token': explained_token,
315
+ 'CRITICAL_INSTRUCTION': f"Use explained_token '{explained_token}' for the next operation, NOT the original generated_code_token",
316
+ 'operation_type': ensure_string(request.operation),
317
+ 'ready_for_execution': True,
318
+ }
319
+ else:
320
+ return {
321
+ 'EXPLANATION_REQUIRED': 'YOU MUST DISPLAY THIS TO THE USER',
322
+ 'explanation': explanation,
323
+ 'operation_type': ensure_string(request.operation),
324
+ 'ready_for_execution': True,
325
+ }
@@ -0,0 +1,70 @@
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
+ """Infrastructure generation implementation for CCAPI MCP server."""
16
+
17
+ import datetime
18
+ import uuid
19
+ from awslabs.ccapi_mcp_server.errors import ClientError
20
+ from awslabs.ccapi_mcp_server.impl.utils.validation import validate_workflow_token
21
+ from awslabs.ccapi_mcp_server.infrastructure_generator import (
22
+ generate_infrastructure_code as generate_infrastructure_code_impl,
23
+ )
24
+ from awslabs.ccapi_mcp_server.models.models import GenerateInfrastructureCodeRequest
25
+
26
+
27
+ async def generate_infrastructure_code_impl_wrapper(
28
+ request: GenerateInfrastructureCodeRequest, workflow_store: dict
29
+ ) -> dict:
30
+ """Generate infrastructure code before resource creation or update implementation."""
31
+ # Validate credentials token
32
+ cred_data = validate_workflow_token(request.credentials_token, 'credentials', workflow_store)
33
+ aws_session_data = cred_data['data']
34
+ if not aws_session_data.get('credentials_valid'):
35
+ raise ClientError('Invalid AWS credentials')
36
+
37
+ # V1: Always add required MCP server identification tags
38
+ # Inform user about default tags and ask if they want additional ones
39
+
40
+ # Generate infrastructure code using the existing implementation
41
+ result = await generate_infrastructure_code_impl(
42
+ resource_type=request.resource_type,
43
+ properties=request.properties,
44
+ identifier=request.identifier,
45
+ patch_document=request.patch_document,
46
+ region=request.region or aws_session_data.get('region') or 'us-east-1',
47
+ )
48
+
49
+ # Generate a generated code token that enforces using the exact properties and template
50
+ generated_code_token = f'generated_code_{str(uuid.uuid4())}'
51
+
52
+ # Store structured workflow data including both properties and CloudFormation template
53
+ workflow_store[generated_code_token] = {
54
+ 'type': 'generated_code',
55
+ 'data': {
56
+ 'properties': result['properties'],
57
+ 'cloudformation_template': result.get('cloudformation_template', result['properties']),
58
+ },
59
+ 'parent_token': request.credentials_token,
60
+ 'timestamp': datetime.datetime.now().isoformat(),
61
+ }
62
+
63
+ # Keep credentials token for later use in create_resource()
64
+
65
+ return {
66
+ 'generated_code_token': generated_code_token,
67
+ 'message': 'Infrastructure code generated successfully. Use generated_code_token with both explain() and run_checkov().',
68
+ 'next_step': 'Use explain() and run_checkov() with generated_code_token, then create_resource() with explained_token.',
69
+ **result, # Include all infrastructure code data for display
70
+ }