kubectl-mcp-server 1.12.0__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.
- kubectl_mcp_server-1.12.0.dist-info/METADATA +711 -0
- kubectl_mcp_server-1.12.0.dist-info/RECORD +45 -0
- kubectl_mcp_server-1.12.0.dist-info/WHEEL +5 -0
- kubectl_mcp_server-1.12.0.dist-info/entry_points.txt +3 -0
- kubectl_mcp_server-1.12.0.dist-info/licenses/LICENSE +21 -0
- kubectl_mcp_server-1.12.0.dist-info/top_level.txt +2 -0
- kubectl_mcp_tool/__init__.py +21 -0
- kubectl_mcp_tool/__main__.py +46 -0
- kubectl_mcp_tool/auth/__init__.py +13 -0
- kubectl_mcp_tool/auth/config.py +71 -0
- kubectl_mcp_tool/auth/scopes.py +148 -0
- kubectl_mcp_tool/auth/verifier.py +82 -0
- kubectl_mcp_tool/cli/__init__.py +9 -0
- kubectl_mcp_tool/cli/__main__.py +10 -0
- kubectl_mcp_tool/cli/cli.py +111 -0
- kubectl_mcp_tool/diagnostics.py +355 -0
- kubectl_mcp_tool/k8s_config.py +289 -0
- kubectl_mcp_tool/mcp_server.py +530 -0
- kubectl_mcp_tool/prompts/__init__.py +5 -0
- kubectl_mcp_tool/prompts/prompts.py +823 -0
- kubectl_mcp_tool/resources/__init__.py +5 -0
- kubectl_mcp_tool/resources/resources.py +305 -0
- kubectl_mcp_tool/tools/__init__.py +28 -0
- kubectl_mcp_tool/tools/browser.py +371 -0
- kubectl_mcp_tool/tools/cluster.py +315 -0
- kubectl_mcp_tool/tools/core.py +421 -0
- kubectl_mcp_tool/tools/cost.py +680 -0
- kubectl_mcp_tool/tools/deployments.py +381 -0
- kubectl_mcp_tool/tools/diagnostics.py +174 -0
- kubectl_mcp_tool/tools/helm.py +1561 -0
- kubectl_mcp_tool/tools/networking.py +296 -0
- kubectl_mcp_tool/tools/operations.py +501 -0
- kubectl_mcp_tool/tools/pods.py +582 -0
- kubectl_mcp_tool/tools/security.py +333 -0
- kubectl_mcp_tool/tools/storage.py +133 -0
- kubectl_mcp_tool/utils/__init__.py +17 -0
- kubectl_mcp_tool/utils/helpers.py +80 -0
- tests/__init__.py +9 -0
- tests/conftest.py +379 -0
- tests/test_auth.py +256 -0
- tests/test_browser.py +349 -0
- tests/test_prompts.py +536 -0
- tests/test_resources.py +343 -0
- tests/test_server.py +384 -0
- tests/test_tools.py +659 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
kubectl_mcp_server-1.12.0.dist-info/licenses/LICENSE,sha256=nH9Z0W0WNH2oQ4cPrBAU8ldDcHfeI6NUbkSGiazYWgQ,1070
|
|
2
|
+
kubectl_mcp_tool/__init__.py,sha256=Z_IyVF0iPum8NzMC_5yXS1P_1vwrW9yQUalEfCEjjzs,580
|
|
3
|
+
kubectl_mcp_tool/__main__.py,sha256=CE6cTD6PA71Ap0i5_gE17Pb9FcedOJmtGRNzZ5-TFSc,1490
|
|
4
|
+
kubectl_mcp_tool/diagnostics.py,sha256=uwolSoHadRkB-J8PAsabbexfj6sTNCIIRRrABBRXoTU,11776
|
|
5
|
+
kubectl_mcp_tool/k8s_config.py,sha256=StM6Bb1SAVbFgNg5wKRmb_9aQ1wxEHtriCTIjvEFc5U,7853
|
|
6
|
+
kubectl_mcp_tool/mcp_server.py,sha256=OUrW04BRVpmsr1-8NDTjC1vaJ0VQCbjkcXkavy9lzVs,20927
|
|
7
|
+
kubectl_mcp_tool/auth/__init__.py,sha256=ot8ivZZkDtV8Rg0y1UYruwobKCPyxX1svqh35wWxKvY,347
|
|
8
|
+
kubectl_mcp_tool/auth/config.py,sha256=wi3wuJNMyDqMeluDHL0MaJyedIFv5CFVxiUaEVaTvzk,2267
|
|
9
|
+
kubectl_mcp_tool/auth/scopes.py,sha256=KPmuGO0SrTkjzlElWFOV29ie9apTdMklOCkiA-965lI,6147
|
|
10
|
+
kubectl_mcp_tool/auth/verifier.py,sha256=ChZM-UsZJgZc3LjSfw8VfSydkKqKBZ1s8es71LlB_A0,2431
|
|
11
|
+
kubectl_mcp_tool/cli/__init__.py,sha256=qGL_jH_5iv4cZsRvAbqAeKUN-ETh1K98_uJAPfgLvSU,147
|
|
12
|
+
kubectl_mcp_tool/cli/__main__.py,sha256=OxhHqW8YsTd565acTDlZknljPAw6FN9JW7pNtgWIcFE,196
|
|
13
|
+
kubectl_mcp_tool/cli/cli.py,sha256=f82U3B2I93lQUVJGKL33dPhxQ-5GBNjaJ26PwPj4O0Y,3769
|
|
14
|
+
kubectl_mcp_tool/prompts/__init__.py,sha256=BacBNfoVxow6aci8Zzcfam3m1oM7yYzM0IRT1L3uCOQ,77
|
|
15
|
+
kubectl_mcp_tool/prompts/prompts.py,sha256=ZfmTCio8NqOYYxF8VVo9f6VWGCS34J8tvmBfhNblr58,22942
|
|
16
|
+
kubectl_mcp_tool/resources/__init__.py,sha256=ERkn0ErlaGi9-dybv4wrAaT8WretvNp6K002h7Agjno,83
|
|
17
|
+
kubectl_mcp_tool/resources/resources.py,sha256=kvK4OM3Ox5cFvWDqJBTXOfBgnRYdoqdvvjsdCg0PJfY,12713
|
|
18
|
+
kubectl_mcp_tool/tools/__init__.py,sha256=mlvnz6P99jKwj6S8NJuX9UyaOLU1ARa5-CCkcc-3ju0,958
|
|
19
|
+
kubectl_mcp_tool/tools/browser.py,sha256=PDT7Uj7CCBQI9CFAaMSM_3i6v3NXIAf-mwd1y7iESa8,15193
|
|
20
|
+
kubectl_mcp_tool/tools/cluster.py,sha256=qQh6bET3-KQ8nkB2GnB8A7gohbJLrx6_K11TPYR6AII,11956
|
|
21
|
+
kubectl_mcp_tool/tools/core.py,sha256=zL0bGCxPGocH34ueZlSsBVsNp_tUL0C2gqyjlJljMXY,15009
|
|
22
|
+
kubectl_mcp_tool/tools/cost.py,sha256=OMiNopWvi5TTY-HX-3JSekelgReTEn88wAFddhui--M,27065
|
|
23
|
+
kubectl_mcp_tool/tools/deployments.py,sha256=TX1QHa22QU7rK7KG2-p5FB5KyWasZGfu-SK2CA08gWI,14990
|
|
24
|
+
kubectl_mcp_tool/tools/diagnostics.py,sha256=_xVcbmT3smQvOMwrZ0hlJ0FpgPbB4bdQgLkNcDJ8Vj4,6695
|
|
25
|
+
kubectl_mcp_tool/tools/helm.py,sha256=VgpiGPNbq4gQ4NQwefaRQPKGI7woN4QTHxtoz0_cNW8,57094
|
|
26
|
+
kubectl_mcp_tool/tools/networking.py,sha256=4_30hyQM-9t-X-yJWBrsdA9nPONZk3F6kyw3Y5-JKrA,11977
|
|
27
|
+
kubectl_mcp_tool/tools/operations.py,sha256=egYhlmqVZygddCkDJ2GDr89EjApps_PssWraOVfRmwQ,18800
|
|
28
|
+
kubectl_mcp_tool/tools/pods.py,sha256=7Ezk9khxoKsJYw8xEyBMjFdpLXmisGGrnKVVJ3rbhLE,23801
|
|
29
|
+
kubectl_mcp_tool/tools/security.py,sha256=Tu5SFPoeSgXkoc7WEpnnmdrMDJaB5znDoIVyyAy1Rho,12550
|
|
30
|
+
kubectl_mcp_tool/tools/storage.py,sha256=lXWp7131P39eTAsaMFrrJbWPuOGfDolVbD6npTBy1eQ,5124
|
|
31
|
+
kubectl_mcp_tool/utils/__init__.py,sha256=CHBCpaXwt994DlqyRFkkRky2TK8OmmDl0Gyc28369gI,348
|
|
32
|
+
kubectl_mcp_tool/utils/helpers.py,sha256=W--wiVSKKqmjpxxdLT0J6rmhOQcp1OFk9jLrtQUVpGw,2444
|
|
33
|
+
tests/__init__.py,sha256=qZPXYXv3whkkWhi61Ngzj09GHnIFlVSZrajE0XRk55o,290
|
|
34
|
+
tests/conftest.py,sha256=6054YlpuGleV3Wg8BnVj4lnKWhGk-Eqc9JYTXxOmsXs,10782
|
|
35
|
+
tests/test_auth.py,sha256=PoESfWiN92wSGUdVwLL3Z1AP6C1zUsVmgTI7Q8ZdlxM,11074
|
|
36
|
+
tests/test_browser.py,sha256=Oav6N4ivr_DW9038bVl31eAACt6vQORTr0uRgfeLs9A,14074
|
|
37
|
+
tests/test_prompts.py,sha256=3TcJUvSNxhTqfySW6DCrW9MwiMDumciLQRjjbucwqlA,17803
|
|
38
|
+
tests/test_resources.py,sha256=Z0Ex8WdRz-B3VZa1s0eAaDDGbhy7dRdqy1uFVOe2Qbo,12689
|
|
39
|
+
tests/test_server.py,sha256=lLvgbqutnivSgQMNrki0O48whBQt0UXjdwT047nf0nw,14415
|
|
40
|
+
tests/test_tools.py,sha256=MYJn8YlZIg2boX0OGTXyONRuA3M7vYZEATkwSVsya0c,30241
|
|
41
|
+
kubectl_mcp_server-1.12.0.dist-info/METADATA,sha256=Czr2PxQzTXEnV2Lc0d4d-wezu5rDBkSj_Fry1pajS_Q,23715
|
|
42
|
+
kubectl_mcp_server-1.12.0.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
|
|
43
|
+
kubectl_mcp_server-1.12.0.dist-info/entry_points.txt,sha256=zeOxQGaNC4r58deEmqsLCU5hfMjF0VqFUt9P5wWsKEM,109
|
|
44
|
+
kubectl_mcp_server-1.12.0.dist-info/top_level.txt,sha256=o5IpfOGG-lqU8rVWJeK9aYC0r4f6qEX09QiBhZlYbkQ,23
|
|
45
|
+
kubectl_mcp_server-1.12.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Rohit Ghumare
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Kubectl MCP Tool - A Model Context Protocol server for Kubernetes.
|
|
3
|
+
|
|
4
|
+
This package provides an MCP server that enables AI assistants to interact
|
|
5
|
+
with Kubernetes clusters through natural language commands.
|
|
6
|
+
|
|
7
|
+
For more information, see: https://github.com/rohitg00/kubectl-mcp-server
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
__version__ = "1.12.0"
|
|
11
|
+
|
|
12
|
+
from .mcp_server import MCPServer
|
|
13
|
+
from .diagnostics import run_diagnostics, check_kubectl_installation, check_cluster_connection
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"__version__",
|
|
17
|
+
"MCPServer",
|
|
18
|
+
"run_diagnostics",
|
|
19
|
+
"check_kubectl_installation",
|
|
20
|
+
"check_cluster_connection",
|
|
21
|
+
]
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Main entry point for the kubectl MCP tool."""
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
import argparse
|
|
6
|
+
import logging
|
|
7
|
+
import sys
|
|
8
|
+
import platform
|
|
9
|
+
|
|
10
|
+
if platform.system() == "Windows":
|
|
11
|
+
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
|
12
|
+
|
|
13
|
+
from .mcp_server import MCPServer
|
|
14
|
+
|
|
15
|
+
logging.basicConfig(
|
|
16
|
+
level=logging.INFO,
|
|
17
|
+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
18
|
+
handlers=[logging.StreamHandler(sys.stderr)]
|
|
19
|
+
)
|
|
20
|
+
logger = logging.getLogger("mcp-server")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def main():
|
|
24
|
+
"""Run the kubectl MCP server."""
|
|
25
|
+
parser = argparse.ArgumentParser(description="Kubectl MCP Server")
|
|
26
|
+
parser.add_argument("--transport", choices=["stdio", "sse", "http", "streamable-http"], default="stdio")
|
|
27
|
+
parser.add_argument("--port", type=int, default=8000)
|
|
28
|
+
parser.add_argument("--host", type=str, default="0.0.0.0")
|
|
29
|
+
parser.add_argument("--non-destructive", action="store_true", help="Block destructive operations")
|
|
30
|
+
args = parser.parse_args()
|
|
31
|
+
|
|
32
|
+
server = MCPServer(name="kubernetes", non_destructive=args.non_destructive)
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
if args.transport == "stdio":
|
|
36
|
+
asyncio.run(server.serve_stdio())
|
|
37
|
+
elif args.transport == "sse":
|
|
38
|
+
asyncio.run(server.serve_sse(host=args.host, port=args.port))
|
|
39
|
+
elif args.transport in ("http", "streamable-http"):
|
|
40
|
+
asyncio.run(server.serve_http(host=args.host, port=args.port))
|
|
41
|
+
except KeyboardInterrupt:
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
if __name__ == "__main__":
|
|
46
|
+
main()
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""MCP Authorization module implementing RFC 9728 OAuth 2.0 Protected Resource Metadata."""
|
|
2
|
+
|
|
3
|
+
from .config import AuthConfig, get_auth_config
|
|
4
|
+
from .scopes import MCPScopes, TOOL_SCOPES
|
|
5
|
+
from .verifier import create_auth_verifier
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"AuthConfig",
|
|
9
|
+
"get_auth_config",
|
|
10
|
+
"MCPScopes",
|
|
11
|
+
"TOOL_SCOPES",
|
|
12
|
+
"create_auth_verifier",
|
|
13
|
+
]
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Authentication configuration loaded from environment variables."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import logging
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import List, Optional
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger("mcp-server.auth")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class AuthConfig:
|
|
13
|
+
"""Authentication configuration."""
|
|
14
|
+
|
|
15
|
+
enabled: bool = False
|
|
16
|
+
issuer_url: Optional[str] = None
|
|
17
|
+
jwks_uri: Optional[str] = None
|
|
18
|
+
audience: str = "kubectl-mcp-server"
|
|
19
|
+
required_scopes: List[str] = field(default_factory=lambda: ["mcp:tools"])
|
|
20
|
+
resource_url: Optional[str] = None
|
|
21
|
+
|
|
22
|
+
def validate(self) -> bool:
|
|
23
|
+
"""Validate configuration."""
|
|
24
|
+
if not self.enabled:
|
|
25
|
+
return True
|
|
26
|
+
|
|
27
|
+
if not self.issuer_url:
|
|
28
|
+
logger.error("MCP_AUTH_ISSUER is required when authentication is enabled")
|
|
29
|
+
return False
|
|
30
|
+
|
|
31
|
+
return True
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def effective_jwks_uri(self) -> Optional[str]:
|
|
35
|
+
"""Get JWKS URI, deriving from issuer if not explicitly set."""
|
|
36
|
+
if self.jwks_uri:
|
|
37
|
+
return self.jwks_uri
|
|
38
|
+
if self.issuer_url:
|
|
39
|
+
# Standard OIDC discovery path
|
|
40
|
+
issuer = self.issuer_url.rstrip("/")
|
|
41
|
+
return f"{issuer}/.well-known/jwks.json"
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_auth_config() -> AuthConfig:
|
|
46
|
+
"""Load authentication configuration from environment variables."""
|
|
47
|
+
enabled = os.environ.get("MCP_AUTH_ENABLED", "").lower() in ("1", "true", "yes")
|
|
48
|
+
|
|
49
|
+
config = AuthConfig(
|
|
50
|
+
enabled=enabled,
|
|
51
|
+
issuer_url=os.environ.get("MCP_AUTH_ISSUER"),
|
|
52
|
+
jwks_uri=os.environ.get("MCP_AUTH_JWKS_URI"),
|
|
53
|
+
audience=os.environ.get("MCP_AUTH_AUDIENCE", "kubectl-mcp-server"),
|
|
54
|
+
required_scopes=_parse_scopes(os.environ.get("MCP_AUTH_REQUIRED_SCOPES", "mcp:tools")),
|
|
55
|
+
resource_url=os.environ.get("MCP_AUTH_RESOURCE_URL"),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
if enabled:
|
|
59
|
+
logger.info(f"Authentication enabled with issuer: {config.issuer_url}")
|
|
60
|
+
logger.info(f"Required scopes: {config.required_scopes}")
|
|
61
|
+
else:
|
|
62
|
+
logger.debug("Authentication disabled")
|
|
63
|
+
|
|
64
|
+
return config
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _parse_scopes(scopes_str: str) -> List[str]:
|
|
68
|
+
"""Parse comma-separated scopes string."""
|
|
69
|
+
if not scopes_str:
|
|
70
|
+
return []
|
|
71
|
+
return [s.strip() for s in scopes_str.split(",") if s.strip()]
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""MCP OAuth 2.0 scope definitions for fine-grained access control."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Dict, List, Set
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class MCPScopes(str, Enum):
|
|
8
|
+
"""MCP OAuth 2.0 scopes."""
|
|
9
|
+
|
|
10
|
+
# Base scopes
|
|
11
|
+
READ = "mcp:read"
|
|
12
|
+
WRITE = "mcp:write"
|
|
13
|
+
ADMIN = "mcp:admin"
|
|
14
|
+
TOOLS = "mcp:tools"
|
|
15
|
+
|
|
16
|
+
# Category-specific scopes
|
|
17
|
+
HELM = "mcp:helm"
|
|
18
|
+
DIAGNOSTICS = "mcp:diagnostics"
|
|
19
|
+
NETWORKING = "mcp:networking"
|
|
20
|
+
STORAGE = "mcp:storage"
|
|
21
|
+
SECURITY = "mcp:security"
|
|
22
|
+
COST = "mcp:cost"
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def all_scopes(cls) -> List[str]:
|
|
26
|
+
"""Return all available scopes."""
|
|
27
|
+
return [scope.value for scope in cls]
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def read_scopes(cls) -> List[str]:
|
|
31
|
+
"""Return scopes for read-only access."""
|
|
32
|
+
return [cls.READ.value, cls.DIAGNOSTICS.value]
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def write_scopes(cls) -> List[str]:
|
|
36
|
+
"""Return scopes for write access."""
|
|
37
|
+
return [cls.READ.value, cls.WRITE.value]
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def admin_scopes(cls) -> List[str]:
|
|
41
|
+
"""Return scopes for admin access."""
|
|
42
|
+
return cls.all_scopes()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# Map tool names to required scopes
|
|
46
|
+
# Tools not in this map require the default scope (mcp:tools)
|
|
47
|
+
TOOL_SCOPES: Dict[str, Set[str]] = {
|
|
48
|
+
# Read-only tools - require mcp:read
|
|
49
|
+
"get_pods": {MCPScopes.READ.value},
|
|
50
|
+
"get_pod_details": {MCPScopes.READ.value},
|
|
51
|
+
"list_namespaces": {MCPScopes.READ.value},
|
|
52
|
+
"get_deployments": {MCPScopes.READ.value},
|
|
53
|
+
"get_services": {MCPScopes.READ.value},
|
|
54
|
+
"get_nodes": {MCPScopes.READ.value},
|
|
55
|
+
"get_events": {MCPScopes.READ.value},
|
|
56
|
+
"get_configmaps": {MCPScopes.READ.value},
|
|
57
|
+
"describe_resource": {MCPScopes.READ.value},
|
|
58
|
+
"get_cluster_info": {MCPScopes.READ.value},
|
|
59
|
+
"get_api_resources": {MCPScopes.READ.value},
|
|
60
|
+
"get_api_versions": {MCPScopes.READ.value},
|
|
61
|
+
"get_resource_usage": {MCPScopes.READ.value},
|
|
62
|
+
"get_pod_logs": {MCPScopes.READ.value},
|
|
63
|
+
|
|
64
|
+
# Write tools - require mcp:write
|
|
65
|
+
"create_namespace": {MCPScopes.WRITE.value},
|
|
66
|
+
"delete_namespace": {MCPScopes.WRITE.value, MCPScopes.ADMIN.value},
|
|
67
|
+
"scale_deployment": {MCPScopes.WRITE.value},
|
|
68
|
+
"restart_deployment": {MCPScopes.WRITE.value},
|
|
69
|
+
"delete_pod": {MCPScopes.WRITE.value},
|
|
70
|
+
"apply_manifest": {MCPScopes.WRITE.value},
|
|
71
|
+
"delete_resource": {MCPScopes.WRITE.value},
|
|
72
|
+
"patch_resource": {MCPScopes.WRITE.value},
|
|
73
|
+
"create_configmap": {MCPScopes.WRITE.value},
|
|
74
|
+
"update_configmap": {MCPScopes.WRITE.value},
|
|
75
|
+
"delete_configmap": {MCPScopes.WRITE.value},
|
|
76
|
+
"create_secret": {MCPScopes.WRITE.value, MCPScopes.ADMIN.value},
|
|
77
|
+
"delete_secret": {MCPScopes.WRITE.value, MCPScopes.ADMIN.value},
|
|
78
|
+
|
|
79
|
+
# Admin tools - require mcp:admin
|
|
80
|
+
"get_rbac_roles": {MCPScopes.ADMIN.value, MCPScopes.SECURITY.value},
|
|
81
|
+
"get_cluster_roles": {MCPScopes.ADMIN.value, MCPScopes.SECURITY.value},
|
|
82
|
+
"audit_rbac_permissions": {MCPScopes.ADMIN.value, MCPScopes.SECURITY.value},
|
|
83
|
+
"analyze_pod_security": {MCPScopes.ADMIN.value, MCPScopes.SECURITY.value},
|
|
84
|
+
"check_secrets_security": {MCPScopes.ADMIN.value, MCPScopes.SECURITY.value},
|
|
85
|
+
"get_pod_security_info": {MCPScopes.ADMIN.value, MCPScopes.SECURITY.value},
|
|
86
|
+
"cordon_node": {MCPScopes.ADMIN.value},
|
|
87
|
+
"uncordon_node": {MCPScopes.ADMIN.value},
|
|
88
|
+
"drain_node": {MCPScopes.ADMIN.value},
|
|
89
|
+
"taint_node": {MCPScopes.ADMIN.value},
|
|
90
|
+
|
|
91
|
+
# Helm tools - require mcp:helm
|
|
92
|
+
"helm_list_releases": {MCPScopes.HELM.value, MCPScopes.READ.value},
|
|
93
|
+
"helm_get_values": {MCPScopes.HELM.value, MCPScopes.READ.value},
|
|
94
|
+
"helm_get_manifest": {MCPScopes.HELM.value, MCPScopes.READ.value},
|
|
95
|
+
"helm_install": {MCPScopes.HELM.value, MCPScopes.WRITE.value},
|
|
96
|
+
"helm_upgrade": {MCPScopes.HELM.value, MCPScopes.WRITE.value},
|
|
97
|
+
"helm_uninstall": {MCPScopes.HELM.value, MCPScopes.WRITE.value},
|
|
98
|
+
"helm_rollback": {MCPScopes.HELM.value, MCPScopes.WRITE.value},
|
|
99
|
+
"helm_repo_add": {MCPScopes.HELM.value, MCPScopes.WRITE.value},
|
|
100
|
+
"helm_repo_list": {MCPScopes.HELM.value, MCPScopes.READ.value},
|
|
101
|
+
"helm_search": {MCPScopes.HELM.value, MCPScopes.READ.value},
|
|
102
|
+
|
|
103
|
+
# Diagnostic tools - require mcp:diagnostics
|
|
104
|
+
"run_pod_diagnostics": {MCPScopes.DIAGNOSTICS.value},
|
|
105
|
+
"check_pod_health": {MCPScopes.DIAGNOSTICS.value},
|
|
106
|
+
"analyze_crashloopbackoff": {MCPScopes.DIAGNOSTICS.value},
|
|
107
|
+
"diagnose_pending_pods": {MCPScopes.DIAGNOSTICS.value},
|
|
108
|
+
"check_resource_quotas": {MCPScopes.DIAGNOSTICS.value},
|
|
109
|
+
"analyze_network_policies": {MCPScopes.DIAGNOSTICS.value, MCPScopes.NETWORKING.value},
|
|
110
|
+
"get_cluster_health": {MCPScopes.DIAGNOSTICS.value},
|
|
111
|
+
|
|
112
|
+
# Networking tools - require mcp:networking
|
|
113
|
+
"get_network_policies": {MCPScopes.NETWORKING.value, MCPScopes.READ.value},
|
|
114
|
+
"get_ingresses": {MCPScopes.NETWORKING.value, MCPScopes.READ.value},
|
|
115
|
+
"test_service_connectivity": {MCPScopes.NETWORKING.value, MCPScopes.DIAGNOSTICS.value},
|
|
116
|
+
|
|
117
|
+
# Storage tools - require mcp:storage
|
|
118
|
+
"get_persistent_volumes": {MCPScopes.STORAGE.value, MCPScopes.READ.value},
|
|
119
|
+
"get_persistent_volume_claims": {MCPScopes.STORAGE.value, MCPScopes.READ.value},
|
|
120
|
+
"get_storage_classes": {MCPScopes.STORAGE.value, MCPScopes.READ.value},
|
|
121
|
+
"analyze_storage_usage": {MCPScopes.STORAGE.value, MCPScopes.DIAGNOSTICS.value},
|
|
122
|
+
|
|
123
|
+
# Cost tools - require mcp:cost
|
|
124
|
+
"estimate_workload_cost": {MCPScopes.COST.value},
|
|
125
|
+
"get_resource_recommendations": {MCPScopes.COST.value, MCPScopes.DIAGNOSTICS.value},
|
|
126
|
+
"analyze_resource_efficiency": {MCPScopes.COST.value, MCPScopes.DIAGNOSTICS.value},
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def get_required_scopes(tool_name: str) -> Set[str]:
|
|
131
|
+
"""Get required scopes for a tool."""
|
|
132
|
+
return TOOL_SCOPES.get(tool_name, {MCPScopes.TOOLS.value})
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def has_required_scopes(token_scopes: Set[str], tool_name: str) -> bool:
|
|
136
|
+
"""Check if token has required scopes for a tool."""
|
|
137
|
+
required = get_required_scopes(tool_name)
|
|
138
|
+
|
|
139
|
+
# mcp:tools grants access to all tools
|
|
140
|
+
if MCPScopes.TOOLS.value in token_scopes:
|
|
141
|
+
return True
|
|
142
|
+
|
|
143
|
+
# mcp:admin grants access to all tools
|
|
144
|
+
if MCPScopes.ADMIN.value in token_scopes:
|
|
145
|
+
return True
|
|
146
|
+
|
|
147
|
+
# Check if token has at least one of the required scopes
|
|
148
|
+
return bool(token_scopes & required)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""JWT token verification via JWKS endpoints from OIDC-compliant identity providers."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Optional, Any
|
|
5
|
+
|
|
6
|
+
from .config import AuthConfig
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger("mcp-server.auth")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def create_auth_verifier(config: AuthConfig) -> Optional[Any]:
|
|
12
|
+
"""
|
|
13
|
+
Create an authentication verifier based on configuration.
|
|
14
|
+
|
|
15
|
+
Returns a FastMCP-compatible auth verifier or None if auth is disabled.
|
|
16
|
+
"""
|
|
17
|
+
if not config.enabled:
|
|
18
|
+
logger.debug("Authentication disabled, no verifier created")
|
|
19
|
+
return None
|
|
20
|
+
|
|
21
|
+
if not config.validate():
|
|
22
|
+
raise ValueError("Invalid authentication configuration")
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
from fastmcp.server.auth import JWTVerifier
|
|
26
|
+
except ImportError:
|
|
27
|
+
logger.warning(
|
|
28
|
+
"FastMCP auth module not available. "
|
|
29
|
+
"Authentication requires fastmcp>=3.0.0 with auth support."
|
|
30
|
+
)
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
jwks_uri = config.effective_jwks_uri
|
|
34
|
+
if not jwks_uri:
|
|
35
|
+
raise ValueError("JWKS URI could not be determined from configuration")
|
|
36
|
+
|
|
37
|
+
logger.info(f"Creating JWT verifier with JWKS URI: {jwks_uri}")
|
|
38
|
+
logger.info(f"Expected audience: {config.audience}")
|
|
39
|
+
logger.info(f"Expected issuer: {config.issuer_url}")
|
|
40
|
+
|
|
41
|
+
verifier = JWTVerifier(
|
|
42
|
+
jwks_uri=jwks_uri,
|
|
43
|
+
issuer=config.issuer_url,
|
|
44
|
+
audience=config.audience,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
return verifier
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def create_auth_settings(config: AuthConfig) -> Optional[Any]:
|
|
51
|
+
"""
|
|
52
|
+
Create RFC 9728 Protected Resource Metadata settings.
|
|
53
|
+
|
|
54
|
+
This enables the /.well-known/oauth-protected-resource endpoint
|
|
55
|
+
that MCP clients use to discover authorization requirements.
|
|
56
|
+
"""
|
|
57
|
+
if not config.enabled:
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
if not config.resource_url:
|
|
61
|
+
logger.debug("No resource URL configured, skipping RFC 9728 metadata")
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
from pydantic import AnyHttpUrl
|
|
66
|
+
from mcp.server.auth.settings import AuthSettings
|
|
67
|
+
except ImportError:
|
|
68
|
+
logger.warning(
|
|
69
|
+
"MCP auth settings not available. "
|
|
70
|
+
"RFC 9728 metadata requires mcp>=1.8.0."
|
|
71
|
+
)
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
logger.info(f"Creating RFC 9728 auth settings for resource: {config.resource_url}")
|
|
75
|
+
|
|
76
|
+
settings = AuthSettings(
|
|
77
|
+
issuer_url=AnyHttpUrl(config.issuer_url),
|
|
78
|
+
resource_server_url=AnyHttpUrl(config.resource_url),
|
|
79
|
+
required_scopes=config.required_scopes,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
return settings
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""CLI module for kubectl-mcp-tool."""
|
|
3
|
+
|
|
4
|
+
import sys
|
|
5
|
+
import os
|
|
6
|
+
import logging
|
|
7
|
+
import asyncio
|
|
8
|
+
import argparse
|
|
9
|
+
import traceback
|
|
10
|
+
from ..mcp_server import MCPServer
|
|
11
|
+
|
|
12
|
+
log_file = os.environ.get("MCP_LOG_FILE")
|
|
13
|
+
log_level = logging.DEBUG if os.environ.get("MCP_DEBUG", "").lower() in ("1", "true") else logging.INFO
|
|
14
|
+
|
|
15
|
+
handlers = []
|
|
16
|
+
if log_file:
|
|
17
|
+
os.makedirs(os.path.dirname(log_file), exist_ok=True)
|
|
18
|
+
handlers.append(logging.FileHandler(log_file))
|
|
19
|
+
handlers.append(logging.StreamHandler(sys.stderr))
|
|
20
|
+
|
|
21
|
+
logging.basicConfig(
|
|
22
|
+
level=log_level,
|
|
23
|
+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
24
|
+
handlers=handlers
|
|
25
|
+
)
|
|
26
|
+
logger = logging.getLogger("kubectl-mcp-cli")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
async def serve_stdio():
|
|
30
|
+
"""Serve the MCP server over stdio transport."""
|
|
31
|
+
server = MCPServer("kubernetes")
|
|
32
|
+
await server.serve_stdio()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
async def serve_sse(host: str, port: int):
|
|
36
|
+
"""Serve the MCP server over SSE transport."""
|
|
37
|
+
server = MCPServer("kubernetes")
|
|
38
|
+
await server.serve_sse(host=host, port=port)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
async def serve_http(host: str, port: int):
|
|
42
|
+
"""Serve the MCP server over HTTP transport."""
|
|
43
|
+
server = MCPServer("kubernetes")
|
|
44
|
+
await server.serve_http(host=host, port=port)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def main():
|
|
48
|
+
"""Main entry point for the CLI."""
|
|
49
|
+
parser = argparse.ArgumentParser(
|
|
50
|
+
description="kubectl-mcp-tool - MCP server for Kubernetes",
|
|
51
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
52
|
+
epilog="""
|
|
53
|
+
Examples:
|
|
54
|
+
kubectl-mcp serve # stdio transport (Claude Desktop/Cursor)
|
|
55
|
+
kubectl-mcp serve --transport sse # SSE transport
|
|
56
|
+
kubectl-mcp serve --transport http # HTTP transport
|
|
57
|
+
kubectl-mcp diagnostics # Run cluster diagnostics
|
|
58
|
+
"""
|
|
59
|
+
)
|
|
60
|
+
subparsers = parser.add_subparsers(dest="command", help="Command to run")
|
|
61
|
+
|
|
62
|
+
serve_parser = subparsers.add_parser("serve", help="Start the MCP server")
|
|
63
|
+
serve_parser.add_argument(
|
|
64
|
+
"--transport",
|
|
65
|
+
choices=["stdio", "sse", "http", "streamable-http"],
|
|
66
|
+
default="stdio",
|
|
67
|
+
help="Transport to use (default: stdio)"
|
|
68
|
+
)
|
|
69
|
+
serve_parser.add_argument("--host", type=str, default="0.0.0.0", help="Host for SSE/HTTP (default: 0.0.0.0)")
|
|
70
|
+
serve_parser.add_argument("--port", type=int, default=8000, help="Port for SSE/HTTP (default: 8000)")
|
|
71
|
+
serve_parser.add_argument("--debug", action="store_true", help="Enable debug logging")
|
|
72
|
+
|
|
73
|
+
subparsers.add_parser("version", help="Show version")
|
|
74
|
+
subparsers.add_parser("diagnostics", help="Run cluster diagnostics")
|
|
75
|
+
|
|
76
|
+
args = parser.parse_args()
|
|
77
|
+
|
|
78
|
+
if hasattr(args, 'debug') and args.debug:
|
|
79
|
+
logging.getLogger().setLevel(logging.DEBUG)
|
|
80
|
+
os.environ["MCP_DEBUG"] = "1"
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
if args.command == "serve":
|
|
84
|
+
if args.transport == "stdio":
|
|
85
|
+
asyncio.run(serve_stdio())
|
|
86
|
+
elif args.transport == "sse":
|
|
87
|
+
asyncio.run(serve_sse(args.host, args.port))
|
|
88
|
+
elif args.transport in ("http", "streamable-http"):
|
|
89
|
+
asyncio.run(serve_http(args.host, args.port))
|
|
90
|
+
elif args.command == "version":
|
|
91
|
+
from .. import __version__
|
|
92
|
+
print(f"kubectl-mcp-tool version {__version__}")
|
|
93
|
+
elif args.command == "diagnostics":
|
|
94
|
+
from ..diagnostics import run_diagnostics
|
|
95
|
+
import json
|
|
96
|
+
results = run_diagnostics()
|
|
97
|
+
print(json.dumps(results, indent=2))
|
|
98
|
+
else:
|
|
99
|
+
parser.print_help()
|
|
100
|
+
except KeyboardInterrupt:
|
|
101
|
+
pass
|
|
102
|
+
except Exception as e:
|
|
103
|
+
logger.error(f"Error: {e}")
|
|
104
|
+
if hasattr(args, 'debug') and args.debug:
|
|
105
|
+
logger.error(traceback.format_exc())
|
|
106
|
+
return 1
|
|
107
|
+
return 0
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
if __name__ == "__main__":
|
|
111
|
+
sys.exit(main())
|