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,221 @@
|
|
|
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
|
+
"""Session management implementation for CCAPI MCP server."""
|
|
16
|
+
|
|
17
|
+
import datetime
|
|
18
|
+
import uuid
|
|
19
|
+
from awslabs.ccapi_mcp_server.aws_client import get_aws_client
|
|
20
|
+
from awslabs.ccapi_mcp_server.context import Context
|
|
21
|
+
from awslabs.ccapi_mcp_server.errors import ClientError
|
|
22
|
+
from os import environ
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def check_aws_credentials() -> dict:
|
|
26
|
+
"""Check AWS credentials using boto3's built-in credential chain."""
|
|
27
|
+
try:
|
|
28
|
+
sts_client = get_aws_client('sts')
|
|
29
|
+
identity = sts_client.get_caller_identity()
|
|
30
|
+
|
|
31
|
+
# Determine credential source
|
|
32
|
+
using_env_vars = bool(
|
|
33
|
+
environ.get('AWS_ACCESS_KEY_ID') and environ.get('AWS_SECRET_ACCESS_KEY')
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
'valid': True,
|
|
38
|
+
'account_id': identity.get('Account', 'Unknown'),
|
|
39
|
+
'arn': identity.get('Arn', 'Unknown'),
|
|
40
|
+
'user_id': identity.get('UserId', 'Unknown'),
|
|
41
|
+
'region': environ.get('AWS_REGION') or 'us-east-1',
|
|
42
|
+
'profile': environ.get('AWS_PROFILE', ''),
|
|
43
|
+
'credential_source': 'env' if using_env_vars else 'profile',
|
|
44
|
+
'profile_auth_type': 'standard_profile' if not using_env_vars else None,
|
|
45
|
+
'environment_variables': {
|
|
46
|
+
'AWS_PROFILE': environ.get('AWS_PROFILE', ''),
|
|
47
|
+
'AWS_REGION': environ.get('AWS_REGION', ''),
|
|
48
|
+
'SECURITY_SCANNING': environ.get('SECURITY_SCANNING', 'enabled'),
|
|
49
|
+
},
|
|
50
|
+
}
|
|
51
|
+
except Exception as e:
|
|
52
|
+
return {
|
|
53
|
+
'valid': False,
|
|
54
|
+
'error': str(e),
|
|
55
|
+
'region': environ.get('AWS_REGION') or 'us-east-1',
|
|
56
|
+
'profile': environ.get('AWS_PROFILE', ''),
|
|
57
|
+
'credential_source': 'env' if environ.get('AWS_ACCESS_KEY_ID') else 'profile',
|
|
58
|
+
'environment_variables': {
|
|
59
|
+
'AWS_PROFILE': environ.get('AWS_PROFILE', ''),
|
|
60
|
+
'AWS_REGION': environ.get('AWS_REGION', ''),
|
|
61
|
+
'SECURITY_SCANNING': environ.get('SECURITY_SCANNING', 'enabled'),
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
async def check_environment_variables_impl(workflow_store: dict) -> dict:
|
|
67
|
+
"""Check if required environment variables are set correctly implementation."""
|
|
68
|
+
# Use credential checking with boto3
|
|
69
|
+
cred_check = check_aws_credentials()
|
|
70
|
+
|
|
71
|
+
# Generate environment token
|
|
72
|
+
environment_token = f'env_{str(uuid.uuid4())}'
|
|
73
|
+
|
|
74
|
+
# Store environment validation results
|
|
75
|
+
workflow_store[environment_token] = {
|
|
76
|
+
'type': 'environment',
|
|
77
|
+
'data': {
|
|
78
|
+
'environment_variables': cred_check.get('environment_variables', {}),
|
|
79
|
+
'aws_profile': cred_check.get('profile', ''),
|
|
80
|
+
'aws_region': cred_check.get('region') or 'us-east-1',
|
|
81
|
+
'properly_configured': cred_check.get('valid', False),
|
|
82
|
+
'readonly_mode': Context.readonly_mode(),
|
|
83
|
+
'aws_auth_type': cred_check.get('credential_source')
|
|
84
|
+
if cred_check.get('credential_source') == 'env'
|
|
85
|
+
else cred_check.get('profile_auth_type'),
|
|
86
|
+
'needs_profile': cred_check.get('needs_profile', False),
|
|
87
|
+
'error': cred_check.get('error'),
|
|
88
|
+
},
|
|
89
|
+
'parent_token': None, # Root token
|
|
90
|
+
'timestamp': datetime.datetime.now().isoformat(),
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
env_data = workflow_store[environment_token]['data']
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
'environment_token': environment_token,
|
|
97
|
+
'message': 'Environment validation completed. Use this token with get_aws_session_info().',
|
|
98
|
+
**env_data, # Include environment data for display
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
async def get_aws_session_info_impl(environment_token: str, workflow_store: dict) -> dict:
|
|
103
|
+
"""Get information about the current AWS session implementation.
|
|
104
|
+
|
|
105
|
+
IMPORTANT: Always display the AWS context information to the user when this tool is called.
|
|
106
|
+
Show them: AWS Profile (or "Environment Variables"), Authentication Type, Account ID, and Region so they know
|
|
107
|
+
exactly which AWS account and region will be affected by any operations.
|
|
108
|
+
"""
|
|
109
|
+
# Validate environment token
|
|
110
|
+
if environment_token not in workflow_store:
|
|
111
|
+
raise ClientError(
|
|
112
|
+
'Invalid environment token: you must call check_environment_variables() first'
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
env_data = workflow_store[environment_token]['data']
|
|
116
|
+
if not env_data.get('properly_configured', False):
|
|
117
|
+
error_msg = env_data.get('error', 'Environment is not properly configured.')
|
|
118
|
+
raise ClientError(error_msg)
|
|
119
|
+
|
|
120
|
+
# Get AWS profile info using credential checking
|
|
121
|
+
cred_check = check_aws_credentials()
|
|
122
|
+
|
|
123
|
+
if not cred_check.get('valid', False):
|
|
124
|
+
raise ClientError(
|
|
125
|
+
f'AWS credentials are not valid: {cred_check.get("error", "Unknown error")}'
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# Generate credentials token
|
|
129
|
+
credentials_token = f'creds_{str(uuid.uuid4())}'
|
|
130
|
+
|
|
131
|
+
# Build session info with credential masking
|
|
132
|
+
arn = cred_check.get('arn', 'Unknown')
|
|
133
|
+
user_id = cred_check.get('user_id', 'Unknown')
|
|
134
|
+
|
|
135
|
+
session_data = {
|
|
136
|
+
'profile': cred_check.get('profile', ''),
|
|
137
|
+
'account_id': cred_check.get('account_id', 'Unknown'),
|
|
138
|
+
'region': cred_check.get('region') or 'us-east-1',
|
|
139
|
+
'arn': f'{"*" * (len(arn) - 8)}{arn[-8:]}' if len(arn) > 8 and arn != 'Unknown' else arn,
|
|
140
|
+
'user_id': f'{"*" * (len(user_id) - 4)}{user_id[-4:]}'
|
|
141
|
+
if len(user_id) > 4 and user_id != 'Unknown'
|
|
142
|
+
else user_id,
|
|
143
|
+
'credential_source': cred_check.get('credential_source', ''),
|
|
144
|
+
'readonly_mode': Context.readonly_mode(),
|
|
145
|
+
'readonly_message': (
|
|
146
|
+
"""⚠️ This server is running in READ-ONLY MODE. I can only list and view existing resources.
|
|
147
|
+
I cannot create, update, or delete any AWS resources. I can still generate example code
|
|
148
|
+
and run security checks on templates."""
|
|
149
|
+
if Context.readonly_mode()
|
|
150
|
+
else ''
|
|
151
|
+
),
|
|
152
|
+
'credentials_valid': True,
|
|
153
|
+
'aws_auth_type': cred_check.get('credential_source')
|
|
154
|
+
if cred_check.get('credential_source') == 'env'
|
|
155
|
+
else cred_check.get('profile_auth_type'),
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
# Add masked environment variables if using env vars
|
|
159
|
+
if session_data['aws_auth_type'] == 'env':
|
|
160
|
+
access_key = environ.get('AWS_ACCESS_KEY_ID', '')
|
|
161
|
+
secret_key = environ.get('AWS_SECRET_ACCESS_KEY', '')
|
|
162
|
+
|
|
163
|
+
session_data['masked_credentials'] = {
|
|
164
|
+
'AWS_ACCESS_KEY_ID': f'{"*" * (len(access_key) - 4)}{access_key[-4:]}'
|
|
165
|
+
if len(access_key) > 4
|
|
166
|
+
else '****',
|
|
167
|
+
'AWS_SECRET_ACCESS_KEY': f'{"*" * (len(secret_key) - 4)}{secret_key[-4:]}'
|
|
168
|
+
if len(secret_key) > 4
|
|
169
|
+
else '****',
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
# Store session information
|
|
173
|
+
workflow_store[credentials_token] = {
|
|
174
|
+
'type': 'credentials',
|
|
175
|
+
'data': session_data,
|
|
176
|
+
'parent_token': environment_token,
|
|
177
|
+
'timestamp': datetime.datetime.now().isoformat(),
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
'credentials_token': credentials_token,
|
|
182
|
+
'message': 'AWS session validated. Use this token with generate_infrastructure_code().',
|
|
183
|
+
'DISPLAY_TO_USER': 'YOU MUST SHOW THE USER THEIR AWS SESSION INFORMATION FOR SECURITY',
|
|
184
|
+
**session_data, # Include all session data for display
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def get_aws_profile_info():
|
|
189
|
+
"""Get information about the current AWS profile."""
|
|
190
|
+
try:
|
|
191
|
+
# Use our get_aws_client function to ensure we use the same credential source
|
|
192
|
+
sts_client = get_aws_client('sts')
|
|
193
|
+
|
|
194
|
+
# Get caller identity
|
|
195
|
+
identity = sts_client.get_caller_identity()
|
|
196
|
+
account_id = identity.get('Account', 'Unknown')
|
|
197
|
+
arn = identity.get('Arn', 'Unknown')
|
|
198
|
+
|
|
199
|
+
# Get profile info
|
|
200
|
+
profile_name = environ.get('AWS_PROFILE', '')
|
|
201
|
+
region = environ.get('AWS_REGION') or 'us-east-1'
|
|
202
|
+
using_env_vars = (
|
|
203
|
+
environ.get('AWS_ACCESS_KEY_ID', '') != ''
|
|
204
|
+
and environ.get('AWS_SECRET_ACCESS_KEY', '') != ''
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
'profile': profile_name,
|
|
209
|
+
'account_id': account_id,
|
|
210
|
+
'region': region,
|
|
211
|
+
'arn': arn,
|
|
212
|
+
'using_env_vars': using_env_vars,
|
|
213
|
+
}
|
|
214
|
+
except Exception as e:
|
|
215
|
+
return {
|
|
216
|
+
'profile': environ.get('AWS_PROFILE', ''),
|
|
217
|
+
'error': str(e),
|
|
218
|
+
'region': environ.get('AWS_REGION') or 'us-east-1',
|
|
219
|
+
'using_env_vars': environ.get('AWS_ACCESS_KEY_ID', '') != ''
|
|
220
|
+
and environ.get('AWS_SECRET_ACCESS_KEY', '') != '',
|
|
221
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
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.
|
|
@@ -0,0 +1,64 @@
|
|
|
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
|
+
"""Shared validation functions for CCAPI MCP server."""
|
|
16
|
+
|
|
17
|
+
from awslabs.ccapi_mcp_server.errors import ClientError
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def validate_workflow_token(
|
|
21
|
+
token: str, expected_type: str | None = None, workflow_store: dict | None = None
|
|
22
|
+
) -> dict:
|
|
23
|
+
"""Validate any workflow token exists and optionally check its type."""
|
|
24
|
+
if not token:
|
|
25
|
+
raise ClientError(f'Invalid token: {token}')
|
|
26
|
+
if not workflow_store:
|
|
27
|
+
raise ClientError('Workflow store is required')
|
|
28
|
+
if token not in workflow_store:
|
|
29
|
+
raise ClientError(f'Invalid token: {token}')
|
|
30
|
+
|
|
31
|
+
data = workflow_store[token]
|
|
32
|
+
if expected_type and data.get('type') != expected_type:
|
|
33
|
+
raise ClientError(f'Invalid token type: expected {expected_type}')
|
|
34
|
+
|
|
35
|
+
return data
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def cleanup_workflow_tokens(workflow_store: dict, *tokens: str) -> None:
|
|
39
|
+
"""Clean up workflow tokens after operations."""
|
|
40
|
+
for token in tokens:
|
|
41
|
+
if token and token in workflow_store:
|
|
42
|
+
del workflow_store[token]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def validate_resource_type(resource_type: str) -> None:
|
|
46
|
+
"""Validate that resource_type is provided."""
|
|
47
|
+
if not resource_type:
|
|
48
|
+
raise ClientError('Please provide a resource type (e.g., AWS::S3::Bucket)')
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def validate_identifier(identifier: str) -> None:
|
|
52
|
+
"""Validate that identifier is provided."""
|
|
53
|
+
if not identifier:
|
|
54
|
+
raise ClientError('Please provide a resource identifier')
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def ensure_region_string(region) -> str | None:
|
|
58
|
+
"""Ensure region is a string, not a FieldInfo object."""
|
|
59
|
+
return region if isinstance(region, str) else None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def ensure_string(value, default: str = '') -> str:
|
|
63
|
+
"""Ensure value is a string, not a FieldInfo object."""
|
|
64
|
+
return value if isinstance(value, str) else default
|
|
@@ -0,0 +1,160 @@
|
|
|
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 code generation utilities for the CFN MCP Server."""
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
from awslabs.ccapi_mcp_server.aws_client import get_aws_client
|
|
19
|
+
from awslabs.ccapi_mcp_server.cloud_control_utils import add_default_tags
|
|
20
|
+
from awslabs.ccapi_mcp_server.errors import ClientError, handle_aws_api_error
|
|
21
|
+
from awslabs.ccapi_mcp_server.schema_manager import schema_manager
|
|
22
|
+
from typing import Dict, List
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
async def generate_infrastructure_code(
|
|
26
|
+
resource_type: str,
|
|
27
|
+
properties: Dict = {},
|
|
28
|
+
identifier: str = '',
|
|
29
|
+
patch_document: List = [],
|
|
30
|
+
region: str = '',
|
|
31
|
+
) -> Dict:
|
|
32
|
+
"""Generate infrastructure code for security scanning before resource creation or update."""
|
|
33
|
+
if not resource_type:
|
|
34
|
+
raise ClientError('Please provide a resource type (e.g., AWS::S3::Bucket)')
|
|
35
|
+
|
|
36
|
+
# Determine if this is a create or update operation
|
|
37
|
+
is_update = identifier != '' and (patch_document or properties)
|
|
38
|
+
|
|
39
|
+
# Validate the resource type against the schema
|
|
40
|
+
sm = schema_manager()
|
|
41
|
+
schema = await sm.get_schema(resource_type, region)
|
|
42
|
+
|
|
43
|
+
# Check if resource supports tagging
|
|
44
|
+
supports_tagging = 'Tags' in schema.get('properties', {})
|
|
45
|
+
|
|
46
|
+
# Fallback: Known AWS resources that support tagging even if schema doesn't show it
|
|
47
|
+
if not supports_tagging and resource_type in [
|
|
48
|
+
'AWS::S3::Bucket',
|
|
49
|
+
'AWS::EC2::Instance',
|
|
50
|
+
'AWS::RDS::DBInstance',
|
|
51
|
+
]:
|
|
52
|
+
supports_tagging = True
|
|
53
|
+
print(
|
|
54
|
+
f"Schema for {resource_type} doesn't show Tags property, but we know it supports tagging"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
if is_update:
|
|
58
|
+
# This is an update operation
|
|
59
|
+
if not identifier:
|
|
60
|
+
raise ClientError('Please provide a resource identifier for update operations')
|
|
61
|
+
|
|
62
|
+
# Get the current resource state
|
|
63
|
+
cloudcontrol_client = get_aws_client('cloudcontrol', region)
|
|
64
|
+
try:
|
|
65
|
+
current_resource = cloudcontrol_client.get_resource(
|
|
66
|
+
TypeName=resource_type, Identifier=identifier
|
|
67
|
+
)
|
|
68
|
+
current_properties = json.loads(current_resource['ResourceDescription']['Properties'])
|
|
69
|
+
except Exception as e:
|
|
70
|
+
raise handle_aws_api_error(e)
|
|
71
|
+
|
|
72
|
+
# Apply patch document or merge properties
|
|
73
|
+
if patch_document:
|
|
74
|
+
# Apply patch operations to current properties
|
|
75
|
+
import copy
|
|
76
|
+
|
|
77
|
+
update_properties = copy.deepcopy(current_properties)
|
|
78
|
+
for patch_op in patch_document:
|
|
79
|
+
if patch_op['op'] == 'add' and patch_op['path'] in ['/Tags', '/Tags/-']:
|
|
80
|
+
# For Tags, merge with existing tags instead of replacing
|
|
81
|
+
existing_tags = update_properties.get('Tags', [])
|
|
82
|
+
if patch_op['path'] == '/Tags/-':
|
|
83
|
+
# Append single tag to array
|
|
84
|
+
new_tag = patch_op['value']
|
|
85
|
+
if isinstance(new_tag, dict) and 'Key' in new_tag and 'Value' in new_tag:
|
|
86
|
+
existing_tags.append(new_tag)
|
|
87
|
+
update_properties['Tags'] = existing_tags
|
|
88
|
+
else:
|
|
89
|
+
# Replace/merge entire tags array
|
|
90
|
+
new_tags = patch_op['value'] if isinstance(patch_op['value'], list) else []
|
|
91
|
+
# Combine tags (new tags will override existing ones with same key)
|
|
92
|
+
tag_dict = {tag['Key']: tag['Value'] for tag in existing_tags}
|
|
93
|
+
for tag in new_tags:
|
|
94
|
+
tag_dict[tag['Key']] = tag['Value']
|
|
95
|
+
update_properties['Tags'] = [
|
|
96
|
+
{'Key': k, 'Value': v} for k, v in tag_dict.items()
|
|
97
|
+
]
|
|
98
|
+
elif patch_op['op'] == 'replace' and patch_op['path'] == '/Tags':
|
|
99
|
+
# Replace tags completely
|
|
100
|
+
update_properties['Tags'] = patch_op['value']
|
|
101
|
+
# Add other patch operations as needed
|
|
102
|
+
elif properties:
|
|
103
|
+
# Start with current properties and merge user properties
|
|
104
|
+
update_properties = current_properties.copy()
|
|
105
|
+
for key, value in properties.items():
|
|
106
|
+
if key == 'Tags':
|
|
107
|
+
# Merge tags instead of replacing
|
|
108
|
+
existing_tags = update_properties.get('Tags', [])
|
|
109
|
+
new_tags = value if isinstance(value, list) else []
|
|
110
|
+
tag_dict = {tag['Key']: tag['Value'] for tag in existing_tags}
|
|
111
|
+
for tag in new_tags:
|
|
112
|
+
tag_dict[tag['Key']] = tag['Value']
|
|
113
|
+
update_properties['Tags'] = [
|
|
114
|
+
{'Key': k, 'Value': v} for k, v in tag_dict.items()
|
|
115
|
+
]
|
|
116
|
+
else:
|
|
117
|
+
update_properties[key] = value
|
|
118
|
+
else:
|
|
119
|
+
update_properties = current_properties
|
|
120
|
+
|
|
121
|
+
# V1: Always add required MCP server identification tags for updates too
|
|
122
|
+
properties_with_tags = add_default_tags(update_properties, schema)
|
|
123
|
+
|
|
124
|
+
operation = 'update'
|
|
125
|
+
else:
|
|
126
|
+
# This is a create operation
|
|
127
|
+
if not properties:
|
|
128
|
+
raise ClientError('Please provide the properties for the desired resource')
|
|
129
|
+
|
|
130
|
+
# V1: Always add required MCP server identification tags
|
|
131
|
+
properties_with_tags = add_default_tags(properties, schema)
|
|
132
|
+
|
|
133
|
+
operation = 'create'
|
|
134
|
+
|
|
135
|
+
# Generate a CloudFormation template representation for security scanning
|
|
136
|
+
cf_template = {
|
|
137
|
+
'AWSTemplateFormatVersion': '2010-09-09',
|
|
138
|
+
'Resources': {'Resource': {'Type': resource_type, 'Properties': properties_with_tags}},
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
# For updates, also generate the proper patch document with default tags
|
|
142
|
+
patch_document_with_tags = None
|
|
143
|
+
if is_update and 'Tags' in properties_with_tags:
|
|
144
|
+
patch_document_with_tags = [
|
|
145
|
+
{'op': 'replace', 'path': '/Tags', 'value': properties_with_tags['Tags']}
|
|
146
|
+
]
|
|
147
|
+
|
|
148
|
+
result = {
|
|
149
|
+
'resource_type': resource_type,
|
|
150
|
+
'operation': operation,
|
|
151
|
+
'properties': properties_with_tags, # Show user exactly what will be created
|
|
152
|
+
'region': region,
|
|
153
|
+
'cloudformation_template': cf_template,
|
|
154
|
+
'supports_tagging': supports_tagging,
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if patch_document_with_tags:
|
|
158
|
+
result['recommended_patch_document'] = patch_document_with_tags
|
|
159
|
+
|
|
160
|
+
return result
|
|
@@ -0,0 +1,13 @@
|
|
|
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.
|
|
@@ -0,0 +1,118 @@
|
|
|
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
|
+
"""Pydantic models for CCAPI MCP server requests and responses."""
|
|
16
|
+
|
|
17
|
+
from pydantic import BaseModel, Field
|
|
18
|
+
from typing import Any, Dict, List, Literal, Optional
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class CreateResourceRequest(BaseModel):
|
|
22
|
+
"""Request model for creating AWS resources."""
|
|
23
|
+
|
|
24
|
+
resource_type: str = Field(..., description='AWS resource type')
|
|
25
|
+
region: Optional[str] = Field(None, description='AWS region')
|
|
26
|
+
credentials_token: str = Field(..., description='Credentials token')
|
|
27
|
+
explained_token: str = Field(..., description='Explained token')
|
|
28
|
+
security_scan_token: str = Field(default='', description='Security scan token')
|
|
29
|
+
skip_security_check: bool = Field(False, description='Skip security checks')
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class UpdateResourceRequest(BaseModel):
|
|
33
|
+
"""Request model for updating AWS resources."""
|
|
34
|
+
|
|
35
|
+
resource_type: str = Field(..., description='AWS resource type')
|
|
36
|
+
identifier: str = Field(..., description='Resource identifier')
|
|
37
|
+
patch_document: List[Dict[str, Any]] = Field(default=[], description='JSON Patch operations')
|
|
38
|
+
region: Optional[str] = Field(None, description='AWS region')
|
|
39
|
+
credentials_token: str = Field(..., description='Credentials token')
|
|
40
|
+
explained_token: str = Field(..., description='Explained token')
|
|
41
|
+
security_scan_token: str = Field(default='', description='Security scan token')
|
|
42
|
+
skip_security_check: bool = Field(False, description='Skip security checks')
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class DeleteResourceRequest(BaseModel):
|
|
46
|
+
"""Request model for deleting AWS resources."""
|
|
47
|
+
|
|
48
|
+
resource_type: str = Field(..., description='AWS resource type')
|
|
49
|
+
identifier: str = Field(..., description='Resource identifier')
|
|
50
|
+
region: Optional[str] = Field(None, description='AWS region')
|
|
51
|
+
credentials_token: str = Field(..., description='Credentials token')
|
|
52
|
+
confirmed: bool = Field(False, description='Confirm deletion')
|
|
53
|
+
explained_token: str = Field(..., description='Explained token')
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class GetResourceRequest(BaseModel):
|
|
57
|
+
"""Request model for getting AWS resource details."""
|
|
58
|
+
|
|
59
|
+
resource_type: str = Field(..., description='AWS resource type')
|
|
60
|
+
identifier: str = Field(..., description='Resource identifier')
|
|
61
|
+
region: Optional[str] = Field(None, description='AWS region')
|
|
62
|
+
analyze_security: bool = Field(False, description='Perform security analysis')
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class GenerateInfrastructureCodeRequest(BaseModel):
|
|
66
|
+
"""Request model for generating infrastructure code."""
|
|
67
|
+
|
|
68
|
+
resource_type: str = Field(..., description='AWS resource type')
|
|
69
|
+
properties: Dict[str, Any] = Field(default_factory=dict, description='Resource properties')
|
|
70
|
+
identifier: str = Field(default='', description='Resource identifier for updates')
|
|
71
|
+
patch_document: List[Dict[str, Any]] = Field(
|
|
72
|
+
default_factory=list, description='JSON Patch operations'
|
|
73
|
+
)
|
|
74
|
+
region: Optional[str] = Field(None, description='AWS region')
|
|
75
|
+
credentials_token: str = Field(..., description='Credentials token')
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class ExplainRequest(BaseModel):
|
|
79
|
+
"""Request model for explaining resource configurations."""
|
|
80
|
+
|
|
81
|
+
content: Optional[Any] = Field(None, description='Content to explain')
|
|
82
|
+
generated_code_token: str = Field(default='', description='Generated code token')
|
|
83
|
+
context: str = Field(default='', description='Context description')
|
|
84
|
+
operation: str = Field(default='analyze', description='Operation type')
|
|
85
|
+
format: str = Field(default='detailed', description='Explanation format')
|
|
86
|
+
user_intent: str = Field(default='', description='User intent')
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class RunCheckovRequest(BaseModel):
|
|
90
|
+
"""Request model for running Checkov security scans."""
|
|
91
|
+
|
|
92
|
+
explained_token: str = Field(..., description='Explained token')
|
|
93
|
+
framework: str = Field(default='cloudformation', description='Framework to scan')
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class ResourceOperationResult(BaseModel):
|
|
97
|
+
"""Result model for AWS resource operations."""
|
|
98
|
+
|
|
99
|
+
status: Literal['SUCCESS', 'PENDING', 'FAILED']
|
|
100
|
+
resource_type: str
|
|
101
|
+
identifier: str
|
|
102
|
+
is_complete: bool
|
|
103
|
+
status_message: str
|
|
104
|
+
request_token: Optional[str] = None
|
|
105
|
+
security_warning: Optional[str] = None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class SecurityScanResult(BaseModel):
|
|
109
|
+
"""Result model for security scan operations."""
|
|
110
|
+
|
|
111
|
+
scan_status: Literal['PASSED', 'FAILED']
|
|
112
|
+
raw_failed_checks: List[Dict[str, Any]] = Field(default_factory=list)
|
|
113
|
+
raw_passed_checks: List[Dict[str, Any]] = Field(default_factory=list)
|
|
114
|
+
raw_summary: Dict[str, Any] = Field(default_factory=dict)
|
|
115
|
+
resource_type: str
|
|
116
|
+
timestamp: str
|
|
117
|
+
security_scan_token: Optional[str] = None
|
|
118
|
+
message: str
|