kubectl-mcp-server 1.22.0__py3-none-any.whl → 1.23.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kubectl-mcp-server
3
- Version: 1.22.0
3
+ Version: 1.23.1
4
4
  Summary: A Model Context Protocol (MCP) server for Kubernetes with 270+ tools, 8 resources, and 8 prompts
5
5
  Home-page: https://github.com/rohitg00/kubectl-mcp-server
6
6
  Author: Rohit Ghumare
@@ -1,12 +1,12 @@
1
- kubectl_mcp_server-1.22.0.dist-info/licenses/LICENSE,sha256=nH9Z0W0WNH2oQ4cPrBAU8ldDcHfeI6NUbkSGiazYWgQ,1070
2
- kubectl_mcp_tool/__init__.py,sha256=aOJUw-ueQaKnSHF0Tid1B-rwVKQZfXqcDV2S2-9f3WY,580
1
+ kubectl_mcp_server-1.23.1.dist-info/licenses/LICENSE,sha256=nH9Z0W0WNH2oQ4cPrBAU8ldDcHfeI6NUbkSGiazYWgQ,1070
2
+ kubectl_mcp_tool/__init__.py,sha256=kA6EKH7Jawvme4pUaNXL97G-_8OkUptR_PK5Htjy6W0,580
3
3
  kubectl_mcp_tool/__main__.py,sha256=CE6cTD6PA71Ap0i5_gE17Pb9FcedOJmtGRNzZ5-TFSc,1490
4
4
  kubectl_mcp_tool/crd_detector.py,sha256=xLZSyg3iIe_MjSMI3mESe2VK8wGJzu40rJ7F72fULBs,7139
5
5
  kubectl_mcp_tool/diagnostics.py,sha256=uwolSoHadRkB-J8PAsabbexfj6sTNCIIRRrABBRXoTU,11776
6
- kubectl_mcp_tool/k8s_config.py,sha256=NK8y4Un03Y-sOPIHM-hrR_PA-8BtdoXzyRZWuhMJIok,17909
7
- kubectl_mcp_tool/mcp_server.py,sha256=bCNR_a_A9p3nctBmFCqnNghepEpYot_o1WcLIjYseYs,32691
8
- kubectl_mcp_tool/providers.py,sha256=_LMpVbCWgFrsRO_u4krSw7wFNYapzp29WtiEh-sbBeY,11521
9
- kubectl_mcp_tool/safety.py,sha256=7bx7tUH_hT4Y_eTnpH5knnHTsvnIZvI_0oo9MSBe_z8,5036
6
+ kubectl_mcp_tool/k8s_config.py,sha256=XePmWOS_cfFFaxG5HjmqGQrtEr8MWOVrYAB4eAipaPg,18950
7
+ kubectl_mcp_tool/mcp_server.py,sha256=EdKWJWdgmQgbnKYyb_1W1F54aiKsDciViRWxVb4mKsY,32056
8
+ kubectl_mcp_tool/providers.py,sha256=gaU9mB0p6ygqbxC--e4E60TwfC8FzW_sVWvc8OWK07I,12805
9
+ kubectl_mcp_tool/safety.py,sha256=l9wb3hhdlr-Yifai_0fDutZvC4sW2HQtrgfuaJWjFjc,3472
10
10
  kubectl_mcp_tool/auth/__init__.py,sha256=ot8ivZZkDtV8Rg0y1UYruwobKCPyxX1svqh35wWxKvY,347
11
11
  kubectl_mcp_tool/auth/config.py,sha256=wi3wuJNMyDqMeluDHL0MaJyedIFv5CFVxiUaEVaTvzk,2267
12
12
  kubectl_mcp_tool/auth/scopes.py,sha256=KPmuGO0SrTkjzlElWFOV29ie9apTdMklOCkiA-965lI,6147
@@ -30,52 +30,53 @@ kubectl_mcp_tool/prompts/prompts.py,sha256=ogDhU6FVsYIlig_QuIw-TblxR9GQJ3Xp8mJHL
30
30
  kubectl_mcp_tool/resources/__init__.py,sha256=ERkn0ErlaGi9-dybv4wrAaT8WretvNp6K002h7Agjno,83
31
31
  kubectl_mcp_tool/resources/resources.py,sha256=kvK4OM3Ox5cFvWDqJBTXOfBgnRYdoqdvvjsdCg0PJfY,12713
32
32
  kubectl_mcp_tool/tools/__init__.py,sha256=ktvGmS_bUqYhv3J65dtQrrj2B8K7r9WTnS2_dbAGp_U,1955
33
+ kubectl_mcp_tool/tools/_cli_utils.py,sha256=lCJzvK1HOsdAfO_1yMhwjLfK8k78OCuQ3kDgngMYOw8,2204
33
34
  kubectl_mcp_tool/tools/backup.py,sha256=M1dvyc-fmCbGFp3o89pGNv8drtgPxds0sBk0OiN0TJA,29148
34
35
  kubectl_mcp_tool/tools/browser.py,sha256=OvXUHzEuciRQInnwLLC16iObOOdniu09b9RT81y7sdc,25338
35
36
  kubectl_mcp_tool/tools/capi.py,sha256=qcNStWl7KHqi4Jorf9xThfOjYRhp7d3ZhUGif_YWrV8,24293
36
37
  kubectl_mcp_tool/tools/certs.py,sha256=82FVevhLSaMTGDyu3rxJNhmiL_3KzBqFwys186b_2IM,25035
37
38
  kubectl_mcp_tool/tools/cilium.py,sha256=ijO-HWcS3R5FVzg1RrE-pKhrEL0SdG0CNyrQ6g0S8eM,18654
38
- kubectl_mcp_tool/tools/cluster.py,sha256=K8MGinOmzk0850dVB332693SadseAzgYf3jV-IKxmNM,54419
39
- kubectl_mcp_tool/tools/core.py,sha256=67Cx1z4DqV0Qd26yXkF1eBXbUIlBB3Qj2W8rOInvb54,17801
40
- kubectl_mcp_tool/tools/cost.py,sha256=fcFlIv-bsMdKESw-l-T6ECsUplDukNHaqM76JCYVHvQ,29629
41
- kubectl_mcp_tool/tools/deployments.py,sha256=e138KV30DQhLCnvXntrofx1XdLH0dC0_mlODbzkluKk,18329
42
- kubectl_mcp_tool/tools/diagnostics.py,sha256=LGpW-cx2ui40IIq4pZPT2O8nVzKCpOEVnnQ1OGUSVt0,7864
39
+ kubectl_mcp_tool/tools/cluster.py,sha256=wT_fePVC7QNgwO1IOT2v6uGbkzJQ3J9kG3ikCHIY0ik,54287
40
+ kubectl_mcp_tool/tools/core.py,sha256=Viyn4R2Av-jchcSZA6hIrV_YeOjj3Ysts4hftZvMXoc,17650
41
+ kubectl_mcp_tool/tools/cost.py,sha256=CTS6R0KPUq9_CWDxEU5by6l38aBD6zKU2_URXrduWp8,29546
42
+ kubectl_mcp_tool/tools/deployments.py,sha256=dCsIaGt0ssk60G60iInZz9ssGAeuiZL4LuBOSWD864o,18178
43
+ kubectl_mcp_tool/tools/diagnostics.py,sha256=i8d4ydbyIlE4YS3MegpDpDuRUiQb6q9XxLQK-dcmxFo,7709
43
44
  kubectl_mcp_tool/tools/gitops.py,sha256=aILtjkYI6QovPfsj5sdJDnkxvmzw5Ojzf8MDcpe2ns0,19168
44
- kubectl_mcp_tool/tools/helm.py,sha256=v_d1aH79VCKJmFi-kXG1LwzZRw6OFToyVC7D9tEp99A,61658
45
+ kubectl_mcp_tool/tools/helm.py,sha256=mS3RsUQpjQ_euRQA7Os0l3XS6EyFakyyg9qaLXLMIok,60732
45
46
  kubectl_mcp_tool/tools/keda.py,sha256=nxoGkLGWrBQmNpN_gFIk4AUxInwOak6HutgGz1nyWtc,14811
46
47
  kubectl_mcp_tool/tools/kiali.py,sha256=tPGH-wpXqhwmW7XfSQ5KldUD0ihe2yuy8NBvmD8Xn74,20531
47
- kubectl_mcp_tool/tools/kind.py,sha256=fWyWGbiI60t6vUPGeAqIXwtkAGjquwv8jliCZBu6JVA,52317
48
+ kubectl_mcp_tool/tools/kind.py,sha256=KGT7FS_SfPyS752Tc6Jb0NLQXN9Jz6tj7uAPHJ__tcI,52187
48
49
  kubectl_mcp_tool/tools/kubevirt.py,sha256=1M4xdihTQabH4O7NVoceQ3DQ11si_gbX1-bMDK5VOkw,25549
49
- kubectl_mcp_tool/tools/networking.py,sha256=5I-2SHp2kK9OyGJJ9KUU0hetlYoREIfin1J6CJA6HVQ,14309
50
- kubectl_mcp_tool/tools/operations.py,sha256=c7j1quXwRDoB9pMkmnLZ5Mk4QoCAuEzWVXJl_hmg2tc,24862
51
- kubectl_mcp_tool/tools/pods.py,sha256=isl-uhuSGrzR78U8d2ePqt4D_4rO_qSTZo5-5N_TFYQ,31252
50
+ kubectl_mcp_tool/tools/networking.py,sha256=5Fj3ULC-psub65_gziWVbyfIPeKIsPeKqTMUbOziJ54,14482
51
+ kubectl_mcp_tool/tools/operations.py,sha256=UoSAAFwzVTBOPHk533mVtjf1VMjbgrCHizK26YHUTI8,26163
52
+ kubectl_mcp_tool/tools/pods.py,sha256=MT7tghx03ru7aQyt6bWcNJ4tC7Oq1TqoxNaYK85XxPg,31300
52
53
  kubectl_mcp_tool/tools/policy.py,sha256=Iz5YPpHUOLFySknFY-sGxw699s9HHtHp18vEzw72X-8,20475
53
54
  kubectl_mcp_tool/tools/rollouts.py,sha256=OtFAe2KIoN6fY4gv-f7WoiYAcQOGQaSJB-y6kI8UQvM,24568
54
55
  kubectl_mcp_tool/tools/security.py,sha256=d7WLj-JZwr-0opzkRNWyPdMykvBjdsDCBSZHovDIPLo,13971
55
56
  kubectl_mcp_tool/tools/storage.py,sha256=ILI3X5GYXASCETqg3A7X33B7PjwD5UsxL-iDG7yTi_8,5676
56
57
  kubectl_mcp_tool/tools/ui.py,sha256=HHi0KM2JTvZFpQjo5V6XkJf02hxFoasec7uyNY5wuuo,39702
57
58
  kubectl_mcp_tool/tools/utils.py,sha256=HvJoW6xa8duPf9omFdxWPnVGF5fSPnz6xfoCuwwCb_4,1416
58
- kubectl_mcp_tool/tools/vind.py,sha256=Sw_ubZRsY-hk7Rui8OlwhylUs2epmCa8j74Q2CdEJaA,21721
59
+ kubectl_mcp_tool/tools/vind.py,sha256=bAZM_SD-lKMjGafCeFEwXNXvZmqZ9Gyri2NqHg4t5sw,21573
59
60
  kubectl_mcp_tool/utils/__init__.py,sha256=CHBCpaXwt994DlqyRFkkRky2TK8OmmDl0Gyc28369gI,348
60
61
  kubectl_mcp_tool/utils/helpers.py,sha256=W--wiVSKKqmjpxxdLT0J6rmhOQcp1OFk9jLrtQUVpGw,2444
61
62
  tests/__init__.py,sha256=qZPXYXv3whkkWhi61Ngzj09GHnIFlVSZrajE0XRk55o,290
62
63
  tests/conftest.py,sha256=6054YlpuGleV3Wg8BnVj4lnKWhGk-Eqc9JYTXxOmsXs,10782
63
64
  tests/test_auth.py,sha256=PoESfWiN92wSGUdVwLL3Z1AP6C1zUsVmgTI7Q8ZdlxM,11074
64
- tests/test_browser.py,sha256=6rB6Atqq3b2-xWnKzFdCVFHbF2Q3nt2Dy8Rp4ZwIgNk,20621
65
+ tests/test_browser.py,sha256=g-Hv60wiwZ8kgNC0WJ7VJ1D3KFAnWTn3RijDhzW0vnQ,20539
65
66
  tests/test_cli.py,sha256=G-f9uByWooe-yUOH0ahBdoft4uz8TC0R0OzDs1Lom-4,10014
66
67
  tests/test_config.py,sha256=9_A6_ntjvX556GvFvHGHuel1ERGo-WrbnFJX8OniUvg,12568
67
- tests/test_ecosystem.py,sha256=SkxDWAASELIxzaYWu0d-Rw1HaKSSlyMMenwwF2HWvSA,13227
68
+ tests/test_ecosystem.py,sha256=gKq9xPcoX2R49oXfHUEbIFK64vXNniFZpmTzzk3iTrk,13113
68
69
  tests/test_kind.py,sha256=1vUNXz-cNcjQcaomytH3t_CG2wA3_H6uuqrl3SD-Rrs,45961
69
- tests/test_mcp_integration.py,sha256=9ePvu1MJ0mTnMRFHthDwl3D2jyVPgQ1beUbchpT8Y3M,8782
70
+ tests/test_mcp_integration.py,sha256=arREaEJ7VQ_4cQ_V8-fSuSO4LBBDVvzOPrEiIDiWC90,8063
70
71
  tests/test_observability.py,sha256=m0m-uIQrE9OJ3FFls_rVZJst3NeAFKRtNUZohFpIZE4,17829
71
72
  tests/test_prompts.py,sha256=VzZAXr5iFO-hG8kRwRnG4wom2LlqOmZ-SeW4d3HQ7hc,42888
72
73
  tests/test_resources.py,sha256=Z0Ex8WdRz-B3VZa1s0eAaDDGbhy7dRdqy1uFVOe2Qbo,12689
73
- tests/test_safety.py,sha256=5IOBQbcH2r8tANQD7ymsR6pBc6O50IspcFsuwrnwh1M,8292
74
- tests/test_server.py,sha256=lLvgbqutnivSgQMNrki0O48whBQt0UXjdwT047nf0nw,14415
75
- tests/test_tools.py,sha256=qZsyaXV-US1O05hNsIDNXjZmeZlg-kIPStM1r8hxySo,36711
74
+ tests/test_safety.py,sha256=SKmfHJpEoQmn6K70KIJS1T-Z9k0jmsdyPKaS1UYFImE,3638
75
+ tests/test_server.py,sha256=_rdUblJG0DmZbkEfBgU1H4vZHExAlTj-v1SIbekSVjk,14347
76
+ tests/test_tools.py,sha256=CrG74XmHCvf6BEc2VxaS8nE74aPjAWRkr-f2lqYFUDw,37271
76
77
  tests/test_vind.py,sha256=L3q6yOXlhbVXwjuH-5YRRnauEO8q4LBOUBB_RGqSTfk,19003
77
- kubectl_mcp_server-1.22.0.dist-info/METADATA,sha256=f4Nx_FchIDzCJSfiXAz4QPJ4XH6JqsQNPNcLun06k3c,43396
78
- kubectl_mcp_server-1.22.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
79
- kubectl_mcp_server-1.22.0.dist-info/entry_points.txt,sha256=zeOxQGaNC4r58deEmqsLCU5hfMjF0VqFUt9P5wWsKEM,109
80
- kubectl_mcp_server-1.22.0.dist-info/top_level.txt,sha256=o5IpfOGG-lqU8rVWJeK9aYC0r4f6qEX09QiBhZlYbkQ,23
81
- kubectl_mcp_server-1.22.0.dist-info/RECORD,,
78
+ kubectl_mcp_server-1.23.1.dist-info/METADATA,sha256=7Wf_HEIiKcqd6QpekGzexH5jQqD-CqJHMd3wku7J2bg,43396
79
+ kubectl_mcp_server-1.23.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
80
+ kubectl_mcp_server-1.23.1.dist-info/entry_points.txt,sha256=zeOxQGaNC4r58deEmqsLCU5hfMjF0VqFUt9P5wWsKEM,109
81
+ kubectl_mcp_server-1.23.1.dist-info/top_level.txt,sha256=o5IpfOGG-lqU8rVWJeK9aYC0r4f6qEX09QiBhZlYbkQ,23
82
+ kubectl_mcp_server-1.23.1.dist-info/RECORD,,
@@ -7,7 +7,7 @@ with Kubernetes clusters through natural language commands.
7
7
  For more information, see: https://github.com/rohitg00/kubectl-mcp-server
8
8
  """
9
9
 
10
- __version__ = "1.22.0"
10
+ __version__ = "1.23.1"
11
11
 
12
12
  from .mcp_server import MCPServer
13
13
  from .diagnostics import run_diagnostics, check_kubectl_installation, check_cluster_connection
@@ -203,6 +203,7 @@ try:
203
203
  KubernetesProvider,
204
204
  ProviderConfig,
205
205
  ProviderType,
206
+ ProviderError,
206
207
  UnknownContextError,
207
208
  get_provider,
208
209
  get_context_names,
@@ -262,9 +263,21 @@ def load_kubernetes_config(context: str = ""):
262
263
 
263
264
 
264
265
  def _patched_load_kube_config(*args, **kwargs):
265
- """Patched version of load_kube_config that tries in-cluster first."""
266
+ """Patched version of load_kube_config that tries in-cluster first.
267
+
268
+ When called with explicit arguments (config_file, context, client_configuration),
269
+ always pass through to the original function. Only try in-cluster as a
270
+ fallback for bare calls during initial module loading.
271
+ """
266
272
  global _config_loaded, _original_load_kube_config
267
273
 
274
+ has_explicit_args = bool(args) or bool(kwargs)
275
+
276
+ if has_explicit_args and _original_load_kube_config:
277
+ _original_load_kube_config(*args, **kwargs)
278
+ _config_loaded = True
279
+ return
280
+
268
281
  if _config_loaded:
269
282
  return
270
283
 
@@ -326,7 +339,7 @@ def _load_config_for_context(context: str = "") -> Any:
326
339
  try:
327
340
  provider = get_provider()
328
341
  return provider.get_api_client(context)
329
- except UnknownContextError:
342
+ except (UnknownContextError, ProviderError):
330
343
  raise
331
344
  except Exception as e:
332
345
  logger.warning(f"Provider failed, falling back to basic config: {e}")
@@ -338,11 +351,20 @@ def _load_config_for_context(context: str = "") -> Any:
338
351
  config.load_incluster_config()
339
352
  return client.ApiClient()
340
353
  except ConfigException:
341
- pass
354
+ logger.debug("In-cluster config not available in fallback path")
342
355
 
343
356
  kubeconfig_path = os.environ.get('KUBECONFIG', '~/.kube/config')
344
357
  kubeconfig_path = os.path.expanduser(kubeconfig_path)
345
358
 
359
+ if not os.path.exists(kubeconfig_path):
360
+ raise RuntimeError(
361
+ f"No Kubernetes configuration found. "
362
+ f"In-cluster config not available and kubeconfig not found at '{kubeconfig_path}'. "
363
+ f"If running in a Kubernetes pod, ensure the ServiceAccount token is mounted "
364
+ f"and KUBERNETES_SERVICE_HOST/KUBERNETES_SERVICE_PORT env vars are set. "
365
+ f"Set MCP_K8S_PROVIDER=in-cluster for in-cluster deployments."
366
+ )
367
+
346
368
  api_config = client.Configuration()
347
369
 
348
370
  if context:
@@ -543,7 +565,8 @@ _BASE_EXPORTS = [
543
565
  if _HAS_PROVIDER:
544
566
  __all__ = _BASE_EXPORTS + [
545
567
  "KubernetesProvider", "ProviderConfig", "ProviderType",
546
- "UnknownContextError", "get_provider", "validate_context",
568
+ "ProviderError", "UnknownContextError",
569
+ "get_provider", "validate_context",
547
570
  ]
548
571
  else:
549
572
  __all__ = _BASE_EXPORTS
@@ -23,15 +23,10 @@ import logging
23
23
  import asyncio
24
24
  import os
25
25
  import platform
26
- import signal
27
- from pathlib import Path
28
26
  from typing import List, Optional, Any, Dict
29
27
 
30
- # Import k8s_config early to patch kubernetes config for in-cluster support
31
- # This must be done before any tools are imported
32
28
  import kubectl_mcp_tool.k8s_config # noqa: F401
33
29
 
34
- # Import safety mode for operation control
35
30
  from kubectl_mcp_tool.safety import (
36
31
  SafetyMode,
37
32
  set_safety_mode,
@@ -39,7 +34,6 @@ from kubectl_mcp_tool.safety import (
39
34
  get_mode_info,
40
35
  )
41
36
 
42
- # Import observability for metrics and tracing
43
37
  from kubectl_mcp_tool.observability import (
44
38
  get_stats_collector,
45
39
  get_metrics,
@@ -52,7 +46,6 @@ from kubectl_mcp_tool.observability import (
52
46
  record_tool_error_metric,
53
47
  )
54
48
 
55
- # Import config loader
56
49
  from kubectl_mcp_tool.config import (
57
50
  load_config,
58
51
  get_config,
@@ -60,12 +53,13 @@ from kubectl_mcp_tool.config import (
60
53
  setup_sighup_handler,
61
54
  )
62
55
 
63
- # Import custom prompts
64
56
  from kubectl_mcp_tool.prompts import (
65
57
  load_prompts_from_config,
66
58
  get_builtin_prompts,
67
59
  )
68
60
 
61
+ from kubectl_mcp_tool import __version__
62
+
69
63
  from kubectl_mcp_tool.tools import (
70
64
  register_helm_tools,
71
65
  register_pod_tools,
@@ -134,23 +128,13 @@ for handler in logging.root.handlers[:]:
134
128
  if isinstance(handler, logging.StreamHandler) and handler.stream == sys.stdout:
135
129
  logging.root.removeHandler(handler)
136
130
 
137
- # FastMCP 3 from gofastmcp.com (standalone package)
138
- # To revert to official SDK: from mcp.server.fastmcp import FastMCP
139
131
  try:
140
132
  from fastmcp import FastMCP
141
- except ImportError:
142
- logger.error("FastMCP not found. Installing...")
143
- import subprocess
144
- try:
145
- subprocess.check_call(
146
- [sys.executable, "-m", "pip", "install", "fastmcp>=3.0.0b1"],
147
- stdout=subprocess.DEVNULL,
148
- stderr=subprocess.DEVNULL
149
- )
150
- from fastmcp import FastMCP
151
- except Exception as e:
152
- logger.error(f"Failed to install FastMCP: {e}")
153
- raise
133
+ except ImportError as err:
134
+ raise ImportError(
135
+ "FastMCP is required but not installed. "
136
+ "Install with: pip install 'fastmcp>=3.0.0b1'"
137
+ ) from err
154
138
 
155
139
 
156
140
  class MCPServer:
@@ -622,7 +606,7 @@ class MCPServer:
622
606
  },
623
607
  "serverInfo": {
624
608
  "name": self.name,
625
- "version": "1.2.0"
609
+ "version": __version__
626
610
  }
627
611
  }
628
612
  elif method == "tools/list":
@@ -3,7 +3,6 @@ import logging
3
3
  from enum import Enum
4
4
  from typing import Any, Dict, List, Optional, Tuple
5
5
  from dataclasses import dataclass, field
6
- from functools import lru_cache
7
6
 
8
7
  logger = logging.getLogger("mcp-server")
9
8
 
@@ -150,8 +149,37 @@ class KubernetesProvider:
150
149
  raise ProviderError(f"Failed to load context '{self.config.context}': {e}")
151
150
 
152
151
  def _initialize_kubeconfig(self):
153
- """Initialize for multi-cluster kubeconfig provider."""
152
+ """Initialize for multi-cluster kubeconfig provider.
153
+
154
+ Falls back to in-cluster config if kubeconfig file is not found.
155
+ """
154
156
  from kubernetes import config
157
+ from kubernetes.config.config_exception import ConfigException
158
+
159
+ kubeconfig_exists = os.path.exists(self.config.kubeconfig_path)
160
+
161
+ if not kubeconfig_exists:
162
+ try:
163
+ config.load_incluster_config()
164
+ self._in_cluster = True
165
+ self._active_context = "in-cluster"
166
+ self._contexts_cache = []
167
+ logger.info(
168
+ f"Kubeconfig not found at {self.config.kubeconfig_path}, "
169
+ f"using in-cluster config"
170
+ )
171
+ return
172
+ except ConfigException:
173
+ raise ProviderError(
174
+ f"No Kubernetes configuration found. "
175
+ f"Kubeconfig not found at '{self.config.kubeconfig_path}' and "
176
+ f"in-cluster config is not available. "
177
+ f"Ensure a valid kubeconfig exists, or if running inside a "
178
+ f"Kubernetes pod, verify the ServiceAccount token is mounted "
179
+ f"and KUBERNETES_SERVICE_HOST/KUBERNETES_SERVICE_PORT env vars "
180
+ f"are set. For in-cluster deployments, set "
181
+ f"MCP_K8S_PROVIDER=in-cluster."
182
+ )
155
183
 
156
184
  try:
157
185
  contexts, active = config.list_kube_config_contexts(
@@ -5,8 +5,7 @@ Provides read-only and disable-destructive modes to prevent accidental cluster m
5
5
  """
6
6
 
7
7
  from enum import Enum
8
- from functools import wraps
9
- from typing import Any, Callable, Dict, Set
8
+ from typing import Any, Dict, Set
10
9
  import logging
11
10
 
12
11
  logger = logging.getLogger("mcp-server")
@@ -86,57 +85,6 @@ def set_safety_mode(mode: SafetyMode) -> None:
86
85
  logger.info(f"Safety mode set to: {mode.value}")
87
86
 
88
87
 
89
- def is_operation_allowed(operation_name: str) -> tuple[bool, str]:
90
- """
91
- Check if an operation is allowed under the current safety mode.
92
-
93
- Returns:
94
- Tuple of (allowed: bool, reason: str)
95
- """
96
- mode = get_safety_mode()
97
-
98
- if mode == SafetyMode.NORMAL:
99
- return True, ""
100
-
101
- if mode == SafetyMode.READ_ONLY:
102
- if operation_name in WRITE_OPERATIONS or operation_name in DESTRUCTIVE_OPERATIONS:
103
- return False, f"Operation '{operation_name}' blocked: read-only mode is enabled"
104
-
105
- if mode == SafetyMode.DISABLE_DESTRUCTIVE:
106
- if operation_name in DESTRUCTIVE_OPERATIONS:
107
- return False, f"Operation '{operation_name}' blocked: destructive operations are disabled"
108
-
109
- return True, ""
110
-
111
-
112
- def check_safety_mode(func: Callable) -> Callable:
113
- """
114
- Decorator to check safety mode before executing a tool function.
115
-
116
- Usage:
117
- @check_safety_mode
118
- def delete_pod(...):
119
- ...
120
- """
121
- @wraps(func)
122
- def wrapper(*args, **kwargs) -> Dict[str, Any]:
123
- operation_name = func.__name__
124
- allowed, reason = is_operation_allowed(operation_name)
125
-
126
- if not allowed:
127
- logger.warning(f"Blocked operation: {operation_name} (mode: {get_safety_mode().value})")
128
- return {
129
- "success": False,
130
- "error": reason,
131
- "blocked_by": get_safety_mode().value,
132
- "operation": operation_name
133
- }
134
-
135
- return func(*args, **kwargs)
136
-
137
- return wrapper
138
-
139
-
140
88
  def get_mode_info() -> Dict[str, Any]:
141
89
  """Get information about the current safety mode."""
142
90
  mode = get_safety_mode()
@@ -0,0 +1,75 @@
1
+ import subprocess
2
+ from functools import lru_cache
3
+ from typing import Any, Dict, List, Optional
4
+
5
+
6
+ @lru_cache(maxsize=16)
7
+ def cli_available(binary: str) -> bool:
8
+ """Check if a CLI binary is available (cached)."""
9
+ try:
10
+ result = subprocess.run(
11
+ [binary, "version"],
12
+ capture_output=True,
13
+ timeout=10
14
+ )
15
+ return result.returncode == 0
16
+ except Exception:
17
+ return False
18
+
19
+
20
+ @lru_cache(maxsize=16)
21
+ def get_cli_version(binary: str) -> Optional[str]:
22
+ """Get CLI binary version string (cached)."""
23
+ try:
24
+ result = subprocess.run(
25
+ [binary, "version"],
26
+ capture_output=True,
27
+ text=True,
28
+ timeout=10
29
+ )
30
+ if result.returncode == 0:
31
+ return result.stdout.strip()
32
+ return None
33
+ except Exception:
34
+ return None
35
+
36
+
37
+ def run_cli(
38
+ binary: str,
39
+ args: List[str],
40
+ timeout: int = 300,
41
+ capture_output: bool = True
42
+ ) -> Dict[str, Any]:
43
+ """Run a CLI command and return a standardized result dict.
44
+
45
+ Args:
46
+ binary: CLI binary name (e.g., "kind", "vcluster", "helm")
47
+ args: Command arguments (without the binary prefix)
48
+ timeout: Command timeout in seconds
49
+ capture_output: Whether to capture stdout/stderr
50
+
51
+ Returns:
52
+ Dict with success status and output/error
53
+ """
54
+ cmd = [binary, *args]
55
+
56
+ try:
57
+ result = subprocess.run(
58
+ cmd,
59
+ capture_output=capture_output,
60
+ text=True,
61
+ timeout=timeout
62
+ )
63
+ if result.returncode == 0:
64
+ output = result.stdout.strip() if capture_output else ""
65
+ return {"success": True, "output": output}
66
+ return {
67
+ "success": False,
68
+ "error": result.stderr.strip() if capture_output else f"Command failed with exit code {result.returncode}"
69
+ }
70
+ except subprocess.TimeoutExpired:
71
+ return {"success": False, "error": f"Command timed out after {timeout} seconds"}
72
+ except FileNotFoundError:
73
+ return {"success": False, "error": f"{binary} CLI not available"}
74
+ except Exception as e:
75
+ return {"success": False, "error": str(e)}
@@ -23,18 +23,12 @@ from kubectl_mcp_tool.k8s_config import (
23
23
  disable_kubeconfig_watch,
24
24
  is_stateless_mode,
25
25
  set_stateless_mode,
26
+ _get_kubectl_context_args,
26
27
  )
27
28
 
28
29
  logger = logging.getLogger("mcp-server")
29
30
 
30
31
 
31
- def _get_kubectl_context_args(context: str = "") -> List[str]:
32
- """Get kubectl context arguments."""
33
- if context:
34
- return ["--context", context]
35
- return []
36
-
37
-
38
32
  # DNS-1123 subdomain regex for node name validation
39
33
  _DNS_1123_PATTERN = re.compile(r'^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$')
40
34
 
@@ -60,7 +54,7 @@ def _validate_node_name(name: str) -> tuple:
60
54
  return True, None
61
55
 
62
56
 
63
- def register_cluster_tools(server, non_destructive: bool):
57
+ def register_cluster_tools(server: "FastMCP", non_destructive: bool):
64
58
  """Register cluster and context management tools."""
65
59
 
66
60
  @server.tool(
@@ -7,19 +7,13 @@ from mcp.types import ToolAnnotations
7
7
  from ..k8s_config import (
8
8
  get_k8s_client,
9
9
  get_apiextensions_client,
10
+ _get_kubectl_context_args,
10
11
  )
11
12
 
12
13
  logger = logging.getLogger("mcp-server")
13
14
 
14
15
 
15
- def _get_kubectl_context_args(context: str) -> List[str]:
16
- """Get kubectl context arguments if context is specified."""
17
- if context:
18
- return ["--context", context]
19
- return []
20
-
21
-
22
- def register_core_tools(server, non_destructive: bool):
16
+ def register_core_tools(server: "FastMCP", non_destructive: bool):
23
17
  """Register core Kubernetes resource tools."""
24
18
 
25
19
  @server.tool(
@@ -6,18 +6,11 @@ from typing import Any, Dict, List, Optional
6
6
 
7
7
  from mcp.types import ToolAnnotations
8
8
 
9
- from ..k8s_config import get_k8s_client, get_apps_client
9
+ from ..k8s_config import get_k8s_client, get_apps_client, _get_kubectl_context_args
10
10
 
11
11
  logger = logging.getLogger("mcp-server")
12
12
 
13
13
 
14
- def _get_kubectl_context_args(context: str) -> List[str]:
15
- """Get kubectl context arguments if context is specified."""
16
- if context:
17
- return ["--context", context]
18
- return []
19
-
20
-
21
14
  def _parse_cpu(cpu_str: str) -> int:
22
15
  """Parse CPU string to millicores."""
23
16
  try:
@@ -28,7 +21,7 @@ def _parse_cpu(cpu_str: str) -> int:
28
21
  return int(cpu_str[:-1]) // 1000000
29
22
  else:
30
23
  return int(float(cpu_str) * 1000)
31
- except:
24
+ except (ValueError, TypeError):
32
25
  return 0
33
26
 
34
27
 
@@ -50,7 +43,7 @@ def _parse_memory(mem_str: str) -> int:
50
43
  return int(mem_str[:-1]) * 1000000000
51
44
  else:
52
45
  return int(mem_str)
53
- except:
46
+ except (ValueError, TypeError):
54
47
  return 0
55
48
 
56
49
 
@@ -61,11 +54,11 @@ def _calculate_available(hard: str, used: str) -> str:
61
54
  used_num = int(re.sub(r'[^\d]', '', str(used)) or 0)
62
55
  suffix = re.sub(r'[\d]', '', str(hard))
63
56
  return f"{max(0, hard_num - used_num)}{suffix}"
64
- except:
57
+ except (ValueError, TypeError):
65
58
  return "N/A"
66
59
 
67
60
 
68
- def register_cost_tools(server, non_destructive: bool):
61
+ def register_cost_tools(server: "FastMCP", non_destructive: bool):
69
62
  """Register cost and resource optimization tools."""
70
63
 
71
64
  @server.tool(
@@ -10,19 +10,13 @@ from ..k8s_config import (
10
10
  get_autoscaling_client,
11
11
  get_policy_client,
12
12
  _load_config_for_context,
13
+ _get_kubectl_context_args,
13
14
  )
14
15
 
15
16
  logger = logging.getLogger("mcp-server")
16
17
 
17
18
 
18
- def _get_kubectl_context_args(context: str) -> List[str]:
19
- """Get kubectl context arguments if context is specified."""
20
- if context:
21
- return ["--context", context]
22
- return []
23
-
24
-
25
- def register_deployment_tools(server, non_destructive: bool):
19
+ def register_deployment_tools(server: "FastMCP", non_destructive: bool):
26
20
  """Register deployment and workload management tools."""
27
21
 
28
22
  @server.tool(
@@ -4,19 +4,12 @@ from typing import Any, Dict, List, Optional
4
4
 
5
5
  from mcp.types import ToolAnnotations
6
6
 
7
- from ..k8s_config import get_k8s_client, get_apps_client
7
+ from ..k8s_config import get_k8s_client, get_apps_client, _get_kubectl_context_args
8
8
 
9
9
  logger = logging.getLogger("mcp-server")
10
10
 
11
11
 
12
- def _get_kubectl_context_args(context: str) -> List[str]:
13
- """Get kubectl context arguments if context is specified."""
14
- if context:
15
- return ["--context", context]
16
- return []
17
-
18
-
19
- def register_diagnostics_tools(server, non_destructive: bool):
12
+ def register_diagnostics_tools(server: "FastMCP", non_destructive: bool):
20
13
  """Register diagnostic and troubleshooting tools.
21
14
 
22
15
  Note: Pod-specific diagnostic tools (diagnose_pod_crash, detect_pending_pods,