awslabs.elasticache-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.
@@ -19,7 +19,7 @@ from ...common.decorators import handle_exceptions
19
19
  from ...common.server import mcp
20
20
  from ...context import Context
21
21
  from botocore.exceptions import ClientError
22
- from typing import Any, Dict, Tuple, Union
22
+ from typing import Any, Dict, Optional, Tuple, Union
23
23
 
24
24
 
25
25
  async def _configure_security_groups(
@@ -252,18 +252,20 @@ async def get_ssh_tunnel_command_cc(
252
252
  @handle_exceptions
253
253
  async def create_jump_host_cc(
254
254
  cache_cluster_id: str,
255
- subnet_id: str,
256
- security_group_id: str,
257
255
  key_name: str,
256
+ subnet_id: Optional[str] = None,
257
+ security_group_id: Optional[str] = None,
258
258
  instance_type: str = 't3.small',
259
259
  ) -> Dict[str, Any]:
260
260
  """Creates an EC2 jump host instance to access an ElastiCache cluster via SSH tunnel.
261
261
 
262
262
  Args:
263
263
  cache_cluster_id (str): ID of the ElastiCache cluster to connect to
264
- subnet_id (str): ID of the subnet to launch the EC2 instance in (must be public)
265
- security_group_id (str): ID of the security group to assign to the EC2 instance
266
264
  key_name (str): Name of the EC2 key pair to use for SSH access
265
+ subnet_id (str, optional): ID of the subnet to launch the EC2 instance in (must be public).
266
+ If not provided and cache uses default VPC, will auto-select a default subnet.
267
+ security_group_id (str, optional): ID of the security group to assign to the EC2 instance.
268
+ If not provided and cache uses default VPC, will use the default security group.
267
269
  instance_type (str, optional): EC2 instance type. Defaults to "t3.small"
268
270
 
269
271
  Returns:
@@ -306,6 +308,59 @@ async def create_jump_host_cc(
306
308
  )['CacheSubnetGroups'][0]
307
309
  cache_vpc_id = cache_subnet_group['VpcId']
308
310
 
311
+ # Check if cache is in default VPC
312
+ vpcs = ec2_client.describe_vpcs(VpcIds=[cache_vpc_id])['Vpcs']
313
+ cache_vpc = vpcs[0] if vpcs else None
314
+ is_default_vpc = cache_vpc and cache_vpc.get('IsDefault', False)
315
+
316
+ # Auto-select subnet if not provided and cache is in default VPC
317
+ if not subnet_id and is_default_vpc:
318
+ # Get default subnets in the default VPC
319
+ subnets = ec2_client.describe_subnets(
320
+ Filters=[
321
+ {'Name': 'vpc-id', 'Values': [cache_vpc_id]},
322
+ {'Name': 'default-for-az', 'Values': ['true']},
323
+ ]
324
+ )['Subnets']
325
+
326
+ if subnets:
327
+ # Pick the first available default subnet
328
+ subnet_id = subnets[0]['SubnetId']
329
+ else:
330
+ # Fallback to any public subnet in the VPC
331
+ all_subnets = ec2_client.describe_subnets(
332
+ Filters=[{'Name': 'vpc-id', 'Values': [cache_vpc_id]}]
333
+ )['Subnets']
334
+
335
+ for subnet in all_subnets:
336
+ if subnet.get('MapPublicIpOnLaunch', False):
337
+ subnet_id = subnet['SubnetId']
338
+ break
339
+
340
+ # Auto-select security group if not provided and cache is in default VPC
341
+ if not security_group_id and is_default_vpc:
342
+ # Get the default security group for the VPC
343
+ security_groups = ec2_client.describe_security_groups(
344
+ Filters=[
345
+ {'Name': 'vpc-id', 'Values': [cache_vpc_id]},
346
+ {'Name': 'group-name', 'Values': ['default']},
347
+ ]
348
+ )['SecurityGroups']
349
+
350
+ if security_groups:
351
+ security_group_id = security_groups[0]['GroupId']
352
+
353
+ # Validate required parameters after auto-selection
354
+ if not subnet_id:
355
+ raise ValueError(
356
+ 'subnet_id is required. Either provide a subnet_id or ensure the cache cluster is in the default VPC with default subnets available.'
357
+ )
358
+
359
+ if not security_group_id:
360
+ raise ValueError(
361
+ 'security_group_id is required. Either provide a security_group_id or ensure the cache cluster is in the default VPC.'
362
+ )
363
+
309
364
  # Get subnet details and verify it's public
310
365
  subnet_response = ec2_client.describe_subnets(SubnetIds=[subnet_id])
311
366
  subnet = subnet_response['Subnets'][0]
@@ -318,6 +373,7 @@ async def create_jump_host_cc(
318
373
  )
319
374
 
320
375
  # Check if subnet is public by looking for route to internet gateway
376
+ # or if it's a default subnet in the default VPC (which are automatically public)
321
377
  route_tables = ec2_client.describe_route_tables(
322
378
  Filters=[{'Name': 'association.subnet-id', 'Values': [subnet_id]}]
323
379
  )['RouteTables']
@@ -331,9 +387,38 @@ async def create_jump_host_cc(
331
387
  if is_public:
332
388
  break
333
389
 
390
+ # If no explicit route table association, check the main route table for the VPC
391
+ if not is_public and not route_tables:
392
+ main_route_tables = ec2_client.describe_route_tables(
393
+ Filters=[
394
+ {'Name': 'vpc-id', 'Values': [subnet_vpc_id]},
395
+ {'Name': 'association.main', 'Values': ['true']},
396
+ ]
397
+ )['RouteTables']
398
+
399
+ for rt in main_route_tables:
400
+ for route in rt.get('Routes', []):
401
+ if route.get('GatewayId', '').startswith('igw-'):
402
+ is_public = True
403
+ break
404
+ if is_public:
405
+ break
406
+
407
+ # If not found via route table, check if it's a default subnet in default VPC
408
+ if not is_public:
409
+ # Check if this is the default VPC
410
+ vpcs = ec2_client.describe_vpcs(VpcIds=[subnet_vpc_id])['Vpcs']
411
+ vpc = vpcs[0] if vpcs else None
412
+
413
+ if vpc and vpc.get('IsDefault', False):
414
+ # In default VPC, check if this is a default subnet
415
+ # Default subnets have MapPublicIpOnLaunch set to True
416
+ if subnet.get('DefaultForAz', False) or subnet.get('MapPublicIpOnLaunch', False):
417
+ is_public = True
418
+
334
419
  if not is_public:
335
420
  raise ValueError(
336
- f'Subnet {subnet_id} is not public (no route to internet gateway found). '
421
+ f'Subnet {subnet_id} is not public (no route to internet gateway found and not a default subnet in default VPC). '
337
422
  'The subnet must be public to allow SSH access to the jump host.'
338
423
  )
339
424
 
@@ -65,7 +65,17 @@ async def get_metric_statistics(
65
65
 
66
66
  # Add optional parameters
67
67
  if dimensions:
68
- params['Dimensions'] = [{'Name': k, 'Value': v} for d in dimensions for k, v in d.items()]
68
+ # Ensure dimensions are properly formatted as [{'Name': name, 'Value': value}, ...]
69
+ formatted_dimensions = []
70
+ for d in dimensions:
71
+ # Check if the dimension is already in the correct format
72
+ if 'Name' in d and 'Value' in d:
73
+ formatted_dimensions.append(d)
74
+ else:
75
+ # Convert from {key: value} format to {'Name': key, 'Value': value}
76
+ for k, v in d.items():
77
+ formatted_dimensions.append({'Name': k, 'Value': v})
78
+ params['Dimensions'] = formatted_dimensions
69
79
  if statistics:
70
80
  params['Statistics'] = statistics
71
81
  if extended_statistics:
@@ -19,7 +19,7 @@ from ...common.decorators import handle_exceptions
19
19
  from ...common.server import mcp
20
20
  from ...context import Context
21
21
  from botocore.exceptions import ClientError
22
- from typing import Any, Dict, List, Tuple, Union
22
+ from typing import Any, Dict, Optional, Tuple, Union
23
23
 
24
24
 
25
25
  async def _configure_security_groups(
@@ -52,28 +52,21 @@ async def _configure_security_groups(
52
52
  ReplicationGroupId=replication_group_id
53
53
  )['ReplicationGroups'][0]
54
54
 
55
- # Get primary cluster details
56
- primary_cluster_id = None
57
- for member in replication_group['MemberClusters']:
58
- cluster = elasticache_client.describe_cache_clusters(
59
- CacheClusterId=member, ShowCacheNodeInfo=True
60
- )['CacheClusters'][0]
61
- if cluster['CacheClusterRole'].lower() == 'primary':
62
- primary_cluster_id = member
63
- break
55
+ # Get first cluster details (MemberClusters doesn't have notion of primary cluster)
56
+ if not replication_group['MemberClusters']:
57
+ raise ValueError(f'No clusters found in replication group {replication_group_id}')
64
58
 
65
- if not primary_cluster_id:
66
- raise ValueError(f'No primary cluster found in replication group {replication_group_id}')
59
+ first_cluster_id = replication_group['MemberClusters'][0]
67
60
 
68
- # Get cache cluster VPC ID from primary cluster
69
- primary_cluster = elasticache_client.describe_cache_clusters(
70
- CacheClusterId=primary_cluster_id, ShowCacheNodeInfo=True
61
+ # Get cache cluster VPC ID from first cluster
62
+ first_cluster = elasticache_client.describe_cache_clusters(
63
+ CacheClusterId=first_cluster_id, ShowCacheNodeInfo=True
71
64
  )['CacheClusters'][0]
72
65
 
73
- # Get subnet group name from cluster
74
- subnet_group_name = primary_cluster.get('CacheSubnetGroupName')
66
+ # Get subnet group name from first cluster
67
+ subnet_group_name = first_cluster.get('CacheSubnetGroupName')
75
68
  if not subnet_group_name:
76
- raise ValueError(f'No cache subnet group found for cluster {primary_cluster_id}')
69
+ raise ValueError(f'No cache subnet group found for cluster {first_cluster_id}')
77
70
 
78
71
  # Get VPC ID from subnet group
79
72
  try:
@@ -98,8 +91,8 @@ async def _configure_security_groups(
98
91
  f'EC2 instance VPC ({instance_vpc_id}) does not match replication group VPC ({cache_vpc_id})'
99
92
  )
100
93
 
101
- # Get cache cluster port from primary node
102
- cache_port = primary_cluster['CacheNodes'][0]['Endpoint']['Port']
94
+ # Get cache cluster port from first node
95
+ cache_port = first_cluster['CacheNodes'][0]['Endpoint']['Port']
103
96
 
104
97
  # Get cache cluster security groups from all member clusters
105
98
  cache_security_groups = set()
@@ -209,15 +202,15 @@ async def connect_jump_host_rg(replication_group_id: str, instance_id: str) -> D
209
202
  @handle_exceptions
210
203
  async def get_ssh_tunnel_command_rg(
211
204
  replication_group_id: str, instance_id: str
212
- ) -> Dict[str, Union[str, int, List[Dict[str, Any]], None]]:
213
- """Generates SSH tunnel commands to connect to an ElastiCache replication group through an EC2 jump host.
205
+ ) -> Dict[str, Union[str, int]]:
206
+ """Generates an SSH tunnel command to connect to an ElastiCache replication group through an EC2 jump host.
214
207
 
215
208
  Args:
216
209
  replication_group_id (str): ID of the ElastiCache replication group to connect to
217
210
  instance_id (str): ID of the EC2 instance to use as jump host
218
211
 
219
212
  Returns:
220
- Dict[str, Union[str, int, List[Dict[str, Any]]]]: Dictionary containing SSH tunnel commands and related details
213
+ Dict[str, Union[str, int]]: Dictionary containing the SSH tunnel command and related details
221
214
 
222
215
  Raises:
223
216
  ValueError: If required resources not found or information cannot be retrieved
@@ -256,52 +249,28 @@ async def get_ssh_tunnel_command_rg(
256
249
  ReplicationGroupId=replication_group_id
257
250
  )['ReplicationGroups'][0]
258
251
 
259
- # Get node details for all clusters
260
- node_commands = []
261
- base_port = None
262
-
263
- for member in replication_group['MemberClusters']:
264
- cluster = elasticache_client.describe_cache_clusters(
265
- CacheClusterId=member, ShowCacheNodeInfo=True
266
- )['CacheClusters'][0]
267
-
268
- if not cluster.get('CacheNodes'):
269
- continue
270
-
271
- node = cluster['CacheNodes'][0]
272
- endpoint = node['Endpoint']['Address']
273
- port = node['Endpoint']['Port']
274
-
275
- if base_port is None:
276
- base_port = port
277
-
278
- # For replicas, use different local ports to avoid conflicts
279
- local_port = port
280
- if cluster['CacheClusterRole'].lower() != 'primary':
281
- local_port = port + 1000 # Use a different port range for replicas
282
-
283
- ssh_command = (
284
- f'ssh -i "{key_name}.pem" -fN -l {user} '
285
- f'-L {local_port}:{endpoint}:{port} {public_dns} -v'
252
+ # Use the ConfigurationEndpoint for the SSH tunnel
253
+ if 'ConfigurationEndpoint' not in replication_group:
254
+ raise ValueError(
255
+ f'No ConfigurationEndpoint found for replication group {replication_group_id}'
286
256
  )
287
257
 
288
- node_commands.append(
289
- {
290
- 'role': cluster['CacheClusterRole'],
291
- 'clusterId': member,
292
- 'command': ssh_command,
293
- 'localPort': local_port,
294
- 'remoteEndpoint': endpoint,
295
- 'remotePort': port,
296
- }
297
- )
258
+ endpoint = replication_group['ConfigurationEndpoint']['Address']
259
+ port = replication_group['ConfigurationEndpoint']['Port']
260
+
261
+ # Generate a single SSH tunnel command
262
+ ssh_command = (
263
+ f'ssh -i "{key_name}.pem" -fN -l {user} -L {port}:{endpoint}:{port} {public_dns} -v'
264
+ )
298
265
 
299
266
  return {
267
+ 'command': ssh_command,
300
268
  'keyName': key_name,
301
269
  'user': user,
302
270
  'jumpHostDns': public_dns,
303
- 'nodes': node_commands,
304
- 'basePort': base_port,
271
+ 'localPort': port,
272
+ 'remoteEndpoint': endpoint,
273
+ 'remotePort': port,
305
274
  }
306
275
 
307
276
  except Exception as e:
@@ -312,18 +281,20 @@ async def get_ssh_tunnel_command_rg(
312
281
  @handle_exceptions
313
282
  async def create_jump_host_rg(
314
283
  replication_group_id: str,
315
- subnet_id: str,
316
- security_group_id: str,
317
284
  key_name: str,
285
+ subnet_id: Optional[str] = None,
286
+ security_group_id: Optional[str] = None,
318
287
  instance_type: str = 't3.small',
319
288
  ) -> Dict[str, Any]:
320
289
  """Creates an EC2 jump host instance to access an ElastiCache replication group via SSH tunnel.
321
290
 
322
291
  Args:
323
292
  replication_group_id (str): ID of the ElastiCache replication group to connect to
324
- subnet_id (str): ID of the subnet to launch the EC2 instance in (must be public)
325
- security_group_id (str): ID of the security group to assign to the EC2 instance
326
293
  key_name (str): Name of the EC2 key pair to use for SSH access
294
+ subnet_id (str, optional): ID of the subnet to launch the EC2 instance in (must be public).
295
+ If not provided and replication group uses default VPC, will auto-select a default subnet.
296
+ security_group_id (str, optional): ID of the security group to assign to the EC2 instance.
297
+ If not provided and replication group uses default VPC, will use the default security group.
327
298
  instance_type (str, optional): EC2 instance type. Defaults to "t3.small"
328
299
 
329
300
  Returns:
@@ -361,31 +332,75 @@ async def create_jump_host_rg(
361
332
  ReplicationGroupId=replication_group_id
362
333
  )['ReplicationGroups'][0]
363
334
 
364
- # Get primary cluster details
365
- primary_cluster_id = None
366
- for member in replication_group['MemberClusters']:
367
- cluster = elasticache_client.describe_cache_clusters(
368
- CacheClusterId=member, ShowCacheNodeInfo=True
369
- )['CacheClusters'][0]
370
- if cluster['CacheClusterRole'].lower() == 'primary':
371
- primary_cluster_id = member
372
- break
335
+ # Get first cluster details (MemberClusters doesn't have notion of primary cluster)
336
+ if not replication_group['MemberClusters']:
337
+ raise ValueError(f'No clusters found in replication group {replication_group_id}')
373
338
 
374
- if not primary_cluster_id:
375
- raise ValueError(
376
- f'No primary cluster found in replication group {replication_group_id}'
377
- )
339
+ first_cluster_id = replication_group['MemberClusters'][0]
378
340
 
379
- # Get VPC details from primary cluster
380
- primary_cluster = elasticache_client.describe_cache_clusters(
381
- CacheClusterId=primary_cluster_id, ShowCacheNodeInfo=True
341
+ # Get VPC details from first cluster
342
+ first_cluster = elasticache_client.describe_cache_clusters(
343
+ CacheClusterId=first_cluster_id, ShowCacheNodeInfo=True
382
344
  )['CacheClusters'][0]
383
345
 
384
346
  cache_subnet_group = elasticache_client.describe_cache_subnet_groups(
385
- CacheSubnetGroupName=primary_cluster['CacheSubnetGroupName']
347
+ CacheSubnetGroupName=first_cluster['CacheSubnetGroupName']
386
348
  )['CacheSubnetGroups'][0]
387
349
  cache_vpc_id = cache_subnet_group['VpcId']
388
350
 
351
+ # Check if replication group is in default VPC
352
+ vpcs = ec2_client.describe_vpcs(VpcIds=[cache_vpc_id])['Vpcs']
353
+ cache_vpc = vpcs[0] if vpcs else None
354
+ is_default_vpc = cache_vpc and cache_vpc.get('IsDefault', False)
355
+
356
+ # Auto-select subnet if not provided and replication group is in default VPC
357
+ if not subnet_id and is_default_vpc:
358
+ # Get default subnets in the default VPC
359
+ subnets = ec2_client.describe_subnets(
360
+ Filters=[
361
+ {'Name': 'vpc-id', 'Values': [cache_vpc_id]},
362
+ {'Name': 'default-for-az', 'Values': ['true']},
363
+ ]
364
+ )['Subnets']
365
+
366
+ if subnets:
367
+ # Pick the first available default subnet
368
+ subnet_id = subnets[0]['SubnetId']
369
+ else:
370
+ # Fallback to any public subnet in the VPC
371
+ all_subnets = ec2_client.describe_subnets(
372
+ Filters=[{'Name': 'vpc-id', 'Values': [cache_vpc_id]}]
373
+ )['Subnets']
374
+
375
+ for subnet in all_subnets:
376
+ if subnet.get('MapPublicIpOnLaunch', False):
377
+ subnet_id = subnet['SubnetId']
378
+ break
379
+
380
+ # Auto-select security group if not provided and replication group is in default VPC
381
+ if not security_group_id and is_default_vpc:
382
+ # Get the default security group for the VPC
383
+ security_groups = ec2_client.describe_security_groups(
384
+ Filters=[
385
+ {'Name': 'vpc-id', 'Values': [cache_vpc_id]},
386
+ {'Name': 'group-name', 'Values': ['default']},
387
+ ]
388
+ )['SecurityGroups']
389
+
390
+ if security_groups:
391
+ security_group_id = security_groups[0]['GroupId']
392
+
393
+ # Validate required parameters after auto-selection
394
+ if not subnet_id:
395
+ raise ValueError(
396
+ 'subnet_id is required. Either provide a subnet_id or ensure the replication group is in the default VPC with default subnets available.'
397
+ )
398
+
399
+ if not security_group_id:
400
+ raise ValueError(
401
+ 'security_group_id is required. Either provide a security_group_id or ensure the replication group is in the default VPC.'
402
+ )
403
+
389
404
  # Get subnet details and verify it's public
390
405
  subnet_response = ec2_client.describe_subnets(SubnetIds=[subnet_id])
391
406
  subnet = subnet_response['Subnets'][0]
@@ -398,20 +413,11 @@ async def create_jump_host_rg(
398
413
  )
399
414
 
400
415
  # Check if subnet is public by looking for route to internet gateway
416
+ # or if it's a default subnet in the default VPC (which are automatically public)
401
417
  route_tables = ec2_client.describe_route_tables(
402
418
  Filters=[{'Name': 'association.subnet-id', 'Values': [subnet_id]}]
403
419
  )['RouteTables']
404
420
 
405
- # If no explicit route table association, check main route table
406
- if not route_tables:
407
- route_tables = ec2_client.describe_route_tables(
408
- Filters=[
409
- {'Name': 'vpc-id', 'Values': [subnet_vpc_id]},
410
- {'Name': 'association.main', 'Values': ['true']},
411
- ]
412
- )['RouteTables']
413
-
414
- # Check for route to internet gateway
415
421
  is_public = False
416
422
  for rt in route_tables:
417
423
  for route in rt.get('Routes', []):
@@ -421,10 +427,38 @@ async def create_jump_host_rg(
421
427
  if is_public:
422
428
  break
423
429
 
424
- # Raise error if no route to internet gateway found
430
+ # If no explicit route table association, check the main route table for the VPC
431
+ if not is_public and not route_tables:
432
+ main_route_tables = ec2_client.describe_route_tables(
433
+ Filters=[
434
+ {'Name': 'vpc-id', 'Values': [subnet_vpc_id]},
435
+ {'Name': 'association.main', 'Values': ['true']},
436
+ ]
437
+ )['RouteTables']
438
+
439
+ for rt in main_route_tables:
440
+ for route in rt.get('Routes', []):
441
+ if route.get('GatewayId', '').startswith('igw-'):
442
+ is_public = True
443
+ break
444
+ if is_public:
445
+ break
446
+
447
+ # If not found via route table, check if it's a default subnet in default VPC
448
+ if not is_public:
449
+ # Check if this is the default VPC
450
+ vpcs = ec2_client.describe_vpcs(VpcIds=[subnet_vpc_id])['Vpcs']
451
+ vpc = vpcs[0] if vpcs else None
452
+
453
+ if vpc and vpc.get('IsDefault', False):
454
+ # In default VPC, check if this is a default subnet
455
+ # Default subnets have MapPublicIpOnLaunch set to True
456
+ if subnet.get('DefaultForAz', False) or subnet.get('MapPublicIpOnLaunch', False):
457
+ is_public = True
458
+
425
459
  if not is_public:
426
460
  raise ValueError(
427
- f'Subnet {subnet_id} is not public (no route to internet gateway found). '
461
+ f'Subnet {subnet_id} is not public (no route to internet gateway found and not a default subnet in default VPC). '
428
462
  'The subnet must be public to allow SSH access to the jump host.'
429
463
  )
430
464
 
@@ -19,7 +19,7 @@ from ...common.decorators import handle_exceptions
19
19
  from ...common.server import mcp
20
20
  from ...context import Context
21
21
  from botocore.exceptions import ClientError
22
- from typing import Any, Dict, Tuple, Union
22
+ from typing import Any, Dict, Optional, Tuple, Union
23
23
 
24
24
 
25
25
  async def _configure_security_groups(
@@ -53,15 +53,28 @@ async def _configure_security_groups(
53
53
  )['ServerlessCaches'][0]
54
54
 
55
55
  # Get cache security groups
56
- cache_security_groups = serverless_cache['VpcSecurityGroups']
56
+ cache_security_groups = serverless_cache['SecurityGroupIds']
57
57
  if not cache_security_groups:
58
58
  raise ValueError(f'No security groups found for serverless cache {serverless_cache_name}')
59
59
 
60
- # Get cache VPC ID
61
- cache_vpc_id = cache_security_groups[0]['VpcId']
60
+ # Get cache VPC ID from subnet IDs
61
+ if not serverless_cache.get('SubnetIds'):
62
+ raise ValueError(f'No subnet IDs found for serverless cache {serverless_cache_name}')
62
63
 
63
- # Get cache port (default is 6379 for Redis)
64
- cache_port = 6379
64
+ # Get subnet details to find VPC ID
65
+ subnet_response = ec2_client.describe_subnets(SubnetIds=[serverless_cache['SubnetIds'][0]])
66
+ cache_vpc_id = subnet_response['Subnets'][0]['VpcId']
67
+
68
+ # Get cache port dynamically from endpoint if available
69
+ # Set default port based on engine type
70
+ engine = serverless_cache.get('Engine', '').lower()
71
+ if engine == 'memcached':
72
+ cache_port = 11211 # Default port for Memcached
73
+ else:
74
+ cache_port = 6379 # Default port for Redis/Valkey
75
+
76
+ if serverless_cache.get('Endpoint') and serverless_cache['Endpoint'].get('Port'):
77
+ cache_port = serverless_cache['Endpoint']['Port']
65
78
 
66
79
  # Get EC2 instance details
67
80
  instance_info = ec2_client.describe_instances(InstanceIds=[instance_id])
@@ -83,8 +96,7 @@ async def _configure_security_groups(
83
96
  raise ValueError(f'No security groups found for EC2 instance {instance_id}')
84
97
 
85
98
  # For each cache security group, ensure it allows inbound access from EC2 security groups
86
- for cache_sg in cache_security_groups:
87
- cache_sg_id = cache_sg['SecurityGroupId']
99
+ for cache_sg_id in cache_security_groups:
88
100
  cache_sg_info = ec2_client.describe_security_groups(GroupIds=[cache_sg_id])[
89
101
  'SecurityGroups'
90
102
  ][0]
@@ -223,9 +235,19 @@ async def get_ssh_tunnel_command_serverless(
223
235
  ServerlessCacheName=serverless_cache_name
224
236
  )['ServerlessCaches'][0]
225
237
 
226
- # Get cache endpoint and port (default port is 6379 for Redis)
238
+ # Get cache endpoint and port
227
239
  cache_endpoint = serverless_cache['Endpoint']['Address']
228
- cache_port = 6379
240
+
241
+ # Get cache port dynamically from endpoint if available
242
+ # Set default port based on engine type
243
+ engine = serverless_cache.get('Engine', '').lower()
244
+ if engine == 'memcached':
245
+ cache_port = 11211 # Default port for Memcached
246
+ else:
247
+ cache_port = 6379 # Default port for Redis/Valkey
248
+
249
+ if serverless_cache.get('Endpoint') and serverless_cache['Endpoint'].get('Port'):
250
+ cache_port = serverless_cache['Endpoint']['Port']
229
251
 
230
252
  # Generate SSH tunnel command
231
253
  ssh_command = (
@@ -251,18 +273,20 @@ async def get_ssh_tunnel_command_serverless(
251
273
  @handle_exceptions
252
274
  async def create_jump_host_serverless(
253
275
  serverless_cache_name: str,
254
- subnet_id: str,
255
- security_group_id: str,
256
276
  key_name: str,
277
+ subnet_id: Optional[str] = None,
278
+ security_group_id: Optional[str] = None,
257
279
  instance_type: str = 't3.small',
258
280
  ) -> Dict[str, Any]:
259
281
  """Creates an EC2 jump host instance to access an ElastiCache serverless cache via SSH tunnel.
260
282
 
261
283
  Args:
262
284
  serverless_cache_name (str): Name of the ElastiCache serverless cache to connect to
263
- subnet_id (str): ID of the subnet to launch the EC2 instance in (must be public)
264
- security_group_id (str): ID of the security group to assign to the EC2 instance
265
285
  key_name (str): Name of the EC2 key pair to use for SSH access
286
+ subnet_id (str, optional): ID of the subnet to launch the EC2 instance in (must be public).
287
+ If not provided and serverless cache uses default VPC, will auto-select a default subnet.
288
+ security_group_id (str, optional): ID of the security group to assign to the EC2 instance.
289
+ If not provided and serverless cache uses default VPC, will use the default security group.
266
290
  instance_type (str, optional): EC2 instance type. Defaults to "t3.small"
267
291
 
268
292
  Returns:
@@ -301,14 +325,72 @@ async def create_jump_host_serverless(
301
325
  )['ServerlessCaches'][0]
302
326
 
303
327
  # Get cache security groups
304
- cache_security_groups = serverless_cache['VpcSecurityGroups']
328
+ cache_security_groups = serverless_cache['SecurityGroupIds']
305
329
  if not cache_security_groups:
306
330
  raise ValueError(
307
331
  f'No security groups found for serverless cache {serverless_cache_name}'
308
332
  )
309
333
 
310
- # Get cache VPC ID
311
- cache_vpc_id = cache_security_groups[0]['VpcId']
334
+ # Get cache VPC ID from subnet IDs
335
+ if not serverless_cache.get('SubnetIds'):
336
+ raise ValueError(f'No subnet IDs found for serverless cache {serverless_cache_name}')
337
+
338
+ # Get subnet details to find VPC ID
339
+ subnet_response = ec2_client.describe_subnets(SubnetIds=[serverless_cache['SubnetIds'][0]])
340
+ cache_vpc_id = subnet_response['Subnets'][0]['VpcId']
341
+
342
+ # Check if serverless cache is in default VPC
343
+ vpcs = ec2_client.describe_vpcs(VpcIds=[cache_vpc_id])['Vpcs']
344
+ cache_vpc = vpcs[0] if vpcs else None
345
+ is_default_vpc = cache_vpc and cache_vpc.get('IsDefault', False)
346
+
347
+ # Auto-select subnet if not provided and serverless cache is in default VPC
348
+ if not subnet_id and is_default_vpc:
349
+ # Get default subnets in the default VPC
350
+ subnets = ec2_client.describe_subnets(
351
+ Filters=[
352
+ {'Name': 'vpc-id', 'Values': [cache_vpc_id]},
353
+ {'Name': 'default-for-az', 'Values': ['true']},
354
+ ]
355
+ )['Subnets']
356
+
357
+ if subnets:
358
+ # Pick the first available default subnet
359
+ subnet_id = subnets[0]['SubnetId']
360
+ else:
361
+ # Fallback to any public subnet in the VPC
362
+ all_subnets = ec2_client.describe_subnets(
363
+ Filters=[{'Name': 'vpc-id', 'Values': [cache_vpc_id]}]
364
+ )['Subnets']
365
+
366
+ for subnet in all_subnets:
367
+ if subnet.get('MapPublicIpOnLaunch', False):
368
+ subnet_id = subnet['SubnetId']
369
+ break
370
+
371
+ # Auto-select security group if not provided and serverless cache is in default VPC
372
+ if not security_group_id and is_default_vpc:
373
+ # Get the default security group for the VPC
374
+ security_groups = ec2_client.describe_security_groups(
375
+ Filters=[
376
+ {'Name': 'vpc-id', 'Values': [cache_vpc_id]},
377
+ {'Name': 'group-name', 'Values': ['default']},
378
+ ]
379
+ )['SecurityGroups']
380
+
381
+ if security_groups:
382
+ security_group_id = security_groups[0]['GroupId']
383
+
384
+ # Validate required parameters after auto-selection
385
+ if not subnet_id:
386
+ raise ValueError(
387
+ 'subnet_id is required. Either provide a subnet_id or ensure the serverless cache is in the default VPC with default subnets available.'
388
+ )
389
+
390
+ if not security_group_id:
391
+ raise ValueError(
392
+ 'security_group_id is required. Either provide a security_group_id or ensure the serverless cache is in the default VPC.'
393
+ )
312
394
 
313
395
  # Get subnet details and verify it's public
314
396
  subnet_response = ec2_client.describe_subnets(SubnetIds=[subnet_id])
@@ -322,6 +404,7 @@ async def create_jump_host_serverless(
322
404
  )
323
405
 
324
406
  # Check if subnet is public by looking for route to internet gateway
407
+ # or if it's a default subnet in the default VPC (which are automatically public)
325
408
  route_tables = ec2_client.describe_route_tables(
326
409
  Filters=[{'Name': 'association.subnet-id', 'Values': [subnet_id]}]
327
410
  )['RouteTables']
@@ -335,10 +418,38 @@ async def create_jump_host_serverless(
335
418
  if is_public:
336
419
  break
337
420
 
338
- # Raise error if no route to internet gateway found
421
+ # If no explicit route table association, check the main route table for the VPC
422
+ if not is_public and not route_tables:
423
+ main_route_tables = ec2_client.describe_route_tables(
424
+ Filters=[
425
+ {'Name': 'vpc-id', 'Values': [subnet_vpc_id]},
426
+ {'Name': 'association.main', 'Values': ['true']},
427
+ ]
428
+ )['RouteTables']
429
+
430
+ for rt in main_route_tables:
431
+ for route in rt.get('Routes', []):
432
+ if route.get('GatewayId', '').startswith('igw-'):
433
+ is_public = True
434
+ break
435
+ if is_public:
436
+ break
437
+
438
+ # If not found via route table, check if it's a default subnet in default VPC
439
+ if not is_public:
440
+ # Check if this is the default VPC
441
+ vpcs = ec2_client.describe_vpcs(VpcIds=[subnet_vpc_id])['Vpcs']
442
+ vpc = vpcs[0] if vpcs else None
443
+
444
+ if vpc and vpc.get('IsDefault', False):
445
+ # In default VPC, check if this is a default subnet
446
+ # Default subnets have MapPublicIpOnLaunch set to True
447
+ if subnet.get('DefaultForAz', False) or subnet.get('MapPublicIpOnLaunch', False):
448
+ is_public = True
449
+
339
450
  if not is_public:
340
451
  raise ValueError(
341
- f'Subnet {subnet_id} is not public (no route to internet gateway found). '
452
+ f'Subnet {subnet_id} is not public (no route to internet gateway found and not a default subnet in default VPC). '
342
453
  'The subnet must be public to allow SSH access to the jump host.'
343
454
  )
344
455
 
@@ -22,7 +22,7 @@ from .models import CreateServerlessCacheRequest
22
22
  from typing import Dict
23
23
 
24
24
 
25
- @mcp.tool(name='create_serverless_cache')
25
+ @mcp.tool(name='create-serverless-cache')
26
26
  @handle_exceptions
27
27
  async def create_serverless_cache(request: CreateServerlessCacheRequest) -> Dict:
28
28
  """Create a new Amazon ElastiCache serverless cache.
@@ -54,7 +54,7 @@ async def create_serverless_cache(request: CreateServerlessCacheRequest) -> Dict
54
54
  3. JSON array: [{"Key": "string", "Value": "string"}, {"Key": "string2", "Value": null}]
55
55
 
56
56
  Can be None if no tags are needed.
57
- vpc_security_group_ids (Optional[List[str]]): List of VPC security group IDs.
57
+ security_group_ids (Optional[List[str]]): List of security group IDs.
58
58
  cache_usage_limits (Optional[CacheUsageLimits]): Usage limits for the cache. Structure:
59
59
  {
60
60
  "DataStorage": {
@@ -121,7 +121,7 @@ async def create_serverless_cache(request: CreateServerlessCacheRequest) -> Dict
121
121
  for param_name, value in [
122
122
  ('SnapshotArnsToRestore', request.snapshot_arns_to_restore),
123
123
  ('SubnetIds', request.subnet_ids),
124
- ('VpcSecurityGroupIds', request.vpc_security_group_ids),
124
+ ('SecurityGroupIds', request.security_group_ids),
125
125
  ]:
126
126
  if value:
127
127
  create_request[param_name] = list(map(str, value))
@@ -20,7 +20,7 @@ from ...common.server import mcp
20
20
  from typing import Dict, Optional
21
21
 
22
22
 
23
- @mcp.tool(name='delete_serverless_cache')
23
+ @mcp.tool(name='delete-serverless-cache')
24
24
  @handle_exceptions
25
25
  async def delete_serverless_cache(
26
26
  serverless_cache_name: str,
@@ -89,9 +89,7 @@ class CreateServerlessCacheRequest(BaseModel):
89
89
  'a list of Tag objects, or a dict of key-value pairs'
90
90
  ),
91
91
  )
92
- vpc_security_group_ids: Optional[List[str]] = Field(
93
- None, description='List of VPC security group IDs'
94
- )
92
+ security_group_ids: Optional[List[str]] = Field(None, description='List of security group IDs')
95
93
  cache_usage_limits: Optional[CacheUsageLimits] = Field(
96
94
  None, description='Usage limits for the cache'
97
95
  )
@@ -143,9 +141,7 @@ class ModifyServerlessCacheRequest(BaseModel):
143
141
  user_group_id: Optional[str] = Field(
144
142
  None, description='ID of the user group to associate with the cache'
145
143
  )
146
- vpc_security_group_ids: Optional[List[str]] = Field(
147
- None, description='List of VPC security group IDs'
148
- )
144
+ security_group_ids: Optional[List[str]] = Field(None, description='List of security group IDs')
149
145
 
150
146
  @field_validator('daily_snapshot_time')
151
147
  def validate_snapshot_time(cls, v):
@@ -21,7 +21,7 @@ from .models import ModifyServerlessCacheRequest
21
21
  from typing import Dict
22
22
 
23
23
 
24
- @mcp.tool(name='modify_serverless_cache')
24
+ @mcp.tool(name='modify-serverless-cache')
25
25
  @handle_exceptions
26
26
  async def modify_serverless_cache(request: ModifyServerlessCacheRequest) -> Dict:
27
27
  """Modify an Amazon ElastiCache serverless cache.
@@ -53,7 +53,7 @@ async def modify_serverless_cache(request: ModifyServerlessCacheRequest) -> Dict
53
53
  "Minimum": int # Minimum ECPU per second
54
54
  }
55
55
  }
56
- security_group_ids (Optional[List[str]]): List of VPC security group IDs.
56
+ security_group_ids (Optional[List[str]]): List of security group IDs.
57
57
  user_group_id (Optional[str]): ID of the user group to associate with the cache.
58
58
 
59
59
  Returns:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: awslabs.elasticache-mcp-server
3
- Version: 0.1.1
3
+ Version: 0.1.3
4
4
  Summary: An AWS Labs Model Context Protocol (MCP) server for Amazon ElastiCache
5
5
  Project-URL: homepage, https://awslabs.github.io/mcp/
6
6
  Project-URL: docs, https://awslabs.github.io/mcp/servers/elasticache-mcp-server/
@@ -38,10 +38,19 @@ Description-Content-Type: text/markdown
38
38
 
39
39
  # AWS ElastiCache MCP Server
40
40
 
41
- The official MCP Server for interacting with AWS ElastiCache
41
+ The official MCP Server for interacting with AWS ElastiCache control plane. In order to interact with your data in ElastiCache Serverless caches and self-designed clusters use the [Valkey MCP Server](https://github.com/awslabs/mcp/blob/main/src/valkey-mcp-server) or the [Memcached MCP Server](https://github.com/awslabs/mcp/blob/main/src/memcached-mcp-server).
42
42
 
43
43
  ## Available MCP Tools
44
44
 
45
+ ### Serverless Cache Operations
46
+ - `create-serverless-cache` - Create a new ElastiCache serverless cache
47
+ - `delete-serverless-cache` - Delete a serverless cache
48
+ - `describe-serverless-caches` - Get information about serverless caches
49
+ - `modify-serverless-cache` - Modify settings of a serverless cache
50
+ - `connect-jump-host-serverless-cache` - Configure an EC2 instance as a jump host for serverless cache access
51
+ - `create-jump-host-serverless-cache` - Create an EC2 jump host to access a serverless cache via SSH tunnel
52
+ - `get-ssh-tunnel-command-serverless-cache` - Generate SSH tunnel command for serverless cache access
53
+
45
54
  ### Replication Group Operations
46
55
  - `create-replication-group` - Create an Amazon ElastiCache replication group with specified configuration
47
56
  - `delete-replication-group` - Delete an ElastiCache replication group with optional final snapshot
@@ -64,15 +73,6 @@ The official MCP Server for interacting with AWS ElastiCache
64
73
  - `create-jump-host-cache-cluster` - Create an EC2 jump host to access a cluster via SSH tunnel
65
74
  - `get-ssh-tunnel-command-cache-cluster` - Generate SSH tunnel command for cluster access
66
75
 
67
- ### Serverless Cache Operations
68
- - `create-serverless-cache` - Create a new ElastiCache serverless cache
69
- - `delete-serverless-cache` - Delete a serverless cache
70
- - `describe-serverless-caches` - Get information about serverless caches
71
- - `modify-serverless-cache` - Modify settings of a serverless cache
72
- - `connect-jump-host-serverless-cache` - Configure an EC2 instance as a jump host for serverless cache access
73
- - `create-jump-host-serverless-cache` - Create an EC2 jump host to access a serverless cache via SSH tunnel
74
- - `get-ssh-tunnel-command-serverless-cache` - Generate SSH tunnel command for serverless cache access
75
-
76
76
  ### CloudWatch Operations
77
77
  - `get-metric-statistics` - Get CloudWatch metric statistics for ElastiCache resources with customizable time periods and dimensions
78
78
 
@@ -114,7 +114,9 @@ All tools support an optional `region_name` parameter to specify which AWS regio
114
114
 
115
115
  ## Installation
116
116
 
117
- Add the MCP to your favorite agentic tools. e.g. for Amazon Q Developer CLI MCP, `~/.aws/amazonq/mcp.json`):
117
+ [![Install MCP Server](https://cursor.com/deeplink/mcp-install-light.svg)](https://cursor.com/install-mcp?name=awslabs.elasticache-mcp-server&config=eyJjb21tYW5kIjoidXZ4IGF3c2xhYnMuZWxhc3RpY2FjaGUtbWNwLXNlcnZlckBsYXRlc3QiLCJlbnYiOnsiQVdTX1BST0ZJTEUiOiJkZWZhdWx0IiwiQVdTX1JFR0lPTiI6InVzLXdlc3QtMiIsIkZBU1RNQ1BfTE9HX0xFVkVMIjoiRVJST1IifSwiZGlzYWJsZWQiOmZhbHNlLCJhdXRvQXBwcm92ZSI6W119)
118
+
119
+ Add the MCP to your favorite agentic tools. (e.g. for Amazon Q Developer CLI MCP, `~/.aws/amazonq/mcp.json`):
118
120
 
119
121
  ```json
120
122
  {
@@ -8,7 +8,7 @@ awslabs/elasticache_mcp_server/common/decorators.py,sha256=jIQQH3N9Ko604eb65ZwKn
8
8
  awslabs/elasticache_mcp_server/common/server.py,sha256=5W1gSo7_BWqbSiWRWZq2NkFy35rv8LhkPorjj0_7w9k,1131
9
9
  awslabs/elasticache_mcp_server/tools/__init__.py,sha256=KSEbmf3L19fZWgATz6RIuYcnZaoGwjhAFAlu_eeb_78,664
10
10
  awslabs/elasticache_mcp_server/tools/cc/__init__.py,sha256=5PAZpzh_4AU5EUf1xR8_Y3M4toKbhzOIaKJ57l8x-nA,1148
11
- awslabs/elasticache_mcp_server/tools/cc/connect.py,sha256=cDHsvCdvfnGXRnCqKLFBHqqwF43BnB5Ct6BMoVHfSy4,17097
11
+ awslabs/elasticache_mcp_server/tools/cc/connect.py,sha256=POaioP5ExNkCNEETAVe7Hha8aTf5--RW1khdOeLm6es,21077
12
12
  awslabs/elasticache_mcp_server/tools/cc/create.py,sha256=cZzqA1MyUvtrv11kqAzscNlqsuOeaUmqmbZlIdqyfW4,9943
13
13
  awslabs/elasticache_mcp_server/tools/cc/delete.py,sha256=i6jecaSHJicrI8H3a4bFZtLJYIfeWWs6IkypCXKZLzg,2435
14
14
  awslabs/elasticache_mcp_server/tools/cc/describe.py,sha256=3glRiRc5ebGeDKPlyMf7PTP1YnaOBMuGhwWkfscbJRA,3642
@@ -18,7 +18,7 @@ awslabs/elasticache_mcp_server/tools/cc/processors.py,sha256=mJ0MTtxRgMcpLD2KGuF
18
18
  awslabs/elasticache_mcp_server/tools/ce/__init__.py,sha256=nY1ui4V0U_cHm4qLsyBeDCRGQnhE36PEVon-n30meV0,806
19
19
  awslabs/elasticache_mcp_server/tools/ce/get_cost_and_usage.py,sha256=2mEXSRFIYfZ6PpEbhmyN2qMVrH-epJfY6NeYeT4JkDQ,2923
20
20
  awslabs/elasticache_mcp_server/tools/cw/__init__.py,sha256=WMqauaWifQk1YoSmaV1SpZqOyb30RT7mWvZMshYC97U,735
21
- awslabs/elasticache_mcp_server/tools/cw/get_metric_statistics.py,sha256=XBnal3dTcdLcJZ8YR3LADj4yOj6VYTgwAie8I8MkrJk,2885
21
+ awslabs/elasticache_mcp_server/tools/cw/get_metric_statistics.py,sha256=vyta4rQ_VcGysxcl4jI1gBbQ8zwNMblgoXhE8hP4DqM,3369
22
22
  awslabs/elasticache_mcp_server/tools/cwlogs/__init__.py,sha256=5pcisxzfTCCbs6JWK7bmy1w9YD-oxHHTfIxVIKcCUFY,1034
23
23
  awslabs/elasticache_mcp_server/tools/cwlogs/create_log_group.py,sha256=6LYK7K86vPyKBNy54WiPCiDKOxM38nWiEUtkMu-snWE,2407
24
24
  awslabs/elasticache_mcp_server/tools/cwlogs/describe_log_groups.py,sha256=FMfMsIhzdD9As86TC7h5nBRfQ3GEFZVMaHts-icLDPU,4652
@@ -36,7 +36,7 @@ awslabs/elasticache_mcp_server/tools/misc/describe_events.py,sha256=OFm8S0-Hnk-c
36
36
  awslabs/elasticache_mcp_server/tools/misc/describe_service_updates.py,sha256=ppnPpSbChI7KOqk-M1PTQvoDRu4maIGmS7reNnJt9R4,2990
37
37
  awslabs/elasticache_mcp_server/tools/rg/__init__.py,sha256=RyGA5z-CeuOjw8X3GeFwpW33WzzThoErmuJ0ddu-gC0,1983
38
38
  awslabs/elasticache_mcp_server/tools/rg/complete_migration.py,sha256=yU1qSI5WRdKvrFp1rSPrtwd1fIrAJmfgoOodZixBzl8,3732
39
- awslabs/elasticache_mcp_server/tools/rg/connect.py,sha256=ObxevDUOY95jsyVoLH_H2gx4PIZXzmg2w81wJ7hAfTA,20800
39
+ awslabs/elasticache_mcp_server/tools/rg/connect.py,sha256=f8pxLQxur8HW12GnsR1GyyzuVsog6A55bRx0kTfNIdw,23052
40
40
  awslabs/elasticache_mcp_server/tools/rg/create.py,sha256=SUe8_vyvQ3emeCru1g9yp9ZxfHmk-ASNpu36DaBoSXU,13495
41
41
  awslabs/elasticache_mcp_server/tools/rg/delete.py,sha256=PA-D8YK2ePs1aa6BRc7OYsrHNSbjGazwN9UP1fDnBDA,2747
42
42
  awslabs/elasticache_mcp_server/tools/rg/describe.py,sha256=wt7tdYKe3VPkxgDj7MdcmNtXwxjRKprKxkwQPx9C1Fg,2984
@@ -46,15 +46,15 @@ awslabs/elasticache_mcp_server/tools/rg/processors.py,sha256=2DEsrF7ucCg7vKhXfoO
46
46
  awslabs/elasticache_mcp_server/tools/rg/start_migration.py,sha256=bReeWPZbCoU7gKlmR8Nfy8VrweVThTrvp4KibK_fmp4,6042
47
47
  awslabs/elasticache_mcp_server/tools/rg/test_migration.py,sha256=Ye05Lxf84tQShOOokm-agL9GDPFSZTrL31A1A7368EE,5597
48
48
  awslabs/elasticache_mcp_server/tools/serverless/__init__.py,sha256=q_te85wY15COM7iFszg1SrY4ianS_BmQrvTEMYpxm3A,1306
49
- awslabs/elasticache_mcp_server/tools/serverless/connect.py,sha256=PETjBHn2vhWIggyL2cvHhrbHFF5Z54tfd_4U0Cl0If8,17185
50
- awslabs/elasticache_mcp_server/tools/serverless/create.py,sha256=zpihqFZWweifjSKpAr39CDn2Hp8mPK6l3pJr7WvXEl0,7513
51
- awslabs/elasticache_mcp_server/tools/serverless/delete.py,sha256=Rc1JXZgDlkpArW217TSxdpZkyOZEW053iaWulp_ZWUk,1858
49
+ awslabs/elasticache_mcp_server/tools/serverless/connect.py,sha256=T_d2gOe-1GGdVjRClvDXvCPlaTUR0VROzfBqmaMRYlU,22558
50
+ awslabs/elasticache_mcp_server/tools/serverless/create.py,sha256=0L4Tji3aSrPmsLYvib-s4D_fuEMaWk541ix9mti_rKE,7498
51
+ awslabs/elasticache_mcp_server/tools/serverless/delete.py,sha256=SByDCgcfNj23zL7ijO0ze_1QL0iTWBO4PVsBxvu1BGc,1858
52
52
  awslabs/elasticache_mcp_server/tools/serverless/describe.py,sha256=GwHuHfi4vltAxOv7hLvvsKiq7GoGF7LUcrt0uCi4mAM,2567
53
- awslabs/elasticache_mcp_server/tools/serverless/models.py,sha256=hex1WG0HwDBbMFNbi7lpbAs0XaGJRzpIerHjpdG53MY,6678
54
- awslabs/elasticache_mcp_server/tools/serverless/modify.py,sha256=OU6x5kNsNvKM1ylETPviBN_dv05LMPWW02zubo47mVE,3872
55
- awslabs_elasticache_mcp_server-0.1.1.dist-info/METADATA,sha256=5M4kdQ43eDpFWk8rLrb2tUdtbUC_w4gqJhFfQILkVO8,10824
56
- awslabs_elasticache_mcp_server-0.1.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
57
- awslabs_elasticache_mcp_server-0.1.1.dist-info/entry_points.txt,sha256=058QYfdxCKFALC9V2L5AQz36-jBT8fV8HVLOuIVngAA,92
58
- awslabs_elasticache_mcp_server-0.1.1.dist-info/licenses/LICENSE,sha256=CeipvOyAZxBGUsFoaFqwkx54aPnIKEtm9a5u2uXxEws,10142
59
- awslabs_elasticache_mcp_server-0.1.1.dist-info/licenses/NOTICE,sha256=89EDSSt20vYGgAdEzUiW5Y_VMpZ5CDd2RvXs8BDdhKk,98
60
- awslabs_elasticache_mcp_server-0.1.1.dist-info/RECORD,,
53
+ awslabs/elasticache_mcp_server/tools/serverless/models.py,sha256=0QTnp8ygVJ4j49JznT4IRlFzdyKYiMXopoJNifO93ok,6634
54
+ awslabs/elasticache_mcp_server/tools/serverless/modify.py,sha256=zBYYhwWF6p8c4WGCZl5tpocXR00pNAHpdqJAF8KRJyI,3868
55
+ awslabs_elasticache_mcp_server-0.1.3.dist-info/METADATA,sha256=z2Jfebuhogj_Dab_a8ZciBeFqrjNsFdMG-UKEcW7TIM,11510
56
+ awslabs_elasticache_mcp_server-0.1.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
57
+ awslabs_elasticache_mcp_server-0.1.3.dist-info/entry_points.txt,sha256=058QYfdxCKFALC9V2L5AQz36-jBT8fV8HVLOuIVngAA,92
58
+ awslabs_elasticache_mcp_server-0.1.3.dist-info/licenses/LICENSE,sha256=CeipvOyAZxBGUsFoaFqwkx54aPnIKEtm9a5u2uXxEws,10142
59
+ awslabs_elasticache_mcp_server-0.1.3.dist-info/licenses/NOTICE,sha256=89EDSSt20vYGgAdEzUiW5Y_VMpZ5CDd2RvXs8BDdhKk,98
60
+ awslabs_elasticache_mcp_server-0.1.3.dist-info/RECORD,,