awslabs.eks-mcp-server 0.1.4__tar.gz → 0.1.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. {awslabs_eks_mcp_server-0.1.4 → awslabs_eks_mcp_server-0.1.5}/PKG-INFO +22 -8
  2. {awslabs_eks_mcp_server-0.1.4 → awslabs_eks_mcp_server-0.1.5}/README.md +21 -7
  3. awslabs_eks_mcp_server-0.1.5/awslabs/eks_mcp_server/aws_helper.py +108 -0
  4. {awslabs_eks_mcp_server-0.1.4 → awslabs_eks_mcp_server-0.1.5}/awslabs/eks_mcp_server/iam_handler.py +24 -16
  5. {awslabs_eks_mcp_server-0.1.4 → awslabs_eks_mcp_server-0.1.5}/awslabs/eks_mcp_server/k8s_client_cache.py +9 -17
  6. {awslabs_eks_mcp_server-0.1.4 → awslabs_eks_mcp_server-0.1.5}/pyproject.toml +1 -1
  7. {awslabs_eks_mcp_server-0.1.4 → awslabs_eks_mcp_server-0.1.5}/tests/test_aws_helper.py +109 -0
  8. {awslabs_eks_mcp_server-0.1.4 → awslabs_eks_mcp_server-0.1.5}/tests/test_eks_stack_handler.py +362 -0
  9. {awslabs_eks_mcp_server-0.1.4 → awslabs_eks_mcp_server-0.1.5}/tests/test_iam_handler.py +346 -37
  10. {awslabs_eks_mcp_server-0.1.4 → awslabs_eks_mcp_server-0.1.5}/tests/test_k8s_apis.py +434 -0
  11. {awslabs_eks_mcp_server-0.1.4 → awslabs_eks_mcp_server-0.1.5}/tests/test_k8s_client_cache.py +54 -69
  12. awslabs_eks_mcp_server-0.1.4/.pre-commit-config.yaml +0 -14
  13. awslabs_eks_mcp_server-0.1.4/awslabs/eks_mcp_server/aws_helper.py +0 -75
  14. {awslabs_eks_mcp_server-0.1.4 → awslabs_eks_mcp_server-0.1.5}/.gitignore +0 -0
  15. {awslabs_eks_mcp_server-0.1.4 → awslabs_eks_mcp_server-0.1.5}/.python-version +0 -0
  16. {awslabs_eks_mcp_server-0.1.4 → awslabs_eks_mcp_server-0.1.5}/CHANGELOG.md +0 -0
  17. {awslabs_eks_mcp_server-0.1.4 → awslabs_eks_mcp_server-0.1.5}/Dockerfile +0 -0
  18. {awslabs_eks_mcp_server-0.1.4 → awslabs_eks_mcp_server-0.1.5}/LICENSE +0 -0
  19. {awslabs_eks_mcp_server-0.1.4 → awslabs_eks_mcp_server-0.1.5}/NOTICE +0 -0
  20. {awslabs_eks_mcp_server-0.1.4 → awslabs_eks_mcp_server-0.1.5}/awslabs/__init__.py +0 -0
  21. {awslabs_eks_mcp_server-0.1.4 → awslabs_eks_mcp_server-0.1.5}/awslabs/eks_mcp_server/__init__.py +0 -0
  22. {awslabs_eks_mcp_server-0.1.4 → awslabs_eks_mcp_server-0.1.5}/awslabs/eks_mcp_server/cloudwatch_handler.py +0 -0
  23. {awslabs_eks_mcp_server-0.1.4 → awslabs_eks_mcp_server-0.1.5}/awslabs/eks_mcp_server/cloudwatch_metrics_guidance_handler.py +0 -0
  24. {awslabs_eks_mcp_server-0.1.4 → awslabs_eks_mcp_server-0.1.5}/awslabs/eks_mcp_server/consts.py +0 -0
  25. {awslabs_eks_mcp_server-0.1.4 → awslabs_eks_mcp_server-0.1.5}/awslabs/eks_mcp_server/data/eks_cloudwatch_metrics_guidance.json +0 -0
  26. {awslabs_eks_mcp_server-0.1.4 → awslabs_eks_mcp_server-0.1.5}/awslabs/eks_mcp_server/eks_kb_handler.py +0 -0
  27. {awslabs_eks_mcp_server-0.1.4 → awslabs_eks_mcp_server-0.1.5}/awslabs/eks_mcp_server/eks_stack_handler.py +0 -0
  28. {awslabs_eks_mcp_server-0.1.4 → awslabs_eks_mcp_server-0.1.5}/awslabs/eks_mcp_server/k8s_apis.py +0 -0
  29. {awslabs_eks_mcp_server-0.1.4 → awslabs_eks_mcp_server-0.1.5}/awslabs/eks_mcp_server/k8s_handler.py +0 -0
  30. {awslabs_eks_mcp_server-0.1.4 → awslabs_eks_mcp_server-0.1.5}/awslabs/eks_mcp_server/logging_helper.py +0 -0
  31. {awslabs_eks_mcp_server-0.1.4 → awslabs_eks_mcp_server-0.1.5}/awslabs/eks_mcp_server/models.py +0 -0
  32. {awslabs_eks_mcp_server-0.1.4 → awslabs_eks_mcp_server-0.1.5}/awslabs/eks_mcp_server/scripts/update_eks_cloudwatch_metrics_guidance.py +0 -0
  33. {awslabs_eks_mcp_server-0.1.4 → awslabs_eks_mcp_server-0.1.5}/awslabs/eks_mcp_server/server.py +0 -0
  34. {awslabs_eks_mcp_server-0.1.4 → awslabs_eks_mcp_server-0.1.5}/awslabs/eks_mcp_server/templates/eks-templates/eks-with-vpc.yaml +0 -0
  35. {awslabs_eks_mcp_server-0.1.4 → awslabs_eks_mcp_server-0.1.5}/awslabs/eks_mcp_server/templates/k8s-templates/deployment.yaml +0 -0
  36. {awslabs_eks_mcp_server-0.1.4 → awslabs_eks_mcp_server-0.1.5}/awslabs/eks_mcp_server/templates/k8s-templates/service.yaml +0 -0
  37. {awslabs_eks_mcp_server-0.1.4 → awslabs_eks_mcp_server-0.1.5}/docker-healthcheck.sh +0 -0
  38. {awslabs_eks_mcp_server-0.1.4 → awslabs_eks_mcp_server-0.1.5}/tests/test_cloudwatch_handler.py +0 -0
  39. {awslabs_eks_mcp_server-0.1.4 → awslabs_eks_mcp_server-0.1.5}/tests/test_cloudwatch_metrics_guidance_handler.py +0 -0
  40. {awslabs_eks_mcp_server-0.1.4 → awslabs_eks_mcp_server-0.1.5}/tests/test_eks_kb_handler.py +0 -0
  41. {awslabs_eks_mcp_server-0.1.4 → awslabs_eks_mcp_server-0.1.5}/tests/test_init.py +0 -0
  42. {awslabs_eks_mcp_server-0.1.4 → awslabs_eks_mcp_server-0.1.5}/tests/test_k8s_handler.py +0 -0
  43. {awslabs_eks_mcp_server-0.1.4 → awslabs_eks_mcp_server-0.1.5}/tests/test_logging_helper.py +0 -0
  44. {awslabs_eks_mcp_server-0.1.4 → awslabs_eks_mcp_server-0.1.5}/tests/test_main.py +0 -0
  45. {awslabs_eks_mcp_server-0.1.4 → awslabs_eks_mcp_server-0.1.5}/tests/test_models.py +0 -0
  46. {awslabs_eks_mcp_server-0.1.4 → awslabs_eks_mcp_server-0.1.5}/tests/test_server.py +0 -0
  47. {awslabs_eks_mcp_server-0.1.4 → awslabs_eks_mcp_server-0.1.5}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: awslabs.eks-mcp-server
3
- Version: 0.1.4
3
+ Version: 0.1.5
4
4
  Summary: An AWS Labs Model Context Protocol (MCP) server for EKS
5
5
  Project-URL: homepage, https://awslabs.github.io/mcp/
6
6
  Project-URL: docs, https://awslabs.github.io/mcp/servers/eks-mcp-server/
@@ -128,7 +128,7 @@ This quickstart guide walks you through the steps to configure the Amazon EKS MC
128
128
 
129
129
  1. Open Cursor.
130
130
  2. Click the gear icon (⚙️) in the top right to open the settings panel, click **MCP**, **Add new global MCP server**.
131
- 3. Paste your MCP server definition. For example, this example shows how to configure the EKS MCP Server, including enabling mutating actions by adding the `--allow-write` flag to the server arguments:
131
+ 3. Paste your MCP server definition. For example, this example shows how to configure the EKS MCP Server, including enabling mutating actions with the `--allow-write` flag and access to sensitive data with the `--allow-sensitive-data-access` flag (see the Arguments section for more details):
132
132
 
133
133
  **For Mac/Linux:**
134
134
 
@@ -141,7 +141,8 @@ This quickstart guide walks you through the steps to configure the Amazon EKS MC
141
141
  "command": "uvx",
142
142
  "args": [
143
143
  "awslabs.eks-mcp-server@latest",
144
- "--allow-write"
144
+ "--allow-write",
145
+ "--allow-sensitive-data-access"
145
146
  ],
146
147
  "env": {
147
148
  "FASTMCP_LOG_LEVEL": "ERROR"
@@ -165,7 +166,8 @@ This quickstart guide walks you through the steps to configure the Amazon EKS MC
165
166
  "--from",
166
167
  "awslabs.eks-mcp-server@latest",
167
168
  "awslabs.eks-mcp-server.exe",
168
- "--allow-write"
169
+ "--allow-write",
170
+ "--allow-sensitive-data-access"
169
171
  ],
170
172
  "env": {
171
173
  "FASTMCP_LOG_LEVEL": "ERROR"
@@ -183,7 +185,9 @@ This quickstart guide walks you through the steps to configure the Amazon EKS MC
183
185
  **Set up the Amazon Q Developer CLI**
184
186
 
185
187
  1. Install the [Amazon Q Developer CLI](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/command-line-installing.html) .
186
- 2. The Q Developer CLI supports MCP servers for tools and prompts out-of-the-box. Edit your Q developer CLI's MCP configuration file named mcp.json following [these instructions](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/command-line-mcp-configuration.html). For example:
188
+ 2. The Q Developer CLI supports MCP servers for tools and prompts out-of-the-box. Edit your Q developer CLI's MCP configuration file named mcp.json following [these instructions](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/command-line-mcp-configuration.html).
189
+
190
+ The example below includes both the `--allow-write` flag for mutating operations and the `--allow-sensitive-data-access` flag for accessing logs and events (see the Arguments section for more details):
187
191
 
188
192
  **For Mac/Linux:**
189
193
 
@@ -192,7 +196,11 @@ This quickstart guide walks you through the steps to configure the Amazon EKS MC
192
196
  "mcpServers": {
193
197
  "awslabs.eks-mcp-server": {
194
198
  "command": "uvx",
195
- "args": ["awslabs.eks-mcp-server@latest"],
199
+ "args": [
200
+ "awslabs.eks-mcp-server@latest",
201
+ "--allow-write",
202
+ "--allow-sensitive-data-access"
203
+ ],
196
204
  "env": {
197
205
  "FASTMCP_LOG_LEVEL": "ERROR"
198
206
  },
@@ -210,7 +218,13 @@ This quickstart guide walks you through the steps to configure the Amazon EKS MC
210
218
  "mcpServers": {
211
219
  "awslabs.eks-mcp-server": {
212
220
  "command": "uvx",
213
- "args": ["--from", "awslabs.eks-mcp-server@latest", "awslabs.eks-mcp-server.exe"],
221
+ "args": [
222
+ "--from",
223
+ "awslabs.eks-mcp-server@latest",
224
+ "awslabs.eks-mcp-server.exe",
225
+ "--allow-write",
226
+ "--allow-sensitive-data-access"
227
+ ],
214
228
  "env": {
215
229
  "FASTMCP_LOG_LEVEL": "ERROR"
216
230
  },
@@ -294,7 +308,7 @@ Enables write access mode, which allows mutating operations (e.g., create, updat
294
308
 
295
309
  #### `--allow-sensitive-data-access` (optional)
296
310
 
297
- Enables access to sensitive data such as logs, events, and Kubernetes Secrets.
311
+ Enables access to sensitive data such as logs, events, and Kubernetes Secrets. This flag is required for tools that access potentially sensitive information, such as get_pod_logs, get_k8s_events, get_cloudwatch_logs, and manage_k8s_resource (when used to read Kubernetes secrets).
298
312
 
299
313
  * Default: false (Access to sensitive data is restricted by default)
300
314
  * Example: Add `--allow-sensitive-data-access` to the `args` list in your MCP server definition.
@@ -94,7 +94,7 @@ This quickstart guide walks you through the steps to configure the Amazon EKS MC
94
94
 
95
95
  1. Open Cursor.
96
96
  2. Click the gear icon (⚙️) in the top right to open the settings panel, click **MCP**, **Add new global MCP server**.
97
- 3. Paste your MCP server definition. For example, this example shows how to configure the EKS MCP Server, including enabling mutating actions by adding the `--allow-write` flag to the server arguments:
97
+ 3. Paste your MCP server definition. For example, this example shows how to configure the EKS MCP Server, including enabling mutating actions with the `--allow-write` flag and access to sensitive data with the `--allow-sensitive-data-access` flag (see the Arguments section for more details):
98
98
 
99
99
  **For Mac/Linux:**
100
100
 
@@ -107,7 +107,8 @@ This quickstart guide walks you through the steps to configure the Amazon EKS MC
107
107
  "command": "uvx",
108
108
  "args": [
109
109
  "awslabs.eks-mcp-server@latest",
110
- "--allow-write"
110
+ "--allow-write",
111
+ "--allow-sensitive-data-access"
111
112
  ],
112
113
  "env": {
113
114
  "FASTMCP_LOG_LEVEL": "ERROR"
@@ -131,7 +132,8 @@ This quickstart guide walks you through the steps to configure the Amazon EKS MC
131
132
  "--from",
132
133
  "awslabs.eks-mcp-server@latest",
133
134
  "awslabs.eks-mcp-server.exe",
134
- "--allow-write"
135
+ "--allow-write",
136
+ "--allow-sensitive-data-access"
135
137
  ],
136
138
  "env": {
137
139
  "FASTMCP_LOG_LEVEL": "ERROR"
@@ -149,7 +151,9 @@ This quickstart guide walks you through the steps to configure the Amazon EKS MC
149
151
  **Set up the Amazon Q Developer CLI**
150
152
 
151
153
  1. Install the [Amazon Q Developer CLI](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/command-line-installing.html) .
152
- 2. The Q Developer CLI supports MCP servers for tools and prompts out-of-the-box. Edit your Q developer CLI's MCP configuration file named mcp.json following [these instructions](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/command-line-mcp-configuration.html). For example:
154
+ 2. The Q Developer CLI supports MCP servers for tools and prompts out-of-the-box. Edit your Q developer CLI's MCP configuration file named mcp.json following [these instructions](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/command-line-mcp-configuration.html).
155
+
156
+ The example below includes both the `--allow-write` flag for mutating operations and the `--allow-sensitive-data-access` flag for accessing logs and events (see the Arguments section for more details):
153
157
 
154
158
  **For Mac/Linux:**
155
159
 
@@ -158,7 +162,11 @@ This quickstart guide walks you through the steps to configure the Amazon EKS MC
158
162
  "mcpServers": {
159
163
  "awslabs.eks-mcp-server": {
160
164
  "command": "uvx",
161
- "args": ["awslabs.eks-mcp-server@latest"],
165
+ "args": [
166
+ "awslabs.eks-mcp-server@latest",
167
+ "--allow-write",
168
+ "--allow-sensitive-data-access"
169
+ ],
162
170
  "env": {
163
171
  "FASTMCP_LOG_LEVEL": "ERROR"
164
172
  },
@@ -176,7 +184,13 @@ This quickstart guide walks you through the steps to configure the Amazon EKS MC
176
184
  "mcpServers": {
177
185
  "awslabs.eks-mcp-server": {
178
186
  "command": "uvx",
179
- "args": ["--from", "awslabs.eks-mcp-server@latest", "awslabs.eks-mcp-server.exe"],
187
+ "args": [
188
+ "--from",
189
+ "awslabs.eks-mcp-server@latest",
190
+ "awslabs.eks-mcp-server.exe",
191
+ "--allow-write",
192
+ "--allow-sensitive-data-access"
193
+ ],
180
194
  "env": {
181
195
  "FASTMCP_LOG_LEVEL": "ERROR"
182
196
  },
@@ -260,7 +274,7 @@ Enables write access mode, which allows mutating operations (e.g., create, updat
260
274
 
261
275
  #### `--allow-sensitive-data-access` (optional)
262
276
 
263
- Enables access to sensitive data such as logs, events, and Kubernetes Secrets.
277
+ Enables access to sensitive data such as logs, events, and Kubernetes Secrets. This flag is required for tools that access potentially sensitive information, such as get_pod_logs, get_k8s_events, get_cloudwatch_logs, and manage_k8s_resource (when used to read Kubernetes secrets).
264
278
 
265
279
  * Default: false (Access to sensitive data is restricted by default)
266
280
  * Example: Add `--allow-sensitive-data-access` to the `args` list in your MCP server definition.
@@ -0,0 +1,108 @@
1
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """AWS helper for the EKS MCP Server."""
16
+
17
+ import boto3
18
+ import os
19
+ from awslabs.eks_mcp_server import __version__
20
+ from botocore.config import Config
21
+ from loguru import logger
22
+ from typing import Any, Dict, Optional
23
+
24
+
25
+ class AwsHelper:
26
+ """Helper class for AWS operations.
27
+
28
+ This class provides utility methods for interacting with AWS services,
29
+ including region and profile management and client creation.
30
+
31
+ This class implements a singleton pattern with a client cache to avoid
32
+ creating multiple clients for the same service.
33
+ """
34
+
35
+ # Singleton instance
36
+ _instance = None
37
+
38
+ # Client cache with AWS service name as key
39
+ _client_cache: Dict[str, Any] = {}
40
+
41
+ @staticmethod
42
+ def get_aws_region() -> Optional[str]:
43
+ """Get the AWS region from the environment if set."""
44
+ return os.environ.get('AWS_REGION')
45
+
46
+ @staticmethod
47
+ def get_aws_profile() -> Optional[str]:
48
+ """Get the AWS profile from the environment if set."""
49
+ return os.environ.get('AWS_PROFILE')
50
+
51
+ @classmethod
52
+ def create_boto3_client(cls, service_name: str, region_name: Optional[str] = None) -> Any:
53
+ """Create or retrieve a cached boto3 client with the appropriate profile and region.
54
+
55
+ The client is configured with a custom user agent suffix 'awslabs/mcp/eks-mcp-server/{version}'
56
+ to identify API calls made by the EKS MCP Server. Clients are cached to improve performance
57
+ and reduce resource usage.
58
+
59
+ Args:
60
+ service_name: The AWS service name (e.g., 'ec2', 's3', 'eks')
61
+ region_name: Optional region name override
62
+
63
+ Returns:
64
+ A boto3 client for the specified service
65
+
66
+ Raises:
67
+ Exception: If there's an error creating the client
68
+ """
69
+ try:
70
+ # Get region from parameter or environment if set
71
+ region: Optional[str] = (
72
+ region_name if region_name is not None else cls.get_aws_region()
73
+ )
74
+
75
+ # Get profile from environment if set
76
+ profile = cls.get_aws_profile()
77
+
78
+ # Use service name as the cache key
79
+ cache_key = service_name
80
+
81
+ # Check if client is already in cache
82
+ if cache_key in cls._client_cache:
83
+ logger.info(f'Using cached boto3 client for {service_name}')
84
+ return cls._client_cache[cache_key]
85
+
86
+ # Create config with user agent suffix
87
+ config = Config(user_agent_extra=f'awslabs/mcp/eks-mcp-server/{__version__}')
88
+
89
+ # Create session with profile if specified
90
+ if profile:
91
+ session = boto3.Session(profile_name=profile)
92
+ if region is not None:
93
+ client = session.client(service_name, region_name=region, config=config)
94
+ else:
95
+ client = session.client(service_name, config=config)
96
+ else:
97
+ if region is not None:
98
+ client = boto3.client(service_name, region_name=region, config=config)
99
+ else:
100
+ client = boto3.client(service_name, config=config)
101
+
102
+ # Cache the client
103
+ cls._client_cache[cache_key] = client
104
+
105
+ return client
106
+ except Exception as e:
107
+ # Re-raise with more context
108
+ raise Exception(f'Failed to create boto3 client for {service_name}: {str(e)}')
@@ -44,7 +44,6 @@ class IAMHandler:
44
44
  allow_write: Whether to enable write access (default: False)
45
45
  """
46
46
  self.mcp = mcp
47
- self.iam_client = AwsHelper.create_boto3_client('iam')
48
47
  self.allow_write = allow_write
49
48
 
50
49
  # Register tools
@@ -94,15 +93,18 @@ class IAMHandler:
94
93
  try:
95
94
  log_with_request_id(ctx, LogLevel.INFO, f'Describing IAM role: {role_name}')
96
95
 
96
+ # Get IAM client
97
+ iam_client = AwsHelper.create_boto3_client('iam')
98
+
97
99
  # Get role details
98
- role_response = self.iam_client.get_role(RoleName=role_name)
100
+ role_response = iam_client.get_role(RoleName=role_name)
99
101
  role = role_response['Role']
100
102
 
101
103
  # Get attached managed policies
102
- managed_policies = self._get_managed_policies(ctx, role_name)
104
+ managed_policies = self._get_managed_policies(ctx, iam_client, role_name)
103
105
 
104
106
  # Get inline policies
105
- inline_policies = self._get_inline_policies(ctx, role_name)
107
+ inline_policies = self._get_inline_policies(ctx, iam_client, role_name)
106
108
 
107
109
  # Parse the assume role policy document if it's a string, otherwise use it directly
108
110
  if isinstance(role['AssumeRolePolicyDocument'], str):
@@ -210,8 +212,11 @@ class IAMHandler:
210
212
  permissions_added={},
211
213
  )
212
214
 
215
+ # Get IAM client
216
+ iam_client = AwsHelper.create_boto3_client('iam')
217
+
213
218
  # Create the inline policy
214
- return self._create_inline_policy(ctx, role_name, policy_name, permissions)
219
+ return self._create_inline_policy(ctx, iam_client, role_name, policy_name, permissions)
215
220
 
216
221
  except Exception as e:
217
222
  error_message = f'Failed to create inline policy: {str(e)}'
@@ -226,27 +231,28 @@ class IAMHandler:
226
231
  permissions_added={},
227
232
  )
228
233
 
229
- def _get_managed_policies(self, ctx, role_name):
234
+ def _get_managed_policies(self, ctx, iam_client, role_name):
230
235
  """Get managed policies attached to a role.
231
236
 
232
237
  Args:
233
238
  ctx: The MCP context
239
+ iam_client: IAM client to use
234
240
  role_name: Name of the IAM role
235
241
 
236
242
  Returns:
237
243
  List of PolicySummary objects
238
244
  """
239
245
  managed_policies = []
240
- managed_policies_response = self.iam_client.list_attached_role_policies(RoleName=role_name)
246
+ managed_policies_response = iam_client.list_attached_role_policies(RoleName=role_name)
241
247
 
242
248
  for policy in managed_policies_response.get('AttachedPolicies', []):
243
249
  policy_arn = policy['PolicyArn']
244
- policy_details = self.iam_client.get_policy(PolicyArn=policy_arn)['Policy']
250
+ policy_details = iam_client.get_policy(PolicyArn=policy_arn)['Policy']
245
251
 
246
252
  # Get the policy version details to get the policy document
247
253
  policy_version = None
248
254
  try:
249
- policy_version_response = self.iam_client.get_policy_version(
255
+ policy_version_response = iam_client.get_policy_version(
250
256
  PolicyArn=policy_arn, VersionId=policy_details.get('DefaultVersionId', 'v1')
251
257
  )
252
258
  policy_version = policy_version_response.get('PolicyVersion', {})
@@ -265,21 +271,22 @@ class IAMHandler:
265
271
 
266
272
  return managed_policies
267
273
 
268
- def _get_inline_policies(self, ctx, role_name):
274
+ def _get_inline_policies(self, ctx, iam_client, role_name):
269
275
  """Get inline policies embedded in a role.
270
276
 
271
277
  Args:
272
278
  ctx: The MCP context
279
+ iam_client: IAM client to use
273
280
  role_name: Name of the IAM role
274
281
 
275
282
  Returns:
276
283
  List of PolicySummary objects
277
284
  """
278
285
  inline_policies = []
279
- inline_policies_response = self.iam_client.list_role_policies(RoleName=role_name)
286
+ inline_policies_response = iam_client.list_role_policies(RoleName=role_name)
280
287
 
281
288
  for policy_name in inline_policies_response.get('PolicyNames', []):
282
- policy_response = self.iam_client.get_role_policy(
289
+ policy_response = iam_client.get_role_policy(
283
290
  RoleName=role_name, PolicyName=policy_name
284
291
  )
285
292
 
@@ -293,11 +300,12 @@ class IAMHandler:
293
300
 
294
301
  return inline_policies
295
302
 
296
- def _create_inline_policy(self, ctx, role_name, policy_name, permissions):
303
+ def _create_inline_policy(self, ctx, iam_client, role_name, policy_name, permissions):
297
304
  """Create a new inline policy with the specified permissions.
298
305
 
299
306
  Args:
300
307
  ctx: The MCP context
308
+ iam_client: IAM client to use
301
309
  role_name: Name of the role
302
310
  policy_name: Name of the new policy to create
303
311
  permissions: Permissions to include in the policy
@@ -313,7 +321,7 @@ class IAMHandler:
313
321
 
314
322
  # Check if the policy already exists
315
323
  try:
316
- self.iam_client.get_role_policy(RoleName=role_name, PolicyName=policy_name)
324
+ iam_client.get_role_policy(RoleName=role_name, PolicyName=policy_name)
317
325
  # If we get here, the policy exists
318
326
  error_message = f'Policy {policy_name} already exists in role {role_name}. Cannot modify existing policies.'
319
327
  log_with_request_id(ctx, LogLevel.ERROR, error_message)
@@ -324,7 +332,7 @@ class IAMHandler:
324
332
  role_name=role_name,
325
333
  permissions_added={},
326
334
  )
327
- except self.iam_client.exceptions.NoSuchEntityException:
335
+ except iam_client.exceptions.NoSuchEntityException:
328
336
  # Policy doesn't exist, we can create it
329
337
  pass
330
338
 
@@ -335,7 +343,7 @@ class IAMHandler:
335
343
  self._add_permissions_to_document(policy_document, permissions)
336
344
 
337
345
  # Create the policy
338
- self.iam_client.put_role_policy(
346
+ iam_client.put_role_policy(
339
347
  RoleName=role_name, PolicyName=policy_name, PolicyDocument=json.dumps(policy_document)
340
348
  )
341
349
 
@@ -55,24 +55,17 @@ class K8sClientCache:
55
55
  # Client cache with TTL to handle token expiration
56
56
  self._client_cache = TTLCache(maxsize=100, ttl=TOKEN_TTL)
57
57
 
58
- # Clients for credential retrieval
59
- self._eks_client = None
60
- self._sts_client = None
58
+ # Flag to track if STS event handlers have been registered
59
+ self._sts_event_handlers_registered = False
61
60
 
62
61
  self._initialized = True
63
62
 
64
- def _get_eks_client(self):
65
- """Get or create the EKS client."""
66
- if self._eks_client is None:
67
- self._eks_client = AwsHelper.create_boto3_client('eks')
68
- return self._eks_client
69
-
70
63
  def _get_sts_client(self):
71
- """Get or create the STS client with event handlers registered."""
72
- if self._sts_client is None:
73
- sts_client = AwsHelper.create_boto3_client('sts')
64
+ """Get the STS client with event handlers registered."""
65
+ sts_client = AwsHelper.create_boto3_client('sts')
74
66
 
75
- # Register STS event handlers
67
+ # Register STS event handlers only once
68
+ if not self._sts_event_handlers_registered:
76
69
  sts_client.meta.events.register(
77
70
  'provide-client-params.sts.GetCallerIdentity',
78
71
  self._retrieve_k8s_aws_id,
@@ -81,10 +74,9 @@ class K8sClientCache:
81
74
  'before-sign.sts.GetCallerIdentity',
82
75
  self._inject_k8s_aws_id_header,
83
76
  )
77
+ self._sts_event_handlers_registered = True
84
78
 
85
- self._sts_client = sts_client
86
-
87
- return self._sts_client
79
+ return sts_client
88
80
 
89
81
  def _retrieve_k8s_aws_id(self, params, context, **kwargs):
90
82
  """Retrieve the Kubernetes AWS ID from parameters."""
@@ -109,7 +101,7 @@ class K8sClientCache:
109
101
  ValueError: If the cluster credentials are invalid
110
102
  Exception: If there's an error getting the cluster credentials
111
103
  """
112
- eks_client = self._get_eks_client()
104
+ eks_client = AwsHelper.create_boto3_client('eks')
113
105
  sts_client = self._get_sts_client()
114
106
 
115
107
  # Get cluster details
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "awslabs.eks-mcp-server"
3
- version = "0.1.4"
3
+ version = "0.1.5"
4
4
  description = "An AWS Labs Model Context Protocol (MCP) server for EKS"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -23,6 +23,11 @@ from unittest.mock import ANY, MagicMock, patch
23
23
  class TestAwsHelper:
24
24
  """Tests for the AwsHelper class."""
25
25
 
26
+ def setup_method(self):
27
+ """Set up the test environment."""
28
+ # Clear the client cache before each test
29
+ AwsHelper._client_cache = {}
30
+
26
31
  @patch.dict(os.environ, {'AWS_REGION': 'us-west-2'})
27
32
  def test_get_aws_region_from_env(self):
28
33
  """Test that get_aws_region returns the region from the environment."""
@@ -151,3 +156,107 @@ class TestAwsHelper:
151
156
  assert config is not None
152
157
  expected_user_agent = f'awslabs/mcp/eks-mcp-server/{__version__}'
153
158
  assert config.user_agent_extra == expected_user_agent
159
+
160
+ @patch('boto3.client')
161
+ def test_client_caching(self, mock_boto3_client):
162
+ """Test that clients are cached and reused."""
163
+ # Create a mock client
164
+ mock_client = MagicMock()
165
+ mock_boto3_client.return_value = mock_client
166
+
167
+ # Mock the get_aws_profile and get_aws_region methods
168
+ with patch.object(AwsHelper, 'get_aws_profile', return_value=None):
169
+ with patch.object(AwsHelper, 'get_aws_region', return_value='us-west-2'):
170
+ # Call create_boto3_client twice with the same parameters
171
+ client1 = AwsHelper.create_boto3_client('cloudformation')
172
+ client2 = AwsHelper.create_boto3_client('cloudformation')
173
+
174
+ # Verify that boto3.client was called only once
175
+ mock_boto3_client.assert_called_once()
176
+
177
+ # Verify that the same client instance was returned both times
178
+ assert client1 is client2
179
+
180
+ @patch('boto3.client')
181
+ def test_different_services_not_cached_together(self, mock_boto3_client):
182
+ """Test that different services get different cached clients."""
183
+ # Create mock clients
184
+ mock_cf_client = MagicMock()
185
+ mock_s3_client = MagicMock()
186
+ mock_boto3_client.side_effect = [mock_cf_client, mock_s3_client]
187
+
188
+ # Mock the get_aws_profile and get_aws_region methods
189
+ with patch.object(AwsHelper, 'get_aws_profile', return_value=None):
190
+ with patch.object(AwsHelper, 'get_aws_region', return_value='us-west-2'):
191
+ # Call create_boto3_client for different services
192
+ cf_client = AwsHelper.create_boto3_client('cloudformation')
193
+ s3_client = AwsHelper.create_boto3_client('s3')
194
+
195
+ # Verify that boto3.client was called twice
196
+ assert mock_boto3_client.call_count == 2
197
+
198
+ # Verify that different client instances were returned
199
+ assert cf_client is not s3_client
200
+
201
+ @patch('boto3.client')
202
+ def test_same_service_different_regions_cached_together(self, mock_boto3_client):
203
+ """Test that clients for the same service are cached together regardless of region."""
204
+ # Create mock client
205
+ mock_client = MagicMock()
206
+ mock_boto3_client.return_value = mock_client
207
+
208
+ # Mock the get_aws_profile method
209
+ with patch.object(AwsHelper, 'get_aws_profile', return_value=None):
210
+ # Call create_boto3_client for different regions
211
+ us_west_client = AwsHelper.create_boto3_client(
212
+ 'cloudformation', region_name='us-west-2'
213
+ )
214
+ eu_west_client = AwsHelper.create_boto3_client(
215
+ 'cloudformation', region_name='eu-west-1'
216
+ )
217
+
218
+ # Verify that boto3.client was called only once
219
+ mock_boto3_client.assert_called_once()
220
+
221
+ # Verify that the same client instance was returned both times
222
+ assert us_west_client is eu_west_client
223
+
224
+ @patch('boto3.client')
225
+ def test_error_handling(self, mock_boto3_client):
226
+ """Test that errors during client creation are handled properly."""
227
+ # Make boto3.client raise an exception
228
+ mock_boto3_client.side_effect = Exception('Test error')
229
+
230
+ # Mock the get_aws_profile and get_aws_region methods
231
+ with patch.object(AwsHelper, 'get_aws_profile', return_value=None):
232
+ with patch.object(AwsHelper, 'get_aws_region', return_value=None):
233
+ # Verify that the exception is re-raised with more context
234
+ try:
235
+ AwsHelper.create_boto3_client('cloudformation')
236
+ assert False, 'Exception was not raised'
237
+ except Exception as e:
238
+ assert 'Failed to create boto3 client for cloudformation: Test error' in str(e)
239
+
240
+ @patch('boto3.Session')
241
+ def test_same_service_different_profiles_cached_together(self, mock_boto3_session):
242
+ """Test that clients for the same service are cached together regardless of profile."""
243
+ # Create mock session and client
244
+ mock_session = MagicMock()
245
+ mock_client = MagicMock()
246
+ mock_session.client.return_value = mock_client
247
+ mock_boto3_session.return_value = mock_session
248
+
249
+ # Call create_boto3_client with different profiles
250
+ with patch.object(AwsHelper, 'get_aws_profile', return_value='profile1'):
251
+ with patch.object(AwsHelper, 'get_aws_region', return_value=None):
252
+ client1 = AwsHelper.create_boto3_client('cloudformation')
253
+
254
+ with patch.object(AwsHelper, 'get_aws_profile', return_value='profile2'):
255
+ with patch.object(AwsHelper, 'get_aws_region', return_value=None):
256
+ client2 = AwsHelper.create_boto3_client('cloudformation')
257
+
258
+ # Verify that boto3.Session was called only once
259
+ mock_boto3_session.assert_called_once()
260
+
261
+ # Verify that the same client instance was returned both times
262
+ assert client1 is client2