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.
- awslabs/__init__.py +16 -0
- awslabs/ccapi_mcp_server/__init__.py +17 -0
- awslabs/ccapi_mcp_server/aws_client.py +62 -0
- awslabs/ccapi_mcp_server/cloud_control_utils.py +120 -0
- awslabs/ccapi_mcp_server/context.py +37 -0
- awslabs/ccapi_mcp_server/errors.py +67 -0
- awslabs/ccapi_mcp_server/iac_generator.py +203 -0
- awslabs/ccapi_mcp_server/impl/__init__.py +13 -0
- awslabs/ccapi_mcp_server/impl/tools/__init__.py +13 -0
- awslabs/ccapi_mcp_server/impl/tools/explanation.py +325 -0
- awslabs/ccapi_mcp_server/impl/tools/infrastructure_generation.py +70 -0
- awslabs/ccapi_mcp_server/impl/tools/resource_operations.py +367 -0
- awslabs/ccapi_mcp_server/impl/tools/security_scanning.py +223 -0
- awslabs/ccapi_mcp_server/impl/tools/session_management.py +221 -0
- awslabs/ccapi_mcp_server/impl/utils/__init__.py +13 -0
- awslabs/ccapi_mcp_server/impl/utils/validation.py +64 -0
- awslabs/ccapi_mcp_server/infrastructure_generator.py +160 -0
- awslabs/ccapi_mcp_server/models/__init__.py +13 -0
- awslabs/ccapi_mcp_server/models/models.py +118 -0
- awslabs/ccapi_mcp_server/schema_manager.py +219 -0
- awslabs/ccapi_mcp_server/server.py +733 -0
- awslabs/ccapi_mcp_server/static/__init__.py +13 -0
- awslabs_ccapi_mcp_server-1.0.1.dist-info/METADATA +656 -0
- awslabs_ccapi_mcp_server-1.0.1.dist-info/RECORD +28 -0
- awslabs_ccapi_mcp_server-1.0.1.dist-info/WHEEL +4 -0
- awslabs_ccapi_mcp_server-1.0.1.dist-info/entry_points.txt +2 -0
- awslabs_ccapi_mcp_server-1.0.1.dist-info/licenses/LICENSE +175 -0
- awslabs_ccapi_mcp_server-1.0.1.dist-info/licenses/NOTICE +2 -0
|
@@ -0,0 +1,219 @@
|
|
|
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
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
from awslabs.ccapi_mcp_server.aws_client import get_aws_client
|
|
18
|
+
from awslabs.ccapi_mcp_server.errors import ClientError
|
|
19
|
+
from datetime import datetime, timedelta
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Dict
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# all schema metadata is stored in .schemas/schema_metadata.json. The schemas themselves are all stored in the directory.
|
|
25
|
+
SCHEMA_CACHE_DIR = '.schemas'
|
|
26
|
+
SCHEMA_METADATA_FILE = 'schema_metadata.json'
|
|
27
|
+
SCHEMA_UPDATE_INTERVAL = timedelta(days=7) # Check for updates weekly
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class SchemaManager:
|
|
31
|
+
"""Responsible for keeping track of schemas, cacheing them locally, and updating them if they are outdated."""
|
|
32
|
+
|
|
33
|
+
def __init__(self):
|
|
34
|
+
"""Initialize the schema manager with the cache directory."""
|
|
35
|
+
cache_dir = os.path.join(os.path.dirname(__file__), '.schemas')
|
|
36
|
+
self.cache_dir = Path(cache_dir)
|
|
37
|
+
self.metadata_file = self.cache_dir / SCHEMA_METADATA_FILE
|
|
38
|
+
self.schema_registry: Dict[str, dict] = {}
|
|
39
|
+
|
|
40
|
+
# Ensure cache directory exists
|
|
41
|
+
self.cache_dir.mkdir(exist_ok=True)
|
|
42
|
+
|
|
43
|
+
# Load metadata if it exists
|
|
44
|
+
self.metadata = self._load_metadata()
|
|
45
|
+
|
|
46
|
+
# Load cached schemas into registry
|
|
47
|
+
self._load_cached_schemas()
|
|
48
|
+
|
|
49
|
+
def _load_metadata(self) -> dict:
|
|
50
|
+
"""Load schema metadata from file or create if it doesn't exist."""
|
|
51
|
+
if self.metadata_file.exists():
|
|
52
|
+
try:
|
|
53
|
+
with open(self.metadata_file, 'r') as f:
|
|
54
|
+
return json.load(f)
|
|
55
|
+
except json.JSONDecodeError:
|
|
56
|
+
print('Corrupted metadata file. Creating new one.')
|
|
57
|
+
|
|
58
|
+
# Default metadata
|
|
59
|
+
metadata = {'version': '1', 'schemas': {}}
|
|
60
|
+
|
|
61
|
+
# Save default metadata
|
|
62
|
+
with open(self.metadata_file, 'w') as f:
|
|
63
|
+
json.dump(metadata, f, indent=2)
|
|
64
|
+
|
|
65
|
+
return metadata
|
|
66
|
+
|
|
67
|
+
def _load_cached_schemas(self):
|
|
68
|
+
"""Load all cached schemas into the registry."""
|
|
69
|
+
for schema_file in self.cache_dir.glob('*.json'):
|
|
70
|
+
if schema_file.name == SCHEMA_METADATA_FILE:
|
|
71
|
+
continue
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
with open(schema_file, 'r') as f:
|
|
75
|
+
schema = json.load(f)
|
|
76
|
+
if 'typeName' in schema:
|
|
77
|
+
resource_type = schema['typeName']
|
|
78
|
+
self.schema_registry[resource_type] = schema
|
|
79
|
+
print(f'Loaded schema for {resource_type} from cache')
|
|
80
|
+
except (json.JSONDecodeError, IOError) as e:
|
|
81
|
+
print(f'Error loading schema from {schema_file}: {str(e)}')
|
|
82
|
+
|
|
83
|
+
async def get_schema(self, resource_type: str, region: str | None = None) -> dict:
|
|
84
|
+
"""Get schema for a resource type, downloading it if necessary."""
|
|
85
|
+
# Check if schema is in registry
|
|
86
|
+
if resource_type in self.schema_registry:
|
|
87
|
+
cached_schema = self.schema_registry[resource_type]
|
|
88
|
+
|
|
89
|
+
# If cached schema is corrupted (empty properties), force reload
|
|
90
|
+
if not cached_schema.get('properties'):
|
|
91
|
+
print(
|
|
92
|
+
f'Cached schema for {resource_type} is corrupted (empty properties), reloading...'
|
|
93
|
+
)
|
|
94
|
+
# Remove from registry to force reload
|
|
95
|
+
del self.schema_registry[resource_type]
|
|
96
|
+
else:
|
|
97
|
+
# Check if schema needs to be updated based on last update time
|
|
98
|
+
if resource_type in self.metadata['schemas']:
|
|
99
|
+
schema_metadata = self.metadata['schemas'][resource_type]
|
|
100
|
+
last_updated_str = schema_metadata.get('last_updated')
|
|
101
|
+
|
|
102
|
+
if last_updated_str:
|
|
103
|
+
try:
|
|
104
|
+
last_updated = datetime.fromisoformat(last_updated_str)
|
|
105
|
+
if datetime.now() - last_updated < SCHEMA_UPDATE_INTERVAL:
|
|
106
|
+
# Schema is recent enough and valid, use cached version
|
|
107
|
+
return cached_schema
|
|
108
|
+
else:
|
|
109
|
+
print(
|
|
110
|
+
f'Schema for {resource_type} is older than {SCHEMA_UPDATE_INTERVAL.days} days, refreshing...'
|
|
111
|
+
)
|
|
112
|
+
except ValueError:
|
|
113
|
+
print(
|
|
114
|
+
f'Invalid timestamp format for {resource_type}: {last_updated_str}'
|
|
115
|
+
)
|
|
116
|
+
else:
|
|
117
|
+
# No metadata for this schema but it's valid, use cached version
|
|
118
|
+
return cached_schema
|
|
119
|
+
|
|
120
|
+
# Download schema (either not cached, expired, or corrupted)
|
|
121
|
+
schema = await self._download_resource_schema(resource_type, region)
|
|
122
|
+
return schema
|
|
123
|
+
|
|
124
|
+
async def _download_resource_schema(
|
|
125
|
+
self, resource_type: str, region: str | None = None
|
|
126
|
+
) -> dict:
|
|
127
|
+
"""Download schema for a specific resource type.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
resource_type: The AWS resource type (e.g., "AWS::S3::Bucket")
|
|
131
|
+
region: AWS region to use for API calls
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
The downloaded schema or None if download failed
|
|
135
|
+
"""
|
|
136
|
+
# Extract service name from resource type
|
|
137
|
+
parts = resource_type.split('::')
|
|
138
|
+
if len(parts) < 2:
|
|
139
|
+
raise ClientError(
|
|
140
|
+
f"Invalid resource type format: {resource_type}. Expected format like 'Namespace::Service::Resource'"
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# If no local spec file or it failed to load, try CloudFormation API
|
|
144
|
+
# Retry logic for schema download
|
|
145
|
+
max_retries = 3
|
|
146
|
+
for attempt in range(max_retries):
|
|
147
|
+
try:
|
|
148
|
+
print(
|
|
149
|
+
f'Downloading schema for {resource_type} using CloudFormation API (attempt {attempt + 1}/{max_retries})'
|
|
150
|
+
)
|
|
151
|
+
cfn_client = get_aws_client('cloudformation', region)
|
|
152
|
+
resp = cfn_client.describe_type(Type='RESOURCE', TypeName=resource_type)
|
|
153
|
+
schema_str = resp['Schema']
|
|
154
|
+
|
|
155
|
+
if not schema_str or len(schema_str) < 100: # Basic sanity check
|
|
156
|
+
raise ClientError(f'Schema response too short: {len(schema_str)} characters')
|
|
157
|
+
|
|
158
|
+
spec = json.loads(schema_str)
|
|
159
|
+
|
|
160
|
+
# Validate that the schema has properties (not empty)
|
|
161
|
+
if not spec.get('properties'):
|
|
162
|
+
raise ClientError(
|
|
163
|
+
f'Downloaded schema for {resource_type} has no properties - API may have failed'
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# For known taggable resources, verify Tags property exists
|
|
167
|
+
if resource_type in [
|
|
168
|
+
'AWS::S3::Bucket',
|
|
169
|
+
'AWS::EC2::Instance',
|
|
170
|
+
'AWS::RDS::DBInstance',
|
|
171
|
+
]:
|
|
172
|
+
if 'Tags' not in spec.get('properties', {}):
|
|
173
|
+
print(
|
|
174
|
+
f'Warning: {resource_type} schema missing Tags property, but resource should support tagging'
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
# Save schema to cache only if it's valid
|
|
178
|
+
schema_file = self.cache_dir / f'{resource_type.replace("::", "_")}.json'
|
|
179
|
+
with open(schema_file, 'w') as f:
|
|
180
|
+
f.write(schema_str)
|
|
181
|
+
|
|
182
|
+
# Update registry with the valid schema
|
|
183
|
+
self.schema_registry[resource_type] = spec
|
|
184
|
+
|
|
185
|
+
# Update metadata
|
|
186
|
+
self.metadata['schemas'][resource_type] = {
|
|
187
|
+
'last_updated': datetime.now().isoformat(),
|
|
188
|
+
'file_path': str(schema_file),
|
|
189
|
+
'source': 'cloudformation_api',
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
with open(self.metadata_file, 'w') as f:
|
|
193
|
+
json.dump(self.metadata, f, indent=2)
|
|
194
|
+
|
|
195
|
+
print(f'Processed and cached schema for {resource_type}')
|
|
196
|
+
return spec
|
|
197
|
+
|
|
198
|
+
except Exception as e:
|
|
199
|
+
print(f'Schema download attempt {attempt + 1} failed: {str(e)}')
|
|
200
|
+
if attempt == max_retries - 1: # Last attempt
|
|
201
|
+
raise ClientError(
|
|
202
|
+
f'Failed to download valid schema for {resource_type} after {max_retries} attempts: {str(e)}'
|
|
203
|
+
)
|
|
204
|
+
# Wait before retry
|
|
205
|
+
import time
|
|
206
|
+
|
|
207
|
+
time.sleep(1)
|
|
208
|
+
|
|
209
|
+
# Should never reach here
|
|
210
|
+
raise ClientError(f'Failed to download schema for {resource_type}')
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
_schema_manager_instance = SchemaManager()
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
# used to load a single instance of the schema manager
|
|
217
|
+
def schema_manager() -> SchemaManager:
|
|
218
|
+
"""Loads a singleton of the resource."""
|
|
219
|
+
return _schema_manager_instance
|