awslabs.eks-mcp-server 0.1.16__py3-none-any.whl → 0.1.18__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,25 +14,26 @@
14
14
 
15
15
  """Kubernetes 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.k8s_apis import K8sApis
20
21
  from awslabs.eks_mcp_server.k8s_client_cache import K8sClientCache
21
22
  from awslabs.eks_mcp_server.logging_helper import LogLevel, log_with_request_id
22
23
  from awslabs.eks_mcp_server.models import (
23
- ApiVersionsResponse,
24
- ApplyYamlResponse,
24
+ ApiVersionsData,
25
+ ApplyYamlData,
25
26
  EventItem,
26
- EventsResponse,
27
- GenerateAppManifestResponse,
28
- KubernetesResourceListResponse,
29
- KubernetesResourceResponse,
27
+ EventsData,
28
+ GenerateAppManifestData,
29
+ KubernetesResourceData,
30
+ KubernetesResourceListData,
30
31
  Operation,
31
- PodLogsResponse,
32
+ PodLogsData,
32
33
  ResourceSummary,
33
34
  )
34
35
  from mcp.server.fastmcp import Context
35
- from mcp.types import TextContent
36
+ from mcp.types import CallToolResult, TextContent
36
37
  from pydantic import Field
37
38
  from typing import Any, Dict, Optional
38
39
 
@@ -106,7 +107,7 @@ class K8sHandler:
106
107
  True,
107
108
  description='Whether to update resources if they already exist (similar to kubectl apply). Set to false to only create new resources.',
108
109
  ),
109
- ) -> ApplyYamlResponse:
110
+ ) -> CallToolResult:
110
111
  """Apply a Kubernetes YAML from a local file.
111
112
 
112
113
  This tool applies Kubernetes resources defined in a YAML file to an EKS cluster,
@@ -141,12 +142,9 @@ class K8sHandler:
141
142
  if not os.path.isabs(yaml_path):
142
143
  error_msg = f'Path must be absolute: {yaml_path}'
143
144
  log_with_request_id(ctx, LogLevel.ERROR, error_msg)
144
- return ApplyYamlResponse(
145
+ return CallToolResult(
145
146
  isError=True,
146
147
  content=[TextContent(type='text', text=error_msg)],
147
- force_applied=force,
148
- resources_created=0,
149
- resources_updated=0,
150
148
  )
151
149
 
152
150
  # Get Kubernetes client for the cluster
@@ -161,22 +159,16 @@ class K8sHandler:
161
159
  except FileNotFoundError:
162
160
  error_msg = f'YAML file not found: {yaml_path}'
163
161
  log_with_request_id(ctx, LogLevel.ERROR, error_msg)
164
- return ApplyYamlResponse(
162
+ return CallToolResult(
165
163
  isError=True,
166
164
  content=[TextContent(type='text', text=error_msg)],
167
- force_applied=force,
168
- resources_created=0,
169
- resources_updated=0,
170
165
  )
171
166
  except IOError as e:
172
167
  error_msg = f'Error reading YAML file {yaml_path}: {str(e)}'
173
168
  log_with_request_id(ctx, LogLevel.ERROR, error_msg)
174
- return ApplyYamlResponse(
169
+ return CallToolResult(
175
170
  isError=True,
176
171
  content=[TextContent(type='text', text=error_msg)],
177
- force_applied=force,
178
- resources_created=0,
179
- resources_updated=0,
180
172
  )
181
173
 
182
174
  # Parse YAML documents
@@ -203,37 +195,37 @@ class K8sHandler:
203
195
  )
204
196
  log_with_request_id(ctx, LogLevel.INFO, success_msg)
205
197
 
206
- return ApplyYamlResponse(
207
- isError=False,
208
- content=[TextContent(type='text', text=success_msg)],
198
+ data = ApplyYamlData(
209
199
  force_applied=force,
210
200
  resources_created=created_count,
211
201
  resources_updated=updated_count,
212
202
  )
213
203
 
204
+ return CallToolResult(
205
+ isError=False,
206
+ content=[
207
+ TextContent(type='text', text=success_msg),
208
+ TextContent(type='text', text=json.dumps(data.model_dump())),
209
+ ],
210
+ )
211
+
214
212
  except Exception as e:
215
213
  # Any exception means the operation failed
216
214
  error_msg = f'Failed to apply YAML from file {yaml_path}: {str(e)}'
217
215
  log_with_request_id(ctx, LogLevel.ERROR, error_msg)
218
216
 
219
- return ApplyYamlResponse(
217
+ return CallToolResult(
220
218
  isError=True,
221
219
  content=[TextContent(type='text', text=error_msg)],
222
- force_applied=force,
223
- resources_created=0,
224
- resources_updated=0,
225
220
  )
226
221
 
227
222
  except Exception as e:
228
223
  error_msg = f'Error applying YAML from file: {str(e)}'
229
224
  log_with_request_id(ctx, LogLevel.ERROR, error_msg)
230
225
 
231
- return ApplyYamlResponse(
226
+ return CallToolResult(
232
227
  isError=True,
233
228
  content=[TextContent(type='text', text=error_msg)],
234
- force_applied=force,
235
- resources_created=0,
236
- resources_updated=0,
237
229
  )
238
230
 
239
231
  def filter_null_values(self, data: Any) -> Any:
@@ -330,7 +322,7 @@ class K8sHandler:
330
322
  For create and replace, this should be a complete resource definition.
331
323
  For patch, this should contain only the fields to update.""",
332
324
  ),
333
- ) -> KubernetesResourceResponse:
325
+ ) -> CallToolResult:
334
326
  """Manage a single Kubernetes resource with various operations.
335
327
 
336
328
  This tool provides complete CRUD (Create, Read, Update, Delete) operations
@@ -381,30 +373,18 @@ class K8sHandler:
381
373
  valid_ops = ', '.join([op.value for op in Operation])
382
374
  error_msg = f'Invalid operation: {operation}. Valid operations are: {valid_ops}'
383
375
  log_with_request_id(ctx, LogLevel.ERROR, error_msg)
384
- return KubernetesResourceResponse(
376
+ return CallToolResult(
385
377
  isError=True,
386
378
  content=[TextContent(type='text', text=error_msg)],
387
- kind=kind,
388
- name=name or '',
389
- namespace=namespace,
390
- api_version=api_version,
391
- operation=operation,
392
- resource=None,
393
379
  )
394
380
 
395
381
  # Check if write access is disabled and trying to perform a mutating operation
396
382
  if not self.allow_write and operation_enum not in [Operation.READ]:
397
383
  error_msg = f'Operation {operation} is not allowed without write access'
398
384
  log_with_request_id(ctx, LogLevel.ERROR, error_msg)
399
- return KubernetesResourceResponse(
385
+ return CallToolResult(
400
386
  isError=True,
401
387
  content=[TextContent(type='text', text=error_msg)],
402
- kind=kind,
403
- name=name or '',
404
- namespace=namespace,
405
- api_version=api_version,
406
- operation=operation,
407
- resource=None,
408
388
  )
409
389
 
410
390
  # Check if sensitive data access is disabled and trying to read Secret resources
@@ -417,15 +397,9 @@ class K8sHandler:
417
397
  'Access to Kubernetes Secrets requires --allow-sensitive-data-access flag'
418
398
  )
419
399
  log_with_request_id(ctx, LogLevel.ERROR, error_msg)
420
- return KubernetesResourceResponse(
400
+ return CallToolResult(
421
401
  isError=True,
422
402
  content=[TextContent(type='text', text=error_msg)],
423
- kind=kind,
424
- name=name or '',
425
- namespace=namespace,
426
- api_version=api_version,
427
- operation=operation,
428
- resource=None,
429
403
  )
430
404
 
431
405
  # Get Kubernetes client for the cluster
@@ -467,15 +441,8 @@ class K8sHandler:
467
441
  f'Cleaned up resource response for {kind} {resource_name}',
468
442
  )
469
443
 
470
- # Return success response
471
- return KubernetesResourceResponse(
472
- isError=False,
473
- content=[
474
- TextContent(
475
- type='text',
476
- text=f'Successfully {operation_past_tense} {kind} {resource_name}',
477
- )
478
- ],
444
+ # Return success response with structured data
445
+ data = KubernetesResourceData(
479
446
  kind=kind,
480
447
  name=name or '',
481
448
  namespace=namespace,
@@ -484,6 +451,20 @@ class K8sHandler:
484
451
  resource=resource_data,
485
452
  )
486
453
 
454
+ return CallToolResult(
455
+ isError=False,
456
+ content=[
457
+ TextContent(
458
+ type='text',
459
+ text=f'Successfully {operation_past_tense} {kind} {resource_name}',
460
+ ),
461
+ TextContent(
462
+ type='text',
463
+ text=json.dumps(data.model_dump()),
464
+ ),
465
+ ],
466
+ )
467
+
487
468
  except Exception as e:
488
469
  # Log error
489
470
  resource_name = f'{namespace + "/" if namespace else ""}{name or ""}'
@@ -491,15 +472,9 @@ class K8sHandler:
491
472
  log_with_request_id(ctx, LogLevel.ERROR, error_msg)
492
473
 
493
474
  # Return error response
494
- return KubernetesResourceResponse(
475
+ return CallToolResult(
495
476
  isError=True,
496
477
  content=[TextContent(type='text', text=error_msg)],
497
- kind=kind,
498
- name=name or '',
499
- namespace=namespace,
500
- api_version=api_version,
501
- operation=operation,
502
- resource=None,
503
478
  )
504
479
 
505
480
  async def list_k8s_resources(
@@ -533,7 +508,7 @@ class K8sHandler:
533
508
  description="""Field selector to filter resources (e.g., 'metadata.name=my-pod,status.phase=Running').
534
509
  Uses the same syntax as kubectl's --field-selector flag.""",
535
510
  ),
536
- ) -> KubernetesResourceListResponse:
511
+ ) -> CallToolResult:
537
512
  """List Kubernetes resources of a specific kind.
538
513
 
539
514
  This tool lists Kubernetes resources of a specified kind in an EKS cluster,
@@ -609,20 +584,27 @@ class K8sHandler:
609
584
  ctx, LogLevel.INFO, f'Listed {len(summaries)} {kind} resources {resource_location}'
610
585
  )
611
586
 
612
- # Return success response
613
- return KubernetesResourceListResponse(
587
+ # Return success response with structured data
588
+ data = KubernetesResourceListData(
589
+ kind=kind,
590
+ api_version=api_version,
591
+ namespace=namespace,
592
+ count=len(summaries),
593
+ items=summaries,
594
+ )
595
+
596
+ return CallToolResult(
614
597
  isError=False,
615
598
  content=[
616
599
  TextContent(
617
600
  type='text',
618
601
  text=f'Successfully listed {len(summaries)} {kind} resources {resource_location}',
619
- )
602
+ ),
603
+ TextContent(
604
+ type='text',
605
+ text=json.dumps(data.model_dump()),
606
+ ),
620
607
  ],
621
- kind=kind,
622
- api_version=api_version,
623
- namespace=namespace,
624
- count=len(summaries),
625
- items=summaries,
626
608
  )
627
609
 
628
610
  except Exception as e:
@@ -631,14 +613,9 @@ class K8sHandler:
631
613
  log_with_request_id(ctx, LogLevel.ERROR, error_msg)
632
614
 
633
615
  # Return error response
634
- return KubernetesResourceListResponse(
616
+ return CallToolResult(
635
617
  isError=True,
636
618
  content=[TextContent(type='text', text=error_msg)],
637
- kind=kind,
638
- api_version=api_version,
639
- namespace=namespace,
640
- count=0,
641
- items=[],
642
619
  )
643
620
 
644
621
  async def generate_app_manifest(
@@ -674,7 +651,7 @@ class K8sHandler:
674
651
  'internal',
675
652
  description='AWS load balancer scheme. Options: "internal" (private VPC only) or "internet-facing" (public access).',
676
653
  ),
677
- ) -> GenerateAppManifestResponse:
654
+ ) -> CallToolResult:
678
655
  """Generate Kubernetes manifest for a deployment and service.
679
656
 
680
657
  This tool generates Kubernetes manifests for deploying an application to an EKS cluster,
@@ -715,20 +692,18 @@ class K8sHandler:
715
692
  if not self.allow_write:
716
693
  error_msg = 'Operation generate_app_manifest is not allowed without write access'
717
694
  log_with_request_id(ctx, LogLevel.ERROR, error_msg)
718
- return GenerateAppManifestResponse(
695
+ return CallToolResult(
719
696
  isError=True,
720
697
  content=[TextContent(type='text', text=error_msg)],
721
- output_file_path='',
722
698
  )
723
699
 
724
700
  # Validate that the path is absolute
725
701
  if not os.path.isabs(output_dir):
726
702
  error_msg = f'Output directory path must be absolute: {output_dir}'
727
703
  log_with_request_id(ctx, LogLevel.ERROR, error_msg)
728
- return GenerateAppManifestResponse(
704
+ return CallToolResult(
729
705
  isError=True,
730
706
  content=[TextContent(type='text', text=error_msg)],
731
- output_file_path='',
732
707
  )
733
708
 
734
709
  log_with_request_id(
@@ -774,20 +749,25 @@ class K8sHandler:
774
749
 
775
750
  log_with_request_id(ctx, LogLevel.INFO, success_message)
776
751
 
777
- return GenerateAppManifestResponse(
778
- isError=False,
779
- content=[TextContent(type='text', text=success_message)],
752
+ data = GenerateAppManifestData(
780
753
  output_file_path=output_file_path,
781
754
  )
782
755
 
756
+ return CallToolResult(
757
+ isError=False,
758
+ content=[
759
+ TextContent(type='text', text=success_message),
760
+ TextContent(type='text', text=json.dumps(data.model_dump())),
761
+ ],
762
+ )
763
+
783
764
  except Exception as e:
784
765
  error_message = f'Failed to generate YAML: {str(e)}'
785
766
  log_with_request_id(ctx, LogLevel.ERROR, error_message)
786
767
 
787
- return GenerateAppManifestResponse(
768
+ return CallToolResult(
788
769
  isError=True,
789
770
  content=[TextContent(type='text', text=error_message)],
790
- output_file_path='',
791
771
  )
792
772
 
793
773
  def _remove_checkov_skip_annotations(self, content: str) -> str:
@@ -882,7 +862,7 @@ class K8sHandler:
882
862
  False,
883
863
  description='Return previous terminated container logs. Default: false. Useful to get logs for pods that are restarting.',
884
864
  ),
885
- ) -> PodLogsResponse:
865
+ ) -> CallToolResult:
886
866
  """Get logs from a pod in a Kubernetes cluster.
887
867
 
888
868
  This tool retrieves logs from a specified pod in an EKS cluster, with options
@@ -918,13 +898,9 @@ class K8sHandler:
918
898
  if not self.allow_sensitive_data_access:
919
899
  error_msg = 'Access to pod logs requires --allow-sensitive-data-access flag'
920
900
  log_with_request_id(ctx, LogLevel.ERROR, error_msg)
921
- return PodLogsResponse(
901
+ return CallToolResult(
922
902
  isError=True,
923
903
  content=[TextContent(type='text', text=error_msg)],
924
- pod_name=pod_name,
925
- namespace=namespace,
926
- container_name=container_name,
927
- log_lines=[],
928
904
  )
929
905
 
930
906
  try:
@@ -959,19 +935,26 @@ class K8sHandler:
959
935
  f'Retrieved {len(log_lines)} log lines from pod {namespace}/{pod_name}{container_info}',
960
936
  )
961
937
 
962
- # Return success response
963
- return PodLogsResponse(
938
+ # Return success response with structured data
939
+ data = PodLogsData(
940
+ pod_name=pod_name,
941
+ namespace=namespace,
942
+ container_name=container_name,
943
+ log_lines=log_lines,
944
+ )
945
+
946
+ return CallToolResult(
964
947
  isError=False,
965
948
  content=[
966
949
  TextContent(
967
950
  type='text',
968
951
  text=f'Successfully retrieved {len(log_lines)} log lines from pod {namespace}/{pod_name}{container_info}',
969
- )
952
+ ),
953
+ TextContent(
954
+ type='text',
955
+ text=json.dumps(data.model_dump()),
956
+ ),
970
957
  ],
971
- pod_name=pod_name,
972
- namespace=namespace,
973
- container_name=container_name,
974
- log_lines=log_lines,
975
958
  )
976
959
 
977
960
  except Exception as e:
@@ -985,13 +968,9 @@ class K8sHandler:
985
968
  log_with_request_id(ctx, LogLevel.ERROR, error_msg)
986
969
 
987
970
  # Return error response
988
- return PodLogsResponse(
971
+ return CallToolResult(
989
972
  isError=True,
990
973
  content=[TextContent(type='text', text=error_msg)],
991
- pod_name=pod_name,
992
- namespace=namespace,
993
- container_name=container_name,
994
- log_lines=[],
995
974
  )
996
975
 
997
976
  async def get_k8s_events(
@@ -1010,7 +989,7 @@ class K8sHandler:
1010
989
  description="""Namespace of the involved object. Required for namespaced resources (like Pods, Deployments).
1011
990
  Not required for cluster-scoped resources (like Nodes, PersistentVolumes).""",
1012
991
  ),
1013
- ) -> EventsResponse:
992
+ ) -> CallToolResult:
1014
993
  """Get events related to a specific Kubernetes resource.
1015
994
 
1016
995
  This tool retrieves Kubernetes events related to a specific resource, providing
@@ -1048,14 +1027,9 @@ class K8sHandler:
1048
1027
  if not self.allow_sensitive_data_access:
1049
1028
  error_msg = 'Access to Kubernetes events requires --allow-sensitive-data-access flag'
1050
1029
  log_with_request_id(ctx, LogLevel.ERROR, error_msg)
1051
- return EventsResponse(
1030
+ return CallToolResult(
1052
1031
  isError=True,
1053
1032
  content=[TextContent(type='text', text=error_msg)],
1054
- involved_object_kind=kind,
1055
- involved_object_name=name,
1056
- involved_object_namespace=namespace,
1057
- count=0,
1058
- events=[],
1059
1033
  )
1060
1034
 
1061
1035
  try:
@@ -1096,20 +1070,27 @@ class K8sHandler:
1096
1070
  ctx, LogLevel.INFO, f'Retrieved {len(events)} events for {kind} {resource_name}'
1097
1071
  )
1098
1072
 
1099
- # Return success response
1100
- return EventsResponse(
1073
+ # Return success response with structured data
1074
+ data = EventsData(
1075
+ involved_object_kind=kind,
1076
+ involved_object_name=name,
1077
+ involved_object_namespace=namespace,
1078
+ count=len(events),
1079
+ events=event_items,
1080
+ )
1081
+
1082
+ return CallToolResult(
1101
1083
  isError=False,
1102
1084
  content=[
1103
1085
  TextContent(
1104
1086
  type='text',
1105
1087
  text=f'Successfully retrieved {len(events)} events for {kind} {resource_name}',
1106
- )
1088
+ ),
1089
+ TextContent(
1090
+ type='text',
1091
+ text=json.dumps(data.model_dump()),
1092
+ ),
1107
1093
  ],
1108
- involved_object_kind=kind,
1109
- involved_object_name=name,
1110
- involved_object_namespace=namespace,
1111
- count=len(events),
1112
- events=event_items,
1113
1094
  )
1114
1095
 
1115
1096
  except Exception as e:
@@ -1121,14 +1102,9 @@ class K8sHandler:
1121
1102
  log_with_request_id(ctx, LogLevel.ERROR, error_msg)
1122
1103
 
1123
1104
  # Return error response
1124
- return EventsResponse(
1105
+ return CallToolResult(
1125
1106
  isError=True,
1126
1107
  content=[TextContent(type='text', text=error_msg)],
1127
- involved_object_kind=kind,
1128
- involved_object_name=name or '',
1129
- involved_object_namespace=namespace,
1130
- count=0,
1131
- events=[],
1132
1108
  )
1133
1109
 
1134
1110
  async def list_api_versions(
@@ -1137,7 +1113,7 @@ class K8sHandler:
1137
1113
  cluster_name: str = Field(
1138
1114
  ..., description='Name of the EKS cluster to query for available API versions.'
1139
1115
  ),
1140
- ) -> ApiVersionsResponse:
1116
+ ) -> CallToolResult:
1141
1117
  """List all available API versions in the Kubernetes cluster.
1142
1118
 
1143
1119
  This tool discovers all available API versions on the Kubernetes cluster,
@@ -1177,18 +1153,25 @@ class K8sHandler:
1177
1153
  f'Retrieved {len(api_versions)} API versions from cluster {cluster_name}',
1178
1154
  )
1179
1155
 
1180
- # Return success response
1181
- return ApiVersionsResponse(
1156
+ # Return success response with structured data
1157
+ data = ApiVersionsData(
1158
+ cluster_name=cluster_name,
1159
+ api_versions=api_versions,
1160
+ count=len(api_versions),
1161
+ )
1162
+
1163
+ return CallToolResult(
1182
1164
  isError=False,
1183
1165
  content=[
1184
1166
  TextContent(
1185
1167
  type='text',
1186
1168
  text=f'Successfully retrieved {len(api_versions)} API versions from cluster {cluster_name}',
1187
- )
1169
+ ),
1170
+ TextContent(
1171
+ type='text',
1172
+ text=json.dumps(data.model_dump()),
1173
+ ),
1188
1174
  ],
1189
- cluster_name=cluster_name,
1190
- api_versions=api_versions,
1191
- count=len(api_versions),
1192
1175
  )
1193
1176
 
1194
1177
  except Exception as e:
@@ -1197,10 +1180,7 @@ class K8sHandler:
1197
1180
  log_with_request_id(ctx, LogLevel.ERROR, error_msg)
1198
1181
 
1199
1182
  # Return error response
1200
- return ApiVersionsResponse(
1183
+ return CallToolResult(
1201
1184
  isError=True,
1202
1185
  content=[TextContent(type='text', text=error_msg)],
1203
- cluster_name=cluster_name,
1204
- api_versions=[],
1205
- count=0,
1206
1186
  )