awslabs.elasticache-mcp-server 0.1.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.
Files changed (60) hide show
  1. awslabs/__init__.py +16 -0
  2. awslabs/elasticache_mcp_server/__init__.py +17 -0
  3. awslabs/elasticache_mcp_server/common/__init__.py +15 -0
  4. awslabs/elasticache_mcp_server/common/connection.py +117 -0
  5. awslabs/elasticache_mcp_server/common/decorators.py +41 -0
  6. awslabs/elasticache_mcp_server/common/server.py +30 -0
  7. awslabs/elasticache_mcp_server/context.py +39 -0
  8. awslabs/elasticache_mcp_server/main.py +52 -0
  9. awslabs/elasticache_mcp_server/tools/__init__.py +15 -0
  10. awslabs/elasticache_mcp_server/tools/cc/__init__.py +31 -0
  11. awslabs/elasticache_mcp_server/tools/cc/connect.py +444 -0
  12. awslabs/elasticache_mcp_server/tools/cc/create.py +212 -0
  13. awslabs/elasticache_mcp_server/tools/cc/delete.py +65 -0
  14. awslabs/elasticache_mcp_server/tools/cc/describe.py +80 -0
  15. awslabs/elasticache_mcp_server/tools/cc/modify.py +159 -0
  16. awslabs/elasticache_mcp_server/tools/cc/parsers.py +78 -0
  17. awslabs/elasticache_mcp_server/tools/cc/processors.py +74 -0
  18. awslabs/elasticache_mcp_server/tools/ce/__init__.py +19 -0
  19. awslabs/elasticache_mcp_server/tools/ce/get_cost_and_usage.py +76 -0
  20. awslabs/elasticache_mcp_server/tools/cw/__init__.py +19 -0
  21. awslabs/elasticache_mcp_server/tools/cw/get_metric_statistics.py +85 -0
  22. awslabs/elasticache_mcp_server/tools/cwlogs/__init__.py +29 -0
  23. awslabs/elasticache_mcp_server/tools/cwlogs/create_log_group.py +68 -0
  24. awslabs/elasticache_mcp_server/tools/cwlogs/describe_log_groups.py +123 -0
  25. awslabs/elasticache_mcp_server/tools/cwlogs/describe_log_streams.py +120 -0
  26. awslabs/elasticache_mcp_server/tools/cwlogs/filter_log_events.py +122 -0
  27. awslabs/elasticache_mcp_server/tools/cwlogs/get_log_events.py +99 -0
  28. awslabs/elasticache_mcp_server/tools/firehose/__init__.py +19 -0
  29. awslabs/elasticache_mcp_server/tools/firehose/list_delivery_streams.py +63 -0
  30. awslabs/elasticache_mcp_server/tools/misc/__init__.py +31 -0
  31. awslabs/elasticache_mcp_server/tools/misc/batch_apply_update_action.py +62 -0
  32. awslabs/elasticache_mcp_server/tools/misc/batch_stop_update_action.py +62 -0
  33. awslabs/elasticache_mcp_server/tools/misc/describe_cache_engine_versions.py +79 -0
  34. awslabs/elasticache_mcp_server/tools/misc/describe_engine_default_parameters.py +64 -0
  35. awslabs/elasticache_mcp_server/tools/misc/describe_events.py +86 -0
  36. awslabs/elasticache_mcp_server/tools/misc/describe_service_updates.py +71 -0
  37. awslabs/elasticache_mcp_server/tools/rg/__init__.py +54 -0
  38. awslabs/elasticache_mcp_server/tools/rg/complete_migration.py +94 -0
  39. awslabs/elasticache_mcp_server/tools/rg/connect.py +537 -0
  40. awslabs/elasticache_mcp_server/tools/rg/create.py +318 -0
  41. awslabs/elasticache_mcp_server/tools/rg/delete.py +68 -0
  42. awslabs/elasticache_mcp_server/tools/rg/describe.py +68 -0
  43. awslabs/elasticache_mcp_server/tools/rg/modify.py +236 -0
  44. awslabs/elasticache_mcp_server/tools/rg/parsers.py +268 -0
  45. awslabs/elasticache_mcp_server/tools/rg/processors.py +227 -0
  46. awslabs/elasticache_mcp_server/tools/rg/start_migration.py +151 -0
  47. awslabs/elasticache_mcp_server/tools/rg/test_migration.py +139 -0
  48. awslabs/elasticache_mcp_server/tools/serverless/__init__.py +37 -0
  49. awslabs/elasticache_mcp_server/tools/serverless/connect.py +451 -0
  50. awslabs/elasticache_mcp_server/tools/serverless/create.py +174 -0
  51. awslabs/elasticache_mcp_server/tools/serverless/delete.py +49 -0
  52. awslabs/elasticache_mcp_server/tools/serverless/describe.py +69 -0
  53. awslabs/elasticache_mcp_server/tools/serverless/models.py +160 -0
  54. awslabs/elasticache_mcp_server/tools/serverless/modify.py +95 -0
  55. awslabs_elasticache_mcp_server-0.1.1.dist-info/METADATA +257 -0
  56. awslabs_elasticache_mcp_server-0.1.1.dist-info/RECORD +60 -0
  57. awslabs_elasticache_mcp_server-0.1.1.dist-info/WHEEL +4 -0
  58. awslabs_elasticache_mcp_server-0.1.1.dist-info/entry_points.txt +2 -0
  59. awslabs_elasticache_mcp_server-0.1.1.dist-info/licenses/LICENSE +175 -0
  60. awslabs_elasticache_mcp_server-0.1.1.dist-info/licenses/NOTICE +2 -0
@@ -0,0 +1,268 @@
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
+ """Parser functions for ElastiCache replication group tools."""
16
+
17
+ import json
18
+ from typing import Any, Dict
19
+
20
+
21
+ def parse_shorthand_resharding(config: str) -> Dict[str, Any]:
22
+ """Parse a single resharding configuration from shorthand syntax.
23
+
24
+ Args:
25
+ config: Shorthand syntax string for resharding configuration
26
+ Format: NodeGroupId=string,NewShardConfiguration={NewReplicaCount=integer,PreferredAvailabilityZones=string1,string2}
27
+
28
+ Returns:
29
+ Dictionary containing the parsed resharding configuration
30
+
31
+ Raises:
32
+ ValueError: If the syntax is invalid
33
+ """
34
+ if not config:
35
+ raise ValueError('Empty resharding configuration')
36
+
37
+ result = {}
38
+ pairs = config.split(',')
39
+
40
+ # Define valid keys and their processors
41
+ key_processors = {
42
+ 'NodeGroupId': str,
43
+ 'NewShardConfiguration': lambda x: parse_new_shard_config(x),
44
+ }
45
+
46
+ for pair in pairs:
47
+ if '=' not in pair:
48
+ raise ValueError(f'Invalid format. Each parameter must be in key=value format: {pair}')
49
+
50
+ key, value = pair.split('=', 1)
51
+ if not key or not value:
52
+ raise ValueError(f'Empty key or value: {pair}')
53
+
54
+ if key not in key_processors:
55
+ raise ValueError(f'Invalid parameter: {key}')
56
+
57
+ try:
58
+ result[key] = key_processors[key](value)
59
+ except ValueError as e:
60
+ raise ValueError(f'Invalid value for {key}: {value}') from e
61
+
62
+ # Validate required fields
63
+ if 'NodeGroupId' not in result:
64
+ raise ValueError('Missing required field: NodeGroupId')
65
+ if 'NewShardConfiguration' not in result:
66
+ raise ValueError('Missing required field: NewShardConfiguration')
67
+
68
+ return result
69
+
70
+
71
+ def parse_new_shard_config(config: str) -> Dict[str, Any]:
72
+ """Parse the NewShardConfiguration portion of resharding configuration.
73
+
74
+ Args:
75
+ config: String containing new shard configuration
76
+ Format: {NewReplicaCount=integer,PreferredAvailabilityZones=string1,string2}
77
+
78
+ Returns:
79
+ Dictionary containing the parsed new shard configuration
80
+
81
+ Raises:
82
+ ValueError: If the syntax is invalid
83
+ """
84
+ if not config.startswith('{') or not config.endswith('}'):
85
+ raise ValueError('NewShardConfiguration must be enclosed in curly braces')
86
+
87
+ # Remove curly braces
88
+ config = config[1:-1]
89
+
90
+ result = {}
91
+ pairs = config.split(',')
92
+
93
+ # Define valid keys and their processors
94
+ key_processors = {
95
+ 'NewReplicaCount': int,
96
+ 'PreferredAvailabilityZones': lambda x: x.split(','),
97
+ }
98
+
99
+ for pair in pairs:
100
+ if '=' not in pair:
101
+ raise ValueError(f'Invalid format. Each parameter must be in key=value format: {pair}')
102
+
103
+ key, value = pair.split('=', 1)
104
+ if not key or not value:
105
+ raise ValueError(f'Empty key or value: {pair}')
106
+
107
+ if key not in key_processors:
108
+ raise ValueError(f'Invalid parameter: {key}')
109
+
110
+ try:
111
+ result[key] = key_processors[key](value)
112
+ except ValueError as e:
113
+ raise ValueError(f'Invalid value for {key}: {value}') from e
114
+
115
+ # Validate required fields
116
+ if 'NewReplicaCount' not in result:
117
+ raise ValueError('Missing required field: NewReplicaCount')
118
+
119
+ return result
120
+
121
+
122
+ def parse_shorthand_nodegroup(group: str) -> Dict[str, Any]:
123
+ """Parse a single nodegroup from shorthand syntax.
124
+
125
+ Args:
126
+ group: Shorthand syntax string for a nodegroup
127
+
128
+ Returns:
129
+ Dictionary containing the parsed nodegroup configuration
130
+
131
+ Raises:
132
+ ValueError: If the syntax is invalid
133
+ """
134
+ if not group:
135
+ raise ValueError('Empty nodegroup configuration')
136
+
137
+ config = {}
138
+
139
+ # Define valid keys
140
+ valid_keys = {
141
+ 'NodeGroupId',
142
+ 'Slots',
143
+ 'ReplicaCount',
144
+ 'PrimaryAvailabilityZone',
145
+ 'ReplicaAvailabilityZones',
146
+ 'PrimaryOutpostArn',
147
+ 'ReplicaOutpostArns',
148
+ }
149
+
150
+ # Define keys that should be treated as arrays
151
+ array_keys = {'ReplicaAvailabilityZones', 'ReplicaOutpostArns'}
152
+
153
+ # Split into key-value pairs
154
+ pairs = group.split(',')
155
+ current_key = None
156
+ current_values = []
157
+
158
+ for pair in pairs:
159
+ pair = pair.strip()
160
+
161
+ # If this part contains an equals sign, it's a new key-value pair
162
+ if '=' in pair:
163
+ # Save any previous array values
164
+ if current_key in array_keys and current_values:
165
+ config[current_key] = current_values
166
+ current_values = []
167
+
168
+ key, value = pair.split('=', 1)
169
+ key = key.strip()
170
+ value = value.strip()
171
+
172
+ if not key or not value:
173
+ raise ValueError(f'Empty key or value: {pair}')
174
+
175
+ if key not in valid_keys:
176
+ raise ValueError(f'Invalid parameter: {key}')
177
+
178
+ current_key = key
179
+
180
+ try:
181
+ if key == 'ReplicaCount':
182
+ config[key] = int(value)
183
+ elif key in array_keys:
184
+ current_values = [value]
185
+ else:
186
+ config[key] = value
187
+ except ValueError as e:
188
+ raise ValueError(f'Invalid value for {key}: {value}') from e
189
+
190
+ # If no equals sign and we're in an array key context, treat as array value
191
+ elif current_key in array_keys:
192
+ current_values.append(pair)
193
+ else:
194
+ raise ValueError(f'Invalid format. Each parameter must be in key=value format: {pair}')
195
+
196
+ # Handle any remaining array values
197
+ if current_key in array_keys and current_values:
198
+ config[current_key] = current_values
199
+
200
+ # Validate required fields
201
+ if 'NodeGroupId' not in config:
202
+ raise ValueError('Missing required field: NodeGroupId')
203
+
204
+ return config
205
+
206
+
207
+ def parse_shorthand_log_delivery(config: str) -> Dict[str, Any]:
208
+ """Parse a single log delivery configuration from shorthand syntax.
209
+
210
+ Args:
211
+ config: Shorthand syntax string for log delivery configuration
212
+
213
+ Returns:
214
+ Dictionary containing the parsed log delivery configuration
215
+
216
+ Raises:
217
+ ValueError: If the syntax is invalid
218
+ """
219
+ if not config:
220
+ raise ValueError('Empty log delivery configuration')
221
+
222
+ result = {}
223
+ pairs = config.split(',')
224
+
225
+ # Define valid keys and their processors
226
+ key_processors = {
227
+ 'LogType': str,
228
+ 'DestinationType': str,
229
+ 'DestinationDetails': lambda x: json.loads(x.replace("'", '"')),
230
+ 'LogFormat': str,
231
+ 'Enabled': lambda x: x.lower() == 'true',
232
+ }
233
+
234
+ for pair in pairs:
235
+ if '=' not in pair:
236
+ raise ValueError(f'Invalid format. Each parameter must be in key=value format: {pair}')
237
+
238
+ key, value = pair.split('=', 1)
239
+ if not key or not value:
240
+ raise ValueError(f'Empty key or value: {pair}')
241
+
242
+ if key not in key_processors:
243
+ raise ValueError(f'Invalid parameter: {key}')
244
+
245
+ try:
246
+ result[key] = key_processors[key](value)
247
+ except ValueError as e:
248
+ raise ValueError(f'Invalid value for {key}: {value}') from e
249
+
250
+ # Validate required fields
251
+ required_fields = ['LogType', 'DestinationType', 'DestinationDetails', 'LogFormat', 'Enabled']
252
+ missing_fields = [field for field in required_fields if field not in result]
253
+ if missing_fields:
254
+ raise ValueError(f'Missing required fields: {", ".join(missing_fields)}')
255
+
256
+ # Validate LogType
257
+ if result['LogType'] not in ['slow-log', 'engine-log']:
258
+ raise ValueError("LogType must be either 'slow-log' or 'engine-log'")
259
+
260
+ # Validate DestinationType
261
+ if result['DestinationType'] not in ['cloudwatch-logs', 'kinesis-firehose']:
262
+ raise ValueError("DestinationType must be either 'cloudwatch-logs' or 'kinesis-firehose'")
263
+
264
+ # Validate LogFormat
265
+ if result['LogFormat'] not in ['text', 'json']:
266
+ raise ValueError("LogFormat must be either 'text' or 'json'")
267
+
268
+ return result
@@ -0,0 +1,227 @@
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
+ """Processor functions for ElastiCache replication group tools."""
16
+
17
+ from .parsers import (
18
+ parse_shorthand_log_delivery,
19
+ parse_shorthand_nodegroup,
20
+ parse_shorthand_resharding,
21
+ )
22
+ from typing import Dict, List, Union
23
+
24
+
25
+ def process_resharding_configuration(
26
+ resharding_configuration: Union[str, List[Dict]],
27
+ ) -> List[Dict]:
28
+ """Process resharding configuration in either shorthand or JSON format.
29
+
30
+ Args:
31
+ resharding_configuration: Resharding configuration in either format
32
+ Shorthand format: "NodeGroupId=string,NewShardConfiguration={NewReplicaCount=integer,PreferredAvailabilityZones=string1,string2}"
33
+ Multiple configurations can be separated by spaces.
34
+ JSON format: List of dictionaries with required fields:
35
+ - NodeGroupId: string
36
+ - NewShardConfiguration:
37
+ - NewReplicaCount: integer
38
+ - PreferredAvailabilityZones: list of strings (optional)
39
+
40
+ Returns:
41
+ List of processed resharding configurations
42
+
43
+ Raises:
44
+ ValueError: If the configuration is invalid
45
+ """
46
+ processed_configs = []
47
+
48
+ if isinstance(resharding_configuration, str):
49
+ # Parse shorthand syntax
50
+ configs = resharding_configuration.split(' ')
51
+ for config in configs:
52
+ if not config:
53
+ continue
54
+ try:
55
+ parsed_config = parse_shorthand_resharding(config)
56
+ processed_configs.append(parsed_config)
57
+ except ValueError as e:
58
+ raise ValueError(f'Invalid resharding shorthand syntax: {str(e)}')
59
+ else:
60
+ # Handle JSON format
61
+ if not isinstance(resharding_configuration, list):
62
+ raise ValueError(
63
+ 'Resharding configuration must be a list of dictionaries or a shorthand string'
64
+ )
65
+
66
+ for config in resharding_configuration:
67
+ if not isinstance(config, dict):
68
+ raise ValueError('Each resharding configuration must be a dictionary')
69
+
70
+ # Validate required fields
71
+ if 'NodeGroupId' not in config:
72
+ raise ValueError('Missing required field: NodeGroupId')
73
+ if 'NewShardConfiguration' not in config:
74
+ raise ValueError('Missing required field: NewShardConfiguration')
75
+
76
+ # Validate NewShardConfiguration
77
+ new_shard = config['NewShardConfiguration']
78
+ if not isinstance(new_shard, dict):
79
+ raise ValueError('NewShardConfiguration must be a dictionary')
80
+ if 'NewReplicaCount' not in new_shard:
81
+ raise ValueError(
82
+ 'Missing required field: NewReplicaCount in NewShardConfiguration'
83
+ )
84
+ if not isinstance(new_shard['NewReplicaCount'], int):
85
+ raise ValueError('NewReplicaCount must be an integer')
86
+
87
+ # Validate PreferredAvailabilityZones if present
88
+ if 'PreferredAvailabilityZones' in new_shard:
89
+ if not isinstance(new_shard['PreferredAvailabilityZones'], list):
90
+ raise ValueError('PreferredAvailabilityZones must be a list of strings')
91
+ for zone in new_shard['PreferredAvailabilityZones']:
92
+ if not isinstance(zone, str):
93
+ raise ValueError('Each availability zone must be a string')
94
+
95
+ processed_configs.append(config)
96
+
97
+ return processed_configs
98
+
99
+
100
+ def process_log_delivery_configurations(
101
+ log_delivery_configurations: Union[str, List[Dict]],
102
+ ) -> List[Dict]:
103
+ """Process log delivery configurations in either shorthand or JSON format.
104
+
105
+ Args:
106
+ log_delivery_configurations: Log delivery configurations in either format
107
+
108
+ Returns:
109
+ List of processed log delivery configurations
110
+
111
+ Raises:
112
+ ValueError: If the configuration is invalid
113
+ """
114
+ processed_configs = []
115
+
116
+ if isinstance(log_delivery_configurations, str):
117
+ # Parse shorthand syntax
118
+ configs = log_delivery_configurations.split(' ')
119
+ for config in configs:
120
+ if not config:
121
+ continue
122
+ try:
123
+ parsed_config = parse_shorthand_log_delivery(config)
124
+ processed_configs.append(parsed_config)
125
+ except ValueError as e:
126
+ raise ValueError(f'Invalid log delivery shorthand syntax: {str(e)}')
127
+ else:
128
+ # Handle JSON format
129
+ if not isinstance(log_delivery_configurations, list):
130
+ raise ValueError(
131
+ 'Log delivery configurations must be a list of dictionaries or a shorthand string'
132
+ )
133
+
134
+ for config in log_delivery_configurations:
135
+ if not isinstance(config, dict):
136
+ raise ValueError('Each log delivery configuration must be a dictionary')
137
+
138
+ # Validate required fields and types
139
+ required_fields = {
140
+ 'LogType': ['slow-log', 'engine-log'],
141
+ 'DestinationType': ['cloudwatch-logs', 'kinesis-firehose'],
142
+ 'DestinationDetails': dict,
143
+ 'LogFormat': ['text', 'json'],
144
+ 'Enabled': bool,
145
+ }
146
+
147
+ for field, valid_values in required_fields.items():
148
+ if field not in config:
149
+ raise ValueError(f'Missing required field: {field}')
150
+
151
+ if field in ['LogType', 'DestinationType', 'LogFormat']:
152
+ if config[field] not in valid_values:
153
+ raise ValueError(f'{field} must be one of {valid_values}')
154
+ elif field == 'DestinationDetails':
155
+ if not isinstance(config[field], valid_values):
156
+ raise ValueError(f'{field} must be a dictionary')
157
+ elif field == 'Enabled':
158
+ if not isinstance(config[field], valid_values):
159
+ raise ValueError(f'{field} must be a boolean')
160
+
161
+ processed_configs.append(config)
162
+
163
+ return processed_configs
164
+
165
+
166
+ def process_nodegroup_configuration(
167
+ node_group_configuration: Union[str, List[Dict]],
168
+ ) -> List[Dict]:
169
+ """Process nodegroup configuration in either shorthand or JSON format.
170
+
171
+ Args:
172
+ node_group_configuration: Nodegroup configuration in either format
173
+
174
+ Returns:
175
+ List of processed nodegroup configurations
176
+
177
+ Raises:
178
+ ValueError: If the configuration is invalid
179
+ """
180
+ processed_config = []
181
+
182
+ if isinstance(node_group_configuration, str):
183
+ # Parse shorthand syntax
184
+ groups = node_group_configuration.split(' ')
185
+ for group in groups:
186
+ if not group:
187
+ continue
188
+ try:
189
+ config = parse_shorthand_nodegroup(group)
190
+ processed_config.append(config)
191
+ except ValueError as e:
192
+ raise ValueError(f'Invalid nodegroup shorthand syntax: {str(e)}')
193
+ else:
194
+ # Handle JSON format
195
+ if not isinstance(node_group_configuration, list):
196
+ raise ValueError(
197
+ 'Node group configuration must be a list of dictionaries or a shorthand string'
198
+ )
199
+
200
+ for config in node_group_configuration:
201
+ if not isinstance(config, dict):
202
+ raise ValueError('Each node group configuration must be a dictionary')
203
+
204
+ # Validate required fields
205
+ if 'NodeGroupId' not in config:
206
+ raise ValueError('Missing required field: NodeGroupId')
207
+
208
+ # Process the configuration
209
+ processed_item = {}
210
+ for k, v in config.items():
211
+ if k == 'ReplicaCount':
212
+ try:
213
+ processed_item[k] = int(v)
214
+ except (ValueError, TypeError):
215
+ raise ValueError(f'ReplicaCount must be an integer: {v}')
216
+ elif k in ['ReplicaAvailabilityZones', 'ReplicaOutpostArns']:
217
+ if isinstance(v, str):
218
+ processed_item[k] = v.split(',')
219
+ elif isinstance(v, list):
220
+ processed_item[k] = v
221
+ else:
222
+ raise ValueError(f'{k} must be a string or list of strings')
223
+ else:
224
+ processed_item[k] = v
225
+ processed_config.append(processed_item)
226
+
227
+ return processed_config
@@ -0,0 +1,151 @@
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
+ """Start migration tool for ElastiCache MCP server."""
16
+
17
+ from ...common.connection import ElastiCacheConnectionManager
18
+ from ...common.decorators import handle_exceptions
19
+ from ...common.server import mcp
20
+ from ...context import Context
21
+ from pydantic import BaseModel, ConfigDict, Field
22
+ from typing import Any, Dict, List, Union
23
+
24
+
25
+ class CustomerNodeEndpoint(BaseModel):
26
+ """Customer node endpoint model."""
27
+
28
+ Address: str = Field(..., description='The address of the node endpoint')
29
+ Port: int = Field(..., description='The port of the node endpoint')
30
+ model_config = ConfigDict(validate_by_name=True, arbitrary_types_allowed=True)
31
+
32
+
33
+ class StartMigrationRequest(BaseModel):
34
+ """Request model for starting migration to an ElastiCache replication group."""
35
+
36
+ model_config = ConfigDict(validate_by_name=True, arbitrary_types_allowed=True)
37
+
38
+ replication_group_id: str = Field(
39
+ ..., description='The ID of the replication group to which data should be migrated'
40
+ )
41
+ customer_node_endpoint_list: Union[str, List[CustomerNodeEndpoint]] = Field(
42
+ ...,
43
+ description='List of endpoints from which data should be migrated. For Valkey or Redis OSS (cluster mode disabled), the list should have only one element.',
44
+ )
45
+
46
+
47
+ def prepare_request_dict(request: StartMigrationRequest) -> Dict[str, Any]:
48
+ """Prepare the request dictionary for the AWS API.
49
+
50
+ Args:
51
+ request: The StartMigrationRequest object
52
+
53
+ Returns:
54
+ Dict containing the properly formatted request parameters
55
+ """
56
+ # Start with required parameters
57
+ start_migration_request: Dict[str, Any] = {
58
+ 'ReplicationGroupId': request.replication_group_id,
59
+ }
60
+
61
+ # Process customer node endpoint list
62
+ if isinstance(request.customer_node_endpoint_list, str):
63
+ # Parse shorthand syntax: Address=string,Port=integer
64
+ try:
65
+ pairs = [
66
+ p.strip() for p in request.customer_node_endpoint_list.split(',') if p.strip()
67
+ ]
68
+ endpoint = {}
69
+ for pair in pairs:
70
+ if '=' not in pair:
71
+ raise ValueError(
72
+ 'Invalid endpoint format. Each parameter must be in key=value format'
73
+ )
74
+ key, value = pair.split('=', 1)
75
+ key = key.strip()
76
+ value = value.strip()
77
+ if not key or not value:
78
+ raise ValueError('Key or value cannot be empty')
79
+
80
+ if key == 'Address':
81
+ endpoint['Address'] = value
82
+ elif key == 'Port':
83
+ try:
84
+ endpoint['Port'] = int(value)
85
+ except ValueError:
86
+ raise ValueError(f'Port must be an integer: {value}')
87
+ else:
88
+ raise ValueError(f'Invalid parameter: {key}')
89
+
90
+ # Validate required fields
91
+ if 'Address' not in endpoint:
92
+ raise ValueError('Missing required field: Address')
93
+ if 'Port' not in endpoint:
94
+ raise ValueError('Missing required field: Port')
95
+
96
+ start_migration_request['CustomerNodeEndpointList'] = [endpoint]
97
+ except Exception as e:
98
+ raise ValueError(
99
+ f'Invalid endpoint shorthand syntax. Expected format: Address=string,Port=integer. Error: {str(e)}'
100
+ )
101
+ elif isinstance(request.customer_node_endpoint_list, list):
102
+ # Handle list format
103
+ if len(request.customer_node_endpoint_list) < 1:
104
+ raise ValueError('CustomerNodeEndpointList should have at least one element')
105
+
106
+ endpoints = [
107
+ endpoint.model_dump(exclude_none=True)
108
+ for endpoint in request.customer_node_endpoint_list
109
+ ]
110
+ start_migration_request['CustomerNodeEndpointList'] = endpoints
111
+ else:
112
+ raise ValueError(
113
+ 'CustomerNodeEndpointList must be a string or a list with at least one element'
114
+ )
115
+
116
+ return start_migration_request
117
+
118
+
119
+ @mcp.tool(name='start-migration')
120
+ @handle_exceptions
121
+ async def start_migration(request: StartMigrationRequest) -> Dict:
122
+ """Start migration to an Amazon ElastiCache replication group.
123
+
124
+ This tool starts migration from a Redis instance to an ElastiCache replication group.
125
+ It initiates the data migration process from the specified endpoint(s) to
126
+ the target replication group.
127
+
128
+ Args:
129
+ request: The StartMigrationRequest object containing:
130
+ - replication_group_id: The ID of the replication group to which data should be migrated
131
+ - customer_node_endpoint_list: List of endpoints from which data should be migrated.
132
+ For Valkey or Redis OSS (cluster mode disabled), the list should have only one element.
133
+
134
+ Returns:
135
+ Dict containing information about the migration start result.
136
+ """
137
+ # Check if readonly mode is enabled
138
+ if Context.readonly_mode():
139
+ raise ValueError(
140
+ 'You have configured this tool in readonly mode. To make this change you will have to update your configuration.'
141
+ )
142
+
143
+ # Get ElastiCache client
144
+ elasticache_client = ElastiCacheConnectionManager.get_connection()
145
+
146
+ # Prepare request dictionary
147
+ start_request = prepare_request_dict(request)
148
+
149
+ # Start the migration
150
+ response = elasticache_client.start_migration(**start_request)
151
+ return response