awslabs.eks-mcp-server 0.1.15__py3-none-any.whl → 0.1.17__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.
@@ -14,6 +14,7 @@
14
14
 
15
15
  """EKS stack handler for the EKS MCP Server."""
16
16
 
17
+ import json
17
18
  import os
18
19
  import yaml
19
20
  from awslabs.eks_mcp_server.aws_helper import AwsHelper
@@ -31,15 +32,12 @@ from awslabs.eks_mcp_server.consts import (
31
32
  )
32
33
  from awslabs.eks_mcp_server.logging_helper import LogLevel, log_with_request_id
33
34
  from awslabs.eks_mcp_server.models import (
34
- DeleteStackResponse,
35
- DeployStackResponse,
36
- DescribeStackResponse,
37
- GenerateTemplateResponse,
35
+ ManageEksStacksData,
38
36
  )
39
37
  from mcp.server.fastmcp import Context
40
- from mcp.types import TextContent
38
+ from mcp.types import CallToolResult, TextContent
41
39
  from pydantic import Field
42
- from typing import Any, Dict, Optional, Tuple, Union
40
+ from typing import Any, Dict, Optional, Tuple
43
41
 
44
42
 
45
43
  class EksStackHandler:
@@ -128,9 +126,7 @@ class EksStackHandler:
128
126
  description="""Name of the EKS cluster (for generate, deploy, describe and delete operations).
129
127
  This name will be used to derive the CloudFormation stack name and will be embedded in the cluster resources.""",
130
128
  ),
131
- ) -> Union[
132
- GenerateTemplateResponse, DeployStackResponse, DescribeStackResponse, DeleteStackResponse
133
- ]:
129
+ ) -> CallToolResult:
134
130
  """Manage EKS CloudFormation stacks with both read and write operations.
135
131
 
136
132
  This tool provides operations for managing EKS CloudFormation stacks, including creating templates,
@@ -158,10 +154,10 @@ class EksStackHandler:
158
154
 
159
155
  ## Response Information
160
156
  The response type varies based on the operation:
161
- - generate: Returns GenerateTemplateResponse with the template path
162
- - deploy: Returns DeployStackResponse with stack name, ARN, and cluster name
163
- - describe: Returns DescribeStackResponse with stack details, outputs, and status
164
- - delete: Returns DeleteStackResponse with stack name, ID, and cluster name
157
+ - generate: Returns CallToolResult with the template path
158
+ - deploy: Returns CallToolResult with stack name, ARN, and cluster name
159
+ - describe: Returns CallToolResult with stack details, outputs, and status
160
+ - delete: Returns CallToolResult with stack name, ID, and cluster name
165
161
 
166
162
  ## Usage Tips
167
163
  - Use the describe operation first to check if a cluster already exists
@@ -177,8 +173,7 @@ class EksStackHandler:
177
173
  cluster_name: Name of the EKS cluster (for all operations)
178
174
 
179
175
  Returns:
180
- Union[GenerateTemplateResponse, DeployStackResponse, DescribeStackResponse, DeleteStackResponse]:
181
- Response specific to the operation performed
176
+ ManageEksStacksResponse: Response with fields populated based on the operation performed
182
177
  """
183
178
  try:
184
179
  # Check if write access is disabled and trying to perform a mutating operation
@@ -188,40 +183,11 @@ class EksStackHandler:
188
183
  error_message = f'Operation {operation} is not allowed without write access'
189
184
  log_with_request_id(ctx, LogLevel.ERROR, error_message)
190
185
 
191
- # Return appropriate response type based on operation
192
- if operation == GENERATE_OPERATION:
193
- return GenerateTemplateResponse(
194
- isError=True,
195
- content=[TextContent(type='text', text=error_message)],
196
- template_path='',
197
- )
198
- elif operation == DEPLOY_OPERATION:
199
- return DeployStackResponse(
200
- isError=True,
201
- content=[TextContent(type='text', text=error_message)],
202
- stack_name='',
203
- stack_arn='',
204
- cluster_name=cluster_name or '',
205
- )
206
- elif operation == DELETE_OPERATION:
207
- return DeleteStackResponse(
208
- isError=True,
209
- content=[TextContent(type='text', text=error_message)],
210
- stack_name='',
211
- stack_id='',
212
- cluster_name=cluster_name or '',
213
- )
214
- else: # Default to describe operation
215
- return DescribeStackResponse(
216
- isError=True,
217
- content=[TextContent(type='text', text=error_message)],
218
- stack_name='',
219
- stack_id='',
220
- cluster_name=cluster_name or '',
221
- creation_time='',
222
- stack_status='',
223
- outputs={},
224
- )
186
+ # Return error response
187
+ return CallToolResult(
188
+ isError=True,
189
+ content=[TextContent(type='text', text=error_message)],
190
+ )
225
191
 
226
192
  if operation == GENERATE_OPERATION:
227
193
  if template_file is None:
@@ -270,16 +236,9 @@ class EksStackHandler:
270
236
  else:
271
237
  error_message = f'Invalid operation: {operation}. Must be one of: generate, deploy, describe, delete'
272
238
  log_with_request_id(ctx, LogLevel.ERROR, error_message)
273
- # Default to DescribeStackResponse for invalid operations
274
- return DescribeStackResponse(
239
+ return CallToolResult(
275
240
  isError=True,
276
241
  content=[TextContent(type='text', text=error_message)],
277
- stack_name='',
278
- stack_id='',
279
- cluster_name=cluster_name or '',
280
- creation_time='',
281
- stack_status='',
282
- outputs={},
283
242
  )
284
243
  except ValueError as e:
285
244
  # Re-raise ValueError for parameter validation errors
@@ -288,21 +247,14 @@ class EksStackHandler:
288
247
  except Exception as e:
289
248
  error_message = f'Error in manage_eks_stacks: {str(e)}'
290
249
  log_with_request_id(ctx, LogLevel.ERROR, error_message)
291
- # Default to DescribeStackResponse for general exceptions
292
- return DescribeStackResponse(
250
+ return CallToolResult(
293
251
  isError=True,
294
252
  content=[TextContent(type='text', text=error_message)],
295
- stack_name='',
296
- stack_id='',
297
- cluster_name=cluster_name or '',
298
- creation_time='',
299
- stack_status='',
300
- outputs={},
301
253
  )
302
254
 
303
255
  async def _generate_template(
304
256
  self, ctx: Context, template_path: str, cluster_name: str
305
- ) -> GenerateTemplateResponse:
257
+ ) -> CallToolResult:
306
258
  """Generate a CloudFormation template at the specified path with the cluster name embedded.
307
259
 
308
260
  The template creates a complete EKS environment including:
@@ -354,29 +306,42 @@ class EksStackHandler:
354
306
  f'Generated CloudFormation template at {template_path} with cluster name {cluster_name}',
355
307
  )
356
308
 
357
- return GenerateTemplateResponse(
309
+ data = ManageEksStacksData(
310
+ operation=GENERATE_OPERATION,
311
+ template_path=template_path,
312
+ cluster_name=cluster_name,
313
+ stack_name='',
314
+ stack_id='',
315
+ stack_arn='',
316
+ creation_time='',
317
+ stack_status='',
318
+ )
319
+
320
+ return CallToolResult(
358
321
  isError=False,
359
322
  content=[
360
323
  TextContent(
361
324
  type='text',
362
325
  text=f'CloudFormation template generated at {template_path} with cluster name {cluster_name}',
363
- )
326
+ ),
327
+ TextContent(
328
+ type='text',
329
+ text=json.dumps(data.model_dump()),
330
+ ),
364
331
  ],
365
- template_path=template_path,
366
332
  )
367
333
  except Exception as e:
368
334
  error_message = f'Failed to generate template: {str(e)}'
369
335
  log_with_request_id(ctx, LogLevel.ERROR, error_message)
370
336
 
371
- return GenerateTemplateResponse(
337
+ return CallToolResult(
372
338
  isError=True,
373
339
  content=[TextContent(type='text', text=error_message or 'Unknown error')],
374
- template_path='',
375
340
  )
376
341
 
377
342
  async def _deploy_stack(
378
343
  self, ctx: Context, template_file: str, stack_name: str, cluster_name: str
379
- ) -> DeployStackResponse:
344
+ ) -> CallToolResult:
380
345
  """Deploy a CloudFormation stack from the specified template file."""
381
346
  try:
382
347
  # Create CloudFormation client
@@ -395,14 +360,11 @@ class EksStackHandler:
395
360
  if stack:
396
361
  stack_exists = True
397
362
  if not success:
398
- return DeployStackResponse(
363
+ return CallToolResult(
399
364
  isError=True,
400
365
  content=[
401
366
  TextContent(type='text', text=error_message or 'Unknown error')
402
367
  ],
403
- stack_name=stack_name,
404
- stack_arn='',
405
- cluster_name=cluster_name,
406
368
  )
407
369
  except Exception:
408
370
  # Stack doesn't exist, we'll create it
@@ -447,33 +409,42 @@ class EksStackHandler:
447
409
  f'CloudFormation stack {operation_text} initiated. Stack ARN: {response["StackId"]}',
448
410
  )
449
411
 
450
- return DeployStackResponse(
412
+ data = ManageEksStacksData(
413
+ operation=DEPLOY_OPERATION,
414
+ stack_name=stack_name,
415
+ stack_arn=response['StackId'],
416
+ stack_id=response['StackId'],
417
+ cluster_name=cluster_name,
418
+ template_path='',
419
+ creation_time='',
420
+ stack_status='',
421
+ )
422
+
423
+ return CallToolResult(
451
424
  isError=False,
452
425
  content=[
453
426
  TextContent(
454
427
  type='text',
455
428
  text=f'CloudFormation stack {operation_text} initiated. Stack {operation_text} is in progress and typically takes 15-20 minutes to complete.',
456
- )
429
+ ),
430
+ TextContent(
431
+ type='text',
432
+ text=json.dumps(data.model_dump()),
433
+ ),
457
434
  ],
458
- stack_name=stack_name,
459
- stack_arn=response['StackId'],
460
- cluster_name=cluster_name,
461
435
  )
462
436
  except Exception as e:
463
437
  error_message = f'Failed to deploy stack: {str(e)}'
464
438
  log_with_request_id(ctx, LogLevel.ERROR, error_message)
465
439
 
466
- return DeployStackResponse(
440
+ return CallToolResult(
467
441
  isError=True,
468
442
  content=[TextContent(type='text', text=error_message or 'Unknown error')],
469
- stack_name=stack_name,
470
- stack_arn='',
471
- cluster_name=cluster_name,
472
443
  )
473
444
 
474
445
  async def _describe_stack(
475
446
  self, ctx: Context, stack_name: str, cluster_name: str
476
- ) -> DescribeStackResponse:
447
+ ) -> CallToolResult:
477
448
  """Describe a CloudFormation stack."""
478
449
  try:
479
450
  # Verify stack ownership
@@ -491,15 +462,9 @@ class EksStackHandler:
491
462
  creation_time = stack['CreationTime'].isoformat()
492
463
  stack_status = stack['StackStatus']
493
464
 
494
- return DescribeStackResponse(
465
+ return CallToolResult(
495
466
  isError=True,
496
467
  content=[TextContent(type='text', text=error_message or 'Unknown error')],
497
- stack_name=stack_name,
498
- stack_id=stack_id,
499
- cluster_name=cluster_name,
500
- creation_time=creation_time,
501
- stack_status=stack_status,
502
- outputs={},
503
468
  )
504
469
 
505
470
  # Extract outputs
@@ -533,34 +498,38 @@ class EksStackHandler:
533
498
 
534
499
  stack_status = stack.get('StackStatus', '')
535
500
 
536
- return DescribeStackResponse(
537
- isError=False,
538
- content=[
539
- TextContent(
540
- type='text',
541
- text=f'Successfully described CloudFormation stack {stack_name} for EKS cluster {cluster_name}',
542
- )
543
- ],
501
+ data = ManageEksStacksData(
502
+ operation=DESCRIBE_OPERATION,
544
503
  stack_name=stack_name,
545
504
  stack_id=stack_id,
546
505
  cluster_name=cluster_name,
547
506
  creation_time=creation_time,
548
507
  stack_status=stack_status,
549
508
  outputs=outputs,
509
+ template_path='',
510
+ stack_arn='',
511
+ )
512
+
513
+ return CallToolResult(
514
+ isError=False,
515
+ content=[
516
+ TextContent(
517
+ type='text',
518
+ text=f'Successfully described CloudFormation stack {stack_name} for EKS cluster {cluster_name}',
519
+ ),
520
+ TextContent(
521
+ type='text',
522
+ text=json.dumps(data.model_dump()),
523
+ ),
524
+ ],
550
525
  )
551
526
  except Exception as e:
552
527
  error_message = f'Failed to describe stack: {str(e)}'
553
528
  log_with_request_id(ctx, LogLevel.ERROR, error_message)
554
529
 
555
- return DescribeStackResponse(
530
+ return CallToolResult(
556
531
  isError=True,
557
532
  content=[TextContent(type='text', text=error_message or 'Unknown error')],
558
- stack_name=stack_name,
559
- stack_id='',
560
- cluster_name=cluster_name,
561
- creation_time='',
562
- stack_status='',
563
- outputs={},
564
533
  )
565
534
 
566
535
  def _remove_checkov_metadata(self, resource: Dict[str, Any]) -> None:
@@ -581,7 +550,7 @@ class EksStackHandler:
581
550
 
582
551
  async def _delete_stack(
583
552
  self, ctx: Context, stack_name: str, cluster_name: str
584
- ) -> DeleteStackResponse:
553
+ ) -> CallToolResult:
585
554
  """Delete a CloudFormation stack."""
586
555
  try:
587
556
  # Create CloudFormation client
@@ -595,12 +564,9 @@ class EksStackHandler:
595
564
  if stack:
596
565
  stack_id = stack['StackId']
597
566
 
598
- return DeleteStackResponse(
567
+ return CallToolResult(
599
568
  isError=True,
600
569
  content=[TextContent(type='text', text=error_message or 'Unknown error')],
601
- stack_name=stack_name,
602
- stack_id=stack_id,
603
- cluster_name=cluster_name,
604
570
  )
605
571
 
606
572
  # Safely extract stack ID
@@ -617,26 +583,35 @@ class EksStackHandler:
617
583
  f'Initiated deletion of CloudFormation stack {stack_name} for EKS cluster {cluster_name}',
618
584
  )
619
585
 
620
- return DeleteStackResponse(
586
+ data = ManageEksStacksData(
587
+ operation=DELETE_OPERATION,
588
+ stack_name=stack_name,
589
+ stack_id=stack_id,
590
+ cluster_name=cluster_name,
591
+ template_path='',
592
+ stack_arn='',
593
+ creation_time='',
594
+ stack_status='',
595
+ )
596
+
597
+ return CallToolResult(
621
598
  isError=False,
622
599
  content=[
623
600
  TextContent(
624
601
  type='text',
625
602
  text=f'Initiated deletion of CloudFormation stack {stack_name} for EKS cluster {cluster_name}. Deletion is in progress.',
626
- )
603
+ ),
604
+ TextContent(
605
+ type='text',
606
+ text=json.dumps(data.model_dump()),
607
+ ),
627
608
  ],
628
- stack_name=stack_name,
629
- stack_id=stack_id,
630
- cluster_name=cluster_name,
631
609
  )
632
610
  except Exception as e:
633
611
  error_message = f'Failed to delete stack: {str(e)}'
634
612
  log_with_request_id(ctx, LogLevel.ERROR, error_message)
635
613
 
636
- return DeleteStackResponse(
614
+ return CallToolResult(
637
615
  isError=True,
638
616
  content=[TextContent(type='text', text=error_message or 'Unknown error')],
639
- stack_name=stack_name,
640
- stack_id='',
641
- cluster_name=cluster_name,
642
617
  )
@@ -18,12 +18,12 @@ import json
18
18
  from awslabs.eks_mcp_server.aws_helper import AwsHelper
19
19
  from awslabs.eks_mcp_server.logging_helper import LogLevel, log_with_request_id
20
20
  from awslabs.eks_mcp_server.models import (
21
- AddInlinePolicyResponse,
21
+ AddInlinePolicyData,
22
22
  PolicySummary,
23
- RoleDescriptionResponse,
23
+ RoleDescriptionData,
24
24
  )
25
25
  from mcp.server.fastmcp import Context
26
- from mcp.types import TextContent
26
+ from mcp.types import CallToolResult, TextContent
27
27
  from pydantic import Field
28
28
  from typing import Any, Dict, List, Union
29
29
 
@@ -57,7 +57,7 @@ class IAMHandler:
57
57
  ...,
58
58
  description='Name of the IAM role to get policies for. The role must exist in your AWS account.',
59
59
  ),
60
- ) -> RoleDescriptionResponse:
60
+ ) -> CallToolResult:
61
61
  """Get all policies attached to an IAM role.
62
62
 
63
63
  This tool retrieves all policies associated with an IAM role, providing a comprehensive view
@@ -112,34 +112,36 @@ class IAMHandler:
112
112
  else:
113
113
  assume_role_policy_document = role['AssumeRolePolicyDocument']
114
114
 
115
- # Create the response
116
- return RoleDescriptionResponse(
115
+ # Create the response with structured data
116
+ data = RoleDescriptionData(
117
+ role_arn=role['Arn'],
118
+ assume_role_policy_document=assume_role_policy_document,
119
+ description=role.get('Description'),
120
+ managed_policies=managed_policies,
121
+ inline_policies=inline_policies,
122
+ )
123
+
124
+ return CallToolResult(
117
125
  isError=False,
118
126
  content=[
119
127
  TextContent(
120
128
  type='text',
121
129
  text=f'Successfully retrieved details for IAM role: {role_name}',
122
- )
130
+ ),
131
+ TextContent(
132
+ type='text',
133
+ text=json.dumps(data.model_dump()),
134
+ ),
123
135
  ],
124
- role_arn=role['Arn'],
125
- assume_role_policy_document=assume_role_policy_document,
126
- description=role.get('Description'),
127
- managed_policies=managed_policies,
128
- inline_policies=inline_policies,
129
136
  )
130
137
  except Exception as e:
131
138
  error_message = f'Failed to describe IAM role: {str(e)}'
132
139
  log_with_request_id(ctx, LogLevel.ERROR, error_message)
133
140
 
134
141
  # Return a response with error status
135
- return RoleDescriptionResponse(
142
+ return CallToolResult(
136
143
  isError=True,
137
144
  content=[TextContent(type='text', text=error_message)],
138
- role_arn='',
139
- assume_role_policy_document={},
140
- description=None,
141
- managed_policies=[],
142
- inline_policies=[],
143
145
  )
144
146
 
145
147
  async def add_inline_policy(
@@ -156,7 +158,7 @@ class IAMHandler:
156
158
  description="""Permissions to include in the policy as IAM policy statements in JSON format.
157
159
  Can be either a single statement object or an array of statement objects.""",
158
160
  ),
159
- ) -> AddInlinePolicyResponse:
161
+ ) -> CallToolResult:
160
162
  """Add a new inline policy to an IAM role.
161
163
 
162
164
  This tool creates a new inline policy with the specified permissions and adds it to an IAM role.
@@ -204,12 +206,9 @@ class IAMHandler:
204
206
  if not self.allow_write:
205
207
  error_message = 'Adding inline policies requires --allow-write flag'
206
208
  log_with_request_id(ctx, LogLevel.ERROR, error_message)
207
- return AddInlinePolicyResponse(
209
+ return CallToolResult(
208
210
  isError=True,
209
211
  content=[TextContent(type='text', text=error_message)],
210
- policy_name=policy_name,
211
- role_name=role_name,
212
- permissions_added={},
213
212
  )
214
213
 
215
214
  # Get IAM client
@@ -223,12 +222,9 @@ class IAMHandler:
223
222
  log_with_request_id(ctx, LogLevel.ERROR, error_message)
224
223
 
225
224
  # Return a response with error status
226
- return AddInlinePolicyResponse(
225
+ return CallToolResult(
227
226
  isError=True,
228
227
  content=[TextContent(type='text', text=error_message)],
229
- policy_name=policy_name,
230
- role_name=role_name,
231
- permissions_added={},
232
228
  )
233
229
 
234
230
  def _get_managed_policies(self, ctx, iam_client, role_name):
@@ -324,12 +320,9 @@ class IAMHandler:
324
320
  # If we get here, the policy exists
325
321
  error_message = f'Policy {policy_name} already exists in role {role_name}. Cannot modify existing policies.'
326
322
  log_with_request_id(ctx, LogLevel.ERROR, error_message)
327
- return AddInlinePolicyResponse(
323
+ return CallToolResult(
328
324
  isError=True,
329
325
  content=[TextContent(type='text', text=error_message)],
330
- policy_name=policy_name,
331
- role_name=role_name,
332
- permissions_added={},
333
326
  )
334
327
  except iam_client.exceptions.NoSuchEntityException:
335
328
  # Policy doesn't exist, we can create it
@@ -346,17 +339,24 @@ class IAMHandler:
346
339
  RoleName=role_name, PolicyName=policy_name, PolicyDocument=json.dumps(policy_document)
347
340
  )
348
341
 
349
- return AddInlinePolicyResponse(
342
+ data = AddInlinePolicyData(
343
+ policy_name=policy_name,
344
+ role_name=role_name,
345
+ permissions_added=permissions,
346
+ )
347
+
348
+ return CallToolResult(
350
349
  isError=False,
351
350
  content=[
352
351
  TextContent(
353
352
  type='text',
354
353
  text=f'Successfully created new inline policy {policy_name} in role {role_name}',
355
- )
354
+ ),
355
+ TextContent(
356
+ type='text',
357
+ text=json.dumps(data.model_dump()),
358
+ ),
356
359
  ],
357
- policy_name=policy_name,
358
- role_name=role_name,
359
- permissions_added=permissions,
360
360
  )
361
361
 
362
362
  def _add_permissions_to_document(self, policy_document, permissions):