awslabs.eks-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.
@@ -0,0 +1,661 @@
1
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance
4
+ # with the License. A copy of the License is located at
5
+ #
6
+ # http://www.apache.org/licenses/LICENSE-2.0
7
+ #
8
+ # or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES
9
+ # OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions
10
+ # and limitations under the License.
11
+
12
+ """EKS stack handler for the EKS MCP Server."""
13
+
14
+ import os
15
+ import yaml
16
+ from awslabs.eks_mcp_server.aws_helper import AwsHelper
17
+ from awslabs.eks_mcp_server.consts import (
18
+ CFN_CAPABILITY_IAM,
19
+ CFN_ON_FAILURE_DELETE,
20
+ CFN_STACK_NAME_TEMPLATE,
21
+ CFN_STACK_TAG_KEY,
22
+ CFN_STACK_TAG_VALUE,
23
+ DELETE_OPERATION,
24
+ DEPLOY_OPERATION,
25
+ DESCRIBE_OPERATION,
26
+ GENERATE_OPERATION,
27
+ STACK_NOT_OWNED_ERROR_TEMPLATE,
28
+ )
29
+ from awslabs.eks_mcp_server.logging_helper import LogLevel, log_with_request_id
30
+ from awslabs.eks_mcp_server.models import (
31
+ DeleteStackResponse,
32
+ DeployStackResponse,
33
+ DescribeStackResponse,
34
+ GenerateTemplateResponse,
35
+ )
36
+ from mcp.server.fastmcp import Context
37
+ from mcp.types import EmbeddedResource, ImageContent, TextContent
38
+ from pydantic import Field
39
+ from typing import Dict, List, Optional, Tuple, Union, cast
40
+
41
+
42
+ class EksStackHandler:
43
+ """Handler for Amazon EKS CloudFormation stack operations.
44
+
45
+ This class provides tools for creating, managing, and deleting CloudFormation
46
+ stacks for EKS clusters.
47
+ """
48
+
49
+ def __init__(self, mcp, allow_write: bool = False):
50
+ """Initialize the EKS stack handler.
51
+
52
+ Args:
53
+ mcp: The MCP server instance
54
+ allow_write: Whether to enable write access (default: False)
55
+ """
56
+ self.mcp = mcp
57
+ self.allow_write = allow_write
58
+
59
+ # Register tools
60
+ self.mcp.tool(name='manage_eks_stacks')(self.manage_eks_stacks)
61
+
62
+ def _ensure_stack_ownership(
63
+ self, ctx: Context, stack_name: str, operation: str
64
+ ) -> Tuple[bool, Optional[Dict], Optional[str]]:
65
+ """Ensure that a stack exists and was created by this tool.
66
+
67
+ Args:
68
+ ctx: The MCP context
69
+ stack_name: Name of the stack to verify
70
+ operation: Operation being performed (for error messages)
71
+
72
+ Returns:
73
+ Tuple of (success, stack_details, error_message)
74
+ - success: True if the stack exists and was created by this tool
75
+ - stack_details: Stack details if the stack exists, None otherwise
76
+ - error_message: Error message if the stack doesn't exist or wasn't created by this tool, None if successful
77
+ """
78
+ try:
79
+ # Create CloudFormation client
80
+ cfn_client = AwsHelper.create_boto3_client('cloudformation')
81
+
82
+ # Get stack details
83
+ stack_details = cfn_client.describe_stacks(StackName=stack_name)
84
+ stack = stack_details['Stacks'][0]
85
+
86
+ # Verify the stack was created by our tool
87
+ tags = stack.get('Tags', [])
88
+ is_our_stack = False
89
+ for tag in tags:
90
+ if tag.get('Key') == CFN_STACK_TAG_KEY and tag.get('Value') == CFN_STACK_TAG_VALUE:
91
+ is_our_stack = True
92
+ break
93
+
94
+ if not is_our_stack:
95
+ error_message = STACK_NOT_OWNED_ERROR_TEMPLATE.format(
96
+ stack_name=stack_name, tool_name=CFN_STACK_TAG_VALUE, operation=operation
97
+ )
98
+ log_with_request_id(ctx, LogLevel.ERROR, error_message)
99
+ return False, stack, error_message
100
+
101
+ return True, stack, None
102
+ except Exception as e:
103
+ if 'does not exist' in str(e):
104
+ error_message = f'Stack {stack_name} not found or cannot be accessed: {str(e)}'
105
+ else:
106
+ error_message = f'Error verifying stack ownership: {str(e)}'
107
+
108
+ log_with_request_id(ctx, LogLevel.ERROR, error_message)
109
+ return False, None, error_message
110
+
111
+ async def manage_eks_stacks(
112
+ self,
113
+ ctx: Context,
114
+ operation: str = Field(
115
+ ...,
116
+ description='Operation to perform: generate, deploy, describe, or delete. Choose "describe" for read-only operations when write access is disabled.',
117
+ ),
118
+ template_file: Optional[str] = Field(
119
+ None,
120
+ description="""Absolute path for the CloudFormation template (for generate and deploy operations).
121
+ IMPORTANT: Assistant must provide the full absolute path to the template file, as the MCP client and server might not run from the same location.""",
122
+ ),
123
+ cluster_name: Optional[str] = Field(
124
+ None,
125
+ description="""Name of the EKS cluster (for generate, deploy, describe and delete operations).
126
+ This name will be used to derive the CloudFormation stack name and will be embedded in the cluster resources.""",
127
+ ),
128
+ ) -> Union[
129
+ GenerateTemplateResponse, DeployStackResponse, DescribeStackResponse, DeleteStackResponse
130
+ ]:
131
+ """Manage EKS CloudFormation stacks with both read and write operations.
132
+
133
+ This tool provides operations for managing EKS CloudFormation stacks, including creating templates,
134
+ deploying stacks, retrieving stack information, and deleting stacks. It serves as the primary
135
+ mechanism for creating and managing EKS clusters through CloudFormation, enabling standardized
136
+ cluster creation, configuration updates, and resource cleanup.
137
+
138
+ ## Requirements
139
+ - The server must be run with the `--allow-write` flag for generate, deploy, and delete operations
140
+ - For deploy and delete operations, the stack must have been created by this tool
141
+ - For template_file parameter, the path must be absolute and accessible to the server
142
+
143
+ ## Operations
144
+ - **generate**: Create a CloudFormation template at the specified absolute path with the cluster name embedded
145
+ - **deploy**: Deploy a CloudFormation template from the specified absolute path (creates a new stack or updates an existing one)
146
+ - **describe**: Get detailed information about a CloudFormation stack for a specific cluster
147
+ - **delete**: Delete a CloudFormation stack for the specified cluster
148
+
149
+ ## Response Information
150
+ The response type varies based on the operation:
151
+ - generate: Returns GenerateTemplateResponse with the template path
152
+ - deploy: Returns DeployStackResponse with stack name, ARN, and cluster name
153
+ - describe: Returns DescribeStackResponse with stack details, outputs, and status
154
+ - delete: Returns DeleteStackResponse with stack name, ID, and cluster name
155
+
156
+ ## Usage Tips
157
+ - Use the describe operation first to check if a cluster already exists
158
+ - For safety, this tool will only modify or delete stacks that it created
159
+ - Stack creation typically takes 15-20 minutes to complete
160
+ - Use absolute paths for template files (e.g., '/home/user/templates/eks-template.yaml')
161
+ - The cluster name is used to derive the CloudFormation stack name
162
+
163
+ Args:
164
+ ctx: MCP context
165
+ operation: Operation to perform (generate, deploy, describe, or delete)
166
+ template_file: Absolute path for the CloudFormation template (for generate and deploy operations)
167
+ cluster_name: Name of the EKS cluster (for all operations)
168
+
169
+ Returns:
170
+ Union[GenerateTemplateResponse, DeployStackResponse, DescribeStackResponse, DeleteStackResponse]:
171
+ Response specific to the operation performed
172
+ """
173
+ try:
174
+ # Check if write access is disabled and trying to perform a mutating operation
175
+ if not self.allow_write and operation not in [
176
+ DESCRIBE_OPERATION,
177
+ ]:
178
+ error_message = f'Operation {operation} is not allowed without write access'
179
+ log_with_request_id(ctx, LogLevel.ERROR, error_message)
180
+
181
+ # Return appropriate response type based on operation
182
+ if operation == GENERATE_OPERATION:
183
+ return GenerateTemplateResponse(
184
+ isError=True,
185
+ content=cast(
186
+ List[Union[TextContent, ImageContent, EmbeddedResource]],
187
+ [TextContent(type='text', text=error_message)],
188
+ ),
189
+ template_path='',
190
+ )
191
+ elif operation == DEPLOY_OPERATION:
192
+ return DeployStackResponse(
193
+ isError=True,
194
+ content=cast(
195
+ List[Union[TextContent, ImageContent, EmbeddedResource]],
196
+ [TextContent(type='text', text=error_message)],
197
+ ),
198
+ stack_name='',
199
+ stack_arn='',
200
+ cluster_name=cluster_name or '',
201
+ )
202
+ elif operation == DELETE_OPERATION:
203
+ return DeleteStackResponse(
204
+ isError=True,
205
+ content=cast(
206
+ List[Union[TextContent, ImageContent, EmbeddedResource]],
207
+ [TextContent(type='text', text=error_message)],
208
+ ),
209
+ stack_name='',
210
+ stack_id='',
211
+ cluster_name=cluster_name or '',
212
+ )
213
+ else: # Default to describe operation
214
+ return DescribeStackResponse(
215
+ isError=True,
216
+ content=cast(
217
+ List[Union[TextContent, ImageContent, EmbeddedResource]],
218
+ [TextContent(type='text', text=error_message)],
219
+ ),
220
+ stack_name='',
221
+ stack_id='',
222
+ cluster_name=cluster_name or '',
223
+ creation_time='',
224
+ stack_status='',
225
+ outputs={},
226
+ )
227
+
228
+ if operation == GENERATE_OPERATION:
229
+ if template_file is None:
230
+ raise ValueError('template_file is required for generate operation')
231
+ if cluster_name is None:
232
+ raise ValueError('cluster_name is required for generate operation')
233
+ return await self._generate_template(
234
+ ctx=ctx, template_path=template_file, cluster_name=cluster_name
235
+ )
236
+
237
+ elif operation == DEPLOY_OPERATION:
238
+ if template_file is None:
239
+ raise ValueError('template_file is required for deploy operation')
240
+ if cluster_name is None:
241
+ raise ValueError('cluster_name is required for deploy operation')
242
+
243
+ # Derive stack name from cluster name
244
+ stack_name = CFN_STACK_NAME_TEMPLATE.format(cluster_name=cluster_name)
245
+ return await self._deploy_stack(
246
+ ctx=ctx,
247
+ template_file=template_file,
248
+ stack_name=stack_name,
249
+ cluster_name=cluster_name,
250
+ )
251
+
252
+ elif operation == DESCRIBE_OPERATION:
253
+ if cluster_name is None:
254
+ raise ValueError('cluster_name is required for describe operation')
255
+
256
+ # Derive stack name from cluster name
257
+ stack_name = CFN_STACK_NAME_TEMPLATE.format(cluster_name=cluster_name)
258
+ return await self._describe_stack(
259
+ ctx=ctx, stack_name=stack_name, cluster_name=cluster_name
260
+ )
261
+
262
+ elif operation == DELETE_OPERATION:
263
+ if cluster_name is None:
264
+ raise ValueError('cluster_name is required for delete operation')
265
+
266
+ # Derive stack name from cluster name
267
+ stack_name = CFN_STACK_NAME_TEMPLATE.format(cluster_name=cluster_name)
268
+ return await self._delete_stack(
269
+ ctx=ctx, stack_name=stack_name, cluster_name=cluster_name
270
+ )
271
+
272
+ else:
273
+ error_message = f'Invalid operation: {operation}. Must be one of: generate, deploy, describe, delete'
274
+ log_with_request_id(ctx, LogLevel.ERROR, error_message)
275
+ # Default to DescribeStackResponse for invalid operations
276
+ return DescribeStackResponse(
277
+ isError=True,
278
+ content=cast(
279
+ List[Union[TextContent, ImageContent, EmbeddedResource]],
280
+ [TextContent(type='text', text=error_message)],
281
+ ),
282
+ stack_name='',
283
+ stack_id='',
284
+ cluster_name=cluster_name or '',
285
+ creation_time='',
286
+ stack_status='',
287
+ outputs={},
288
+ )
289
+ except ValueError as e:
290
+ # Re-raise ValueError for parameter validation errors
291
+ log_with_request_id(ctx, LogLevel.ERROR, f'Parameter validation error: {str(e)}')
292
+ raise
293
+ except Exception as e:
294
+ error_message = f'Error in manage_eks_stacks: {str(e)}'
295
+ log_with_request_id(ctx, LogLevel.ERROR, error_message)
296
+ # Default to DescribeStackResponse for general exceptions
297
+ return DescribeStackResponse(
298
+ isError=True,
299
+ content=cast(
300
+ List[Union[TextContent, ImageContent, EmbeddedResource]],
301
+ [TextContent(type='text', text=error_message)],
302
+ ),
303
+ stack_name='',
304
+ stack_id='',
305
+ cluster_name=cluster_name or '',
306
+ creation_time='',
307
+ stack_status='',
308
+ outputs={},
309
+ )
310
+
311
+ async def _generate_template(
312
+ self, ctx: Context, template_path: str, cluster_name: str
313
+ ) -> GenerateTemplateResponse:
314
+ """Generate a CloudFormation template at the specified path with the cluster name embedded.
315
+
316
+ The template creates a complete EKS environment including:
317
+ - A dedicated VPC with public and private subnets across two availability zones
318
+ - Internet Gateway and NAT Gateways for outbound connectivity
319
+ - Security groups for cluster communication
320
+ - IAM roles for the EKS cluster and worker nodes
321
+ - An EKS cluster in Auto Mode with:
322
+ - Compute configuration with general-purpose and system node pools
323
+ - Kubernetes network configuration with elastic load balancing
324
+ - Block storage configuration
325
+ - API authentication mode
326
+ """
327
+ try:
328
+ # Get the source template path
329
+ source_template_path = os.path.join(
330
+ os.path.dirname(__file__), 'templates', 'eks-templates', 'eks-with-vpc.yaml'
331
+ )
332
+
333
+ # Create directory if it doesn't exist
334
+ os.makedirs(os.path.dirname(template_path), exist_ok=True)
335
+
336
+ # Read the template
337
+ with open(source_template_path, 'r') as source_file:
338
+ template_content = source_file.read()
339
+
340
+ # Parse the template as YAML
341
+ template_yaml = yaml.safe_load(template_content)
342
+
343
+ # Modify the template to set the cluster name directly
344
+ # Find the ClusterName parameter and set its default value
345
+ if 'Parameters' in template_yaml and 'ClusterName' in template_yaml['Parameters']:
346
+ template_yaml['Parameters']['ClusterName']['Default'] = cluster_name
347
+
348
+ # Convert back to YAML
349
+ modified_template = yaml.dump(template_yaml, default_flow_style=False)
350
+
351
+ # Write the modified template to the destination
352
+ with open(template_path, 'w') as dest_file:
353
+ dest_file.write(modified_template)
354
+
355
+ log_with_request_id(
356
+ ctx,
357
+ LogLevel.INFO,
358
+ f'Generated CloudFormation template at {template_path} with cluster name {cluster_name}',
359
+ )
360
+
361
+ return GenerateTemplateResponse(
362
+ isError=False,
363
+ content=cast(
364
+ List[Union[TextContent, ImageContent, EmbeddedResource]],
365
+ [
366
+ TextContent(
367
+ type='text',
368
+ text=f'CloudFormation template generated at {template_path} with cluster name {cluster_name}',
369
+ )
370
+ ],
371
+ ),
372
+ template_path=template_path,
373
+ )
374
+ except Exception as e:
375
+ error_message = f'Failed to generate template: {str(e)}'
376
+ log_with_request_id(ctx, LogLevel.ERROR, error_message)
377
+
378
+ return GenerateTemplateResponse(
379
+ isError=True,
380
+ content=cast(
381
+ List[Union[TextContent, ImageContent, EmbeddedResource]],
382
+ [TextContent(type='text', text=error_message or 'Unknown error')],
383
+ ),
384
+ template_path='',
385
+ )
386
+
387
+ async def _deploy_stack(
388
+ self, ctx: Context, template_file: str, stack_name: str, cluster_name: str
389
+ ) -> DeployStackResponse:
390
+ """Deploy a CloudFormation stack from the specified template file."""
391
+ try:
392
+ # Create CloudFormation client
393
+ cfn_client = AwsHelper.create_boto3_client('cloudformation')
394
+
395
+ # Read the template
396
+ with open(template_file, 'r') as template_file_obj:
397
+ template_body = template_file_obj.read()
398
+
399
+ # Check if the stack already exists and verify ownership
400
+ stack_exists = False
401
+ try:
402
+ success, stack, error_message = self._ensure_stack_ownership(
403
+ ctx, stack_name, 'update'
404
+ )
405
+ if stack:
406
+ stack_exists = True
407
+ if not success:
408
+ return DeployStackResponse(
409
+ isError=True,
410
+ content=cast(
411
+ List[Union[TextContent, ImageContent, EmbeddedResource]],
412
+ [TextContent(type='text', text=error_message or 'Unknown error')],
413
+ ),
414
+ stack_name=stack_name,
415
+ stack_arn='',
416
+ cluster_name=cluster_name,
417
+ )
418
+ except Exception:
419
+ # Stack doesn't exist, we'll create it
420
+ stack_exists = False
421
+
422
+ # Create or update the stack
423
+ if stack_exists:
424
+ log_with_request_id(
425
+ ctx,
426
+ LogLevel.INFO,
427
+ f'Updating CloudFormation stack {stack_name} for EKS cluster {cluster_name}',
428
+ )
429
+
430
+ response = cfn_client.update_stack(
431
+ StackName=stack_name,
432
+ TemplateBody=template_body,
433
+ Capabilities=[CFN_CAPABILITY_IAM],
434
+ Tags=[{'Key': CFN_STACK_TAG_KEY, 'Value': CFN_STACK_TAG_VALUE}],
435
+ )
436
+
437
+ operation_text = 'update'
438
+ else:
439
+ log_with_request_id(
440
+ ctx,
441
+ LogLevel.INFO,
442
+ f'Creating CloudFormation stack {stack_name} for EKS cluster {cluster_name}',
443
+ )
444
+
445
+ response = cfn_client.create_stack(
446
+ StackName=stack_name,
447
+ TemplateBody=template_body,
448
+ Capabilities=[CFN_CAPABILITY_IAM],
449
+ OnFailure=CFN_ON_FAILURE_DELETE,
450
+ Tags=[{'Key': CFN_STACK_TAG_KEY, 'Value': CFN_STACK_TAG_VALUE}],
451
+ )
452
+
453
+ operation_text = 'creation'
454
+
455
+ log_with_request_id(
456
+ ctx,
457
+ LogLevel.INFO,
458
+ f'CloudFormation stack {operation_text} initiated. Stack ARN: {response["StackId"]}',
459
+ )
460
+
461
+ return DeployStackResponse(
462
+ isError=False,
463
+ content=cast(
464
+ List[Union[TextContent, ImageContent, EmbeddedResource]],
465
+ [
466
+ TextContent(
467
+ type='text',
468
+ text=f'CloudFormation stack {operation_text} initiated. Stack {operation_text} is in progress and typically takes 15-20 minutes to complete.',
469
+ )
470
+ ],
471
+ ),
472
+ stack_name=stack_name,
473
+ stack_arn=response['StackId'],
474
+ cluster_name=cluster_name,
475
+ )
476
+ except Exception as e:
477
+ error_message = f'Failed to deploy stack: {str(e)}'
478
+ log_with_request_id(ctx, LogLevel.ERROR, error_message)
479
+
480
+ return DeployStackResponse(
481
+ isError=True,
482
+ content=cast(
483
+ List[Union[TextContent, ImageContent, EmbeddedResource]],
484
+ [TextContent(type='text', text=error_message or 'Unknown error')],
485
+ ),
486
+ stack_name=stack_name,
487
+ stack_arn='',
488
+ cluster_name=cluster_name,
489
+ )
490
+
491
+ async def _describe_stack(
492
+ self, ctx: Context, stack_name: str, cluster_name: str
493
+ ) -> DescribeStackResponse:
494
+ """Describe a CloudFormation stack."""
495
+ try:
496
+ # Verify stack ownership
497
+ success, stack, error_message = self._ensure_stack_ownership(
498
+ ctx, stack_name, 'describe'
499
+ )
500
+ if not success:
501
+ # Prepare error response with available stack details
502
+ stack_id = ''
503
+ creation_time = ''
504
+ stack_status = ''
505
+
506
+ if stack:
507
+ stack_id = stack['StackId']
508
+ creation_time = stack['CreationTime'].isoformat()
509
+ stack_status = stack['StackStatus']
510
+
511
+ return DescribeStackResponse(
512
+ isError=True,
513
+ content=cast(
514
+ List[Union[TextContent, ImageContent, EmbeddedResource]],
515
+ [TextContent(type='text', text=error_message or 'Unknown error')],
516
+ ),
517
+ stack_name=stack_name,
518
+ stack_id=stack_id,
519
+ cluster_name=cluster_name,
520
+ creation_time=creation_time,
521
+ stack_status=stack_status,
522
+ outputs={},
523
+ )
524
+
525
+ # Extract outputs
526
+ outputs = {}
527
+ if stack and 'Outputs' in stack:
528
+ for output in stack['Outputs']:
529
+ if 'OutputKey' in output and 'OutputValue' in output:
530
+ outputs[output['OutputKey']] = output['OutputValue']
531
+
532
+ log_with_request_id(
533
+ ctx,
534
+ LogLevel.INFO,
535
+ f'Described CloudFormation stack {stack_name} for EKS cluster {cluster_name}',
536
+ )
537
+
538
+ # Safely extract stack details
539
+ stack_id = ''
540
+ creation_time = ''
541
+ stack_status = ''
542
+
543
+ if stack:
544
+ stack_id = stack.get('StackId', '')
545
+
546
+ # Safely handle creation time
547
+ if 'CreationTime' in stack:
548
+ creation_time_obj = stack['CreationTime']
549
+ if hasattr(creation_time_obj, 'isoformat'):
550
+ creation_time = creation_time_obj.isoformat()
551
+ else:
552
+ creation_time = str(creation_time_obj)
553
+
554
+ stack_status = stack.get('StackStatus', '')
555
+
556
+ return DescribeStackResponse(
557
+ isError=False,
558
+ content=cast(
559
+ List[Union[TextContent, ImageContent, EmbeddedResource]],
560
+ [
561
+ TextContent(
562
+ type='text',
563
+ text=f'Successfully described CloudFormation stack {stack_name} for EKS cluster {cluster_name}',
564
+ )
565
+ ],
566
+ ),
567
+ stack_name=stack_name,
568
+ stack_id=stack_id,
569
+ cluster_name=cluster_name,
570
+ creation_time=creation_time,
571
+ stack_status=stack_status,
572
+ outputs=outputs,
573
+ )
574
+ except Exception as e:
575
+ error_message = f'Failed to describe stack: {str(e)}'
576
+ log_with_request_id(ctx, LogLevel.ERROR, error_message)
577
+
578
+ return DescribeStackResponse(
579
+ isError=True,
580
+ content=cast(
581
+ List[Union[TextContent, ImageContent, EmbeddedResource]],
582
+ [TextContent(type='text', text=error_message or 'Unknown error')],
583
+ ),
584
+ stack_name=stack_name,
585
+ stack_id='',
586
+ cluster_name=cluster_name,
587
+ creation_time='',
588
+ stack_status='',
589
+ outputs={},
590
+ )
591
+
592
+ async def _delete_stack(
593
+ self, ctx: Context, stack_name: str, cluster_name: str
594
+ ) -> DeleteStackResponse:
595
+ """Delete a CloudFormation stack."""
596
+ try:
597
+ # Create CloudFormation client
598
+ cfn_client = AwsHelper.create_boto3_client('cloudformation')
599
+
600
+ # Verify stack ownership
601
+ success, stack, error_message = self._ensure_stack_ownership(ctx, stack_name, 'delete')
602
+ if not success:
603
+ # Prepare error response with available stack details
604
+ stack_id = ''
605
+ if stack:
606
+ stack_id = stack['StackId']
607
+
608
+ return DeleteStackResponse(
609
+ isError=True,
610
+ content=cast(
611
+ List[Union[TextContent, ImageContent, EmbeddedResource]],
612
+ [TextContent(type='text', text=error_message or 'Unknown error')],
613
+ ),
614
+ stack_name=stack_name,
615
+ stack_id=stack_id,
616
+ cluster_name=cluster_name,
617
+ )
618
+
619
+ # Safely extract stack ID
620
+ stack_id = ''
621
+ if stack and 'StackId' in stack:
622
+ stack_id = stack['StackId']
623
+
624
+ # Delete the stack
625
+ cfn_client.delete_stack(StackName=stack_name)
626
+
627
+ log_with_request_id(
628
+ ctx,
629
+ LogLevel.INFO,
630
+ f'Initiated deletion of CloudFormation stack {stack_name} for EKS cluster {cluster_name}',
631
+ )
632
+
633
+ return DeleteStackResponse(
634
+ isError=False,
635
+ content=cast(
636
+ List[Union[TextContent, ImageContent, EmbeddedResource]],
637
+ [
638
+ TextContent(
639
+ type='text',
640
+ text=f'Initiated deletion of CloudFormation stack {stack_name} for EKS cluster {cluster_name}. Deletion is in progress.',
641
+ )
642
+ ],
643
+ ),
644
+ stack_name=stack_name,
645
+ stack_id=stack_id,
646
+ cluster_name=cluster_name,
647
+ )
648
+ except Exception as e:
649
+ error_message = f'Failed to delete stack: {str(e)}'
650
+ log_with_request_id(ctx, LogLevel.ERROR, error_message)
651
+
652
+ return DeleteStackResponse(
653
+ isError=True,
654
+ content=cast(
655
+ List[Union[TextContent, ImageContent, EmbeddedResource]],
656
+ [TextContent(type='text', text=error_message or 'Unknown error')],
657
+ ),
658
+ stack_name=stack_name,
659
+ stack_id='',
660
+ cluster_name=cluster_name,
661
+ )