whitecapdata-dev 0.1.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.
@@ -0,0 +1,12 @@
1
+ """homelab-mcp — operate a k3s / Kubernetes cluster from an MCP client.
2
+
3
+ Exposes read and (guarded) write tools over the Kubernetes API so an agent
4
+ (Claude Code, Claude Desktop, Cursor, …) can inspect cluster health and perform
5
+ safe, allowlisted operations — without shelling out to kubectl.
6
+ """
7
+
8
+ from .config import Config
9
+ from .kube import HomelabMCPError, KubeClient
10
+
11
+ __all__ = ["Config", "KubeClient", "HomelabMCPError", "__version__"]
12
+ __version__ = "0.1.0"
homelab_mcp/config.py ADDED
@@ -0,0 +1,54 @@
1
+ """Environment-driven configuration for the homelab-mcp server."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from dataclasses import dataclass
7
+
8
+ # Sensible homelab default: only let mutating tools touch these namespaces.
9
+ # Override with HOMELAB_MCP_MUTABLE_NAMESPACES; set it to "*" to allow all.
10
+ DEFAULT_MUTABLE_NAMESPACES = ("default", "apps", "monitoring", "ci")
11
+ DEFAULT_MAX_REPLICAS = 10
12
+ _TRUE = {"1", "true", "yes", "on"}
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class Config:
17
+ """Effective server configuration, sourced from the environment."""
18
+
19
+ context: str | None = None
20
+ read_only: bool = False
21
+ mutable_namespaces: tuple[str, ...] = DEFAULT_MUTABLE_NAMESPACES
22
+ max_replicas: int = DEFAULT_MAX_REPLICAS
23
+
24
+ @classmethod
25
+ def from_env(cls, env: dict[str, str] | None = None) -> Config:
26
+ src = os.environ if env is None else env
27
+ raw_ns = src.get("HOMELAB_MCP_MUTABLE_NAMESPACES")
28
+ if raw_ns is None:
29
+ namespaces = DEFAULT_MUTABLE_NAMESPACES
30
+ elif raw_ns.strip() == "*":
31
+ namespaces = () # empty tuple == every namespace allowed
32
+ else:
33
+ namespaces = tuple(n.strip() for n in raw_ns.split(",") if n.strip())
34
+ return cls(
35
+ context=src.get("HOMELAB_MCP_CONTEXT") or None,
36
+ read_only=src.get("HOMELAB_MCP_READONLY", "").lower() in _TRUE,
37
+ mutable_namespaces=namespaces,
38
+ max_replicas=int(src.get("HOMELAB_MCP_MAX_REPLICAS", str(DEFAULT_MAX_REPLICAS))),
39
+ )
40
+
41
+ def namespace_allowed(self, namespace: str) -> bool:
42
+ """True if mutating tools may act on ``namespace``.
43
+
44
+ An empty allowlist means "all namespaces" (the operator opted in via ``*``).
45
+ """
46
+ return not self.mutable_namespaces or namespace in self.mutable_namespaces
47
+
48
+ def as_dict(self) -> dict[str, object]:
49
+ return {
50
+ "context": self.context or "(current-context)",
51
+ "read_only": self.read_only,
52
+ "mutable_namespaces": list(self.mutable_namespaces) or ["*"],
53
+ "max_replicas": self.max_replicas,
54
+ }
homelab_mcp/format.py ADDED
@@ -0,0 +1,125 @@
1
+ """Pure mappers from Kubernetes API objects to plain JSON-able structures.
2
+
3
+ These take any object that exposes the same attributes as the official
4
+ ``kubernetes`` client models, so they're unit-testable with lightweight fakes
5
+ (e.g. ``types.SimpleNamespace``) — no cluster required.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any
11
+
12
+
13
+ def _attr(obj: Any, *path: str, default: Any = None) -> Any:
14
+ """Safely walk a chain of attributes, returning ``default`` on any miss."""
15
+ cur = obj
16
+ for name in path:
17
+ if cur is None:
18
+ return default
19
+ cur = getattr(cur, name, None)
20
+ return cur if cur is not None else default
21
+
22
+
23
+ def pod_is_healthy(pod: Any) -> bool:
24
+ """A pod is healthy if it's Running/Succeeded and all containers are ready."""
25
+ phase = _attr(pod, "status", "phase", default="")
26
+ if phase == "Succeeded":
27
+ return True
28
+ if phase != "Running":
29
+ return False
30
+ statuses = _attr(pod, "status", "container_statuses", default=[]) or []
31
+ return all(getattr(cs, "ready", False) for cs in statuses)
32
+
33
+
34
+ def pod_row(pod: Any) -> dict[str, Any]:
35
+ statuses = _attr(pod, "status", "container_statuses", default=[]) or []
36
+ restarts = sum(getattr(cs, "restart_count", 0) or 0 for cs in statuses)
37
+ return {
38
+ "namespace": _attr(pod, "metadata", "namespace", default=""),
39
+ "name": _attr(pod, "metadata", "name", default=""),
40
+ "phase": _attr(pod, "status", "phase", default=""),
41
+ "ready": pod_is_healthy(pod),
42
+ "restarts": restarts,
43
+ "node": _attr(pod, "spec", "node_name", default=""),
44
+ }
45
+
46
+
47
+ def pod_rows(pods: list[Any]) -> list[dict[str, Any]]:
48
+ """Rows for a pod list, unhealthy first then by namespace/name."""
49
+ rows = [pod_row(p) for p in pods]
50
+ rows.sort(key=lambda r: (r["ready"], r["namespace"], r["name"]))
51
+ return rows
52
+
53
+
54
+ def deployment_row(dep: Any) -> dict[str, Any]:
55
+ desired = _attr(dep, "spec", "replicas", default=0) or 0
56
+ ready = _attr(dep, "status", "ready_replicas", default=0) or 0
57
+ return {
58
+ "namespace": _attr(dep, "metadata", "namespace", default=""),
59
+ "name": _attr(dep, "metadata", "name", default=""),
60
+ "ready_replicas": ready,
61
+ "desired_replicas": desired,
62
+ "available": ready >= desired and desired > 0,
63
+ }
64
+
65
+
66
+ def deployment_rows(deps: list[Any]) -> list[dict[str, Any]]:
67
+ rows = [deployment_row(d) for d in deps]
68
+ rows.sort(key=lambda r: (r["available"], r["namespace"], r["name"]))
69
+ return rows
70
+
71
+
72
+ def node_row(node: Any) -> dict[str, Any]:
73
+ conditions = _attr(node, "status", "conditions", default=[]) or []
74
+ ready = any(getattr(c, "type", "") == "Ready" and getattr(c, "status", "") == "True" for c in conditions)
75
+ # Pressure conditions are healthy when "False".
76
+ pressures = {
77
+ getattr(c, "type", ""): getattr(c, "status", "")
78
+ for c in conditions
79
+ if getattr(c, "type", "").endswith("Pressure")
80
+ }
81
+ cap = _attr(node, "status", "capacity", default={}) or {}
82
+ return {
83
+ "name": _attr(node, "metadata", "name", default=""),
84
+ "ready": ready,
85
+ "kubelet": _attr(node, "status", "node_info", "kubelet_version", default=""),
86
+ "cpu": cap.get("cpu", ""),
87
+ "memory": cap.get("memory", ""),
88
+ "pressure": [k for k, v in pressures.items() if v == "True"],
89
+ }
90
+
91
+
92
+ def node_rows(nodes: list[Any]) -> list[dict[str, Any]]:
93
+ return [node_row(n) for n in nodes]
94
+
95
+
96
+ def cluster_summary(nodes: list[Any], pods: list[Any]) -> dict[str, Any]:
97
+ node_data = node_rows(nodes)
98
+ pod_data = pod_rows(pods)
99
+ unhealthy = [p for p in pod_data if not p["ready"]]
100
+ phases: dict[str, int] = {}
101
+ for p in pod_data:
102
+ phases[p["phase"]] = phases.get(p["phase"], 0) + 1
103
+ return {
104
+ "nodes": {"total": len(node_data), "ready": sum(1 for n in node_data if n["ready"])},
105
+ "pods": {"total": len(pod_data), "by_phase": phases, "unhealthy": len(unhealthy)},
106
+ "unhealthy_pods": unhealthy,
107
+ }
108
+
109
+
110
+ def event_row(ev: Any) -> dict[str, Any]:
111
+ return {
112
+ "type": _attr(ev, "type", default=""),
113
+ "reason": _attr(ev, "reason", default=""),
114
+ "object": f"{_attr(ev, 'involved_object', 'kind', default='')}/"
115
+ f"{_attr(ev, 'involved_object', 'name', default='')}",
116
+ "namespace": _attr(ev, "metadata", "namespace", default=""),
117
+ "message": _attr(ev, "message", default=""),
118
+ }
119
+
120
+
121
+ def event_rows(events: list[Any], limit: int) -> list[dict[str, Any]]:
122
+ rows = [event_row(e) for e in events]
123
+ # Warnings first so problems surface at the top.
124
+ rows.sort(key=lambda r: r["type"] != "Warning")
125
+ return rows[:limit]
homelab_mcp/kube.py ADDED
@@ -0,0 +1,118 @@
1
+ """Kubernetes API wrapper used by the MCP tools.
2
+
3
+ Reads happen through the CoreV1/AppsV1 APIs and are shaped by :mod:`format`.
4
+ Mutations are gated on the operator's config (read-only switch + namespace
5
+ allowlist) *before* any API call, so a misbehaving agent can't scale or delete
6
+ outside the sandbox the operator allowed.
7
+
8
+ The official ``kubernetes`` client is imported lazily so the pure logic (and
9
+ its tests) don't require it to be installed or a cluster to be reachable.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from datetime import UTC, datetime
15
+ from typing import Any
16
+
17
+ from . import format as fmt
18
+ from .config import Config
19
+
20
+ RESTART_ANNOTATION = "kubectl.kubernetes.io/restartedAt"
21
+
22
+
23
+ class HomelabMCPError(RuntimeError):
24
+ """A user-facing error (bad config, blocked mutation, or API failure)."""
25
+
26
+
27
+ def _load_apis(config: Config) -> tuple[Any, Any]:
28
+ """Load kube config (in-cluster, else kubeconfig) and return (core, apps)."""
29
+ try:
30
+ from kubernetes import client # noqa: PLC0415
31
+ from kubernetes import config as kube_config # noqa: PLC0415
32
+ except ImportError as exc: # pragma: no cover - import guard
33
+ raise HomelabMCPError(
34
+ "The 'kubernetes' package is required. Install with: pip install homelab-mcp"
35
+ ) from exc
36
+ try:
37
+ kube_config.load_incluster_config()
38
+ except kube_config.ConfigException:
39
+ kube_config.load_kube_config(context=config.context)
40
+ return client.CoreV1Api(), client.AppsV1Api()
41
+
42
+
43
+ class KubeClient:
44
+ """Thin, testable facade over the parts of the Kubernetes API we expose."""
45
+
46
+ def __init__(self, config: Config, core_v1: Any = None, apps_v1: Any = None) -> None:
47
+ self._config = config
48
+ if core_v1 is None or apps_v1 is None:
49
+ core_v1, apps_v1 = _load_apis(config)
50
+ self._core = core_v1
51
+ self._apps = apps_v1
52
+
53
+ @property
54
+ def config(self) -> Config:
55
+ return self._config
56
+
57
+ # -- reads ---------------------------------------------------------------
58
+
59
+ def _pods(self, namespace: str = "") -> list[Any]:
60
+ if namespace:
61
+ return self._core.list_namespaced_pod(namespace).items
62
+ return self._core.list_pod_for_all_namespaces().items
63
+
64
+ def cluster_summary(self) -> dict[str, Any]:
65
+ nodes = self._core.list_node().items
66
+ return fmt.cluster_summary(nodes, self._pods())
67
+
68
+ def list_pods(self, namespace: str = "") -> list[dict[str, Any]]:
69
+ return fmt.pod_rows(self._pods(namespace))
70
+
71
+ def list_deployments(self, namespace: str = "") -> list[dict[str, Any]]:
72
+ if namespace:
73
+ deps = self._apps.list_namespaced_deployment(namespace).items
74
+ else:
75
+ deps = self._apps.list_deployment_for_all_namespaces().items
76
+ return fmt.deployment_rows(deps)
77
+
78
+ def list_events(self, limit: int = 30) -> list[dict[str, Any]]:
79
+ events = self._core.list_event_for_all_namespaces().items
80
+ return fmt.event_rows(events, limit)
81
+
82
+ def pod_logs(self, namespace: str, pod: str, tail: int = 200) -> str:
83
+ return self._core.read_namespaced_pod_log(name=pod, namespace=namespace, tail_lines=tail)
84
+
85
+ def node_health(self) -> list[dict[str, Any]]:
86
+ return fmt.node_rows(self._core.list_node().items)
87
+
88
+ # -- mutations (guarded) -------------------------------------------------
89
+
90
+ def _guard(self, namespace: str) -> None:
91
+ if self._config.read_only:
92
+ raise HomelabMCPError("Server is in read-only mode; mutations are disabled.")
93
+ if not self._config.namespace_allowed(namespace):
94
+ allowed = ", ".join(self._config.mutable_namespaces) or "*"
95
+ raise HomelabMCPError(
96
+ f"Namespace '{namespace}' is not in the mutable allowlist ({allowed})."
97
+ )
98
+
99
+ def restart_deployment(self, namespace: str, name: str) -> str:
100
+ self._guard(namespace)
101
+ stamp = datetime.now(UTC).isoformat()
102
+ body = {"spec": {"template": {"metadata": {"annotations": {RESTART_ANNOTATION: stamp}}}}}
103
+ self._apps.patch_namespaced_deployment(name=name, namespace=namespace, body=body)
104
+ return f"restarted deployment {namespace}/{name} at {stamp}"
105
+
106
+ def scale_deployment(self, namespace: str, name: str, replicas: int) -> str:
107
+ self._guard(namespace)
108
+ if not 0 <= replicas <= self._config.max_replicas:
109
+ raise HomelabMCPError(f"replicas must be between 0 and {self._config.max_replicas}.")
110
+ self._apps.patch_namespaced_deployment_scale(
111
+ name=name, namespace=namespace, body={"spec": {"replicas": replicas}}
112
+ )
113
+ return f"scaled deployment {namespace}/{name} to {replicas} replicas"
114
+
115
+ def delete_pod(self, namespace: str, name: str) -> str:
116
+ self._guard(namespace)
117
+ self._core.delete_namespaced_pod(name=name, namespace=namespace)
118
+ return f"deleted pod {namespace}/{name} (its controller will recreate it)"
homelab_mcp/server.py ADDED
@@ -0,0 +1,113 @@
1
+ """The homelab-mcp MCP server.
2
+
3
+ Tools return JSON strings so the calling agent gets structured, compact data.
4
+ Reads are always available; mutations are gated by the operator's config
5
+ (read-only switch + namespace allowlist) enforced in :class:`KubeClient`.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ from typing import Any
12
+
13
+ from mcp.server.fastmcp import FastMCP
14
+
15
+ from .config import Config
16
+ from .kube import KubeClient
17
+
18
+ mcp = FastMCP("homelab")
19
+
20
+ _client: KubeClient | None = None
21
+
22
+
23
+ def get_client() -> KubeClient:
24
+ """Lazily build the cluster client (so import never touches a cluster)."""
25
+ global _client
26
+ if _client is None:
27
+ _client = KubeClient(Config.from_env())
28
+ return _client
29
+
30
+
31
+ def set_client(client: KubeClient) -> None:
32
+ """Replace the module-level client (used by tests)."""
33
+ global _client
34
+ _client = client
35
+
36
+
37
+ def _json(data: Any) -> str:
38
+ return json.dumps(data, indent=2, default=str)
39
+
40
+
41
+ # -- reads -------------------------------------------------------------------
42
+
43
+
44
+ @mcp.tool()
45
+ def cluster_summary() -> str:
46
+ """Node and pod health totals plus the list of unhealthy pods. Start here."""
47
+ return _json(get_client().cluster_summary())
48
+
49
+
50
+ @mcp.tool()
51
+ def list_pods(namespace: str = "") -> str:
52
+ """List pods (optionally one namespace). Unhealthy pods sort first."""
53
+ return _json(get_client().list_pods(namespace))
54
+
55
+
56
+ @mcp.tool()
57
+ def list_deployments(namespace: str = "") -> str:
58
+ """List deployments with ready/desired replica counts."""
59
+ return _json(get_client().list_deployments(namespace))
60
+
61
+
62
+ @mcp.tool()
63
+ def list_events(limit: int = 30) -> str:
64
+ """Recent cluster events; Warning-type events sort first."""
65
+ return _json(get_client().list_events(limit))
66
+
67
+
68
+ @mcp.tool()
69
+ def pod_logs(namespace: str, pod: str, tail: int = 200) -> str:
70
+ """Tail a pod's logs."""
71
+ return get_client().pod_logs(namespace, pod, tail)
72
+
73
+
74
+ @mcp.tool()
75
+ def node_health() -> str:
76
+ """Per-node readiness, kubelet version, capacity, and pressure conditions."""
77
+ return _json(get_client().node_health())
78
+
79
+
80
+ # -- mutations (guarded by read-only + namespace allowlist) ------------------
81
+
82
+
83
+ @mcp.tool()
84
+ def restart_deployment(namespace: str, name: str) -> str:
85
+ """Rollout-restart a deployment (subject to the mutable-namespace allowlist)."""
86
+ return get_client().restart_deployment(namespace, name)
87
+
88
+
89
+ @mcp.tool()
90
+ def scale_deployment(namespace: str, name: str, replicas: int) -> str:
91
+ """Scale a deployment to N replicas (0..max), subject to the allowlist."""
92
+ return get_client().scale_deployment(namespace, name, replicas)
93
+
94
+
95
+ @mcp.tool()
96
+ def delete_pod(namespace: str, name: str) -> str:
97
+ """Delete a pod so its controller recreates it (subject to the allowlist)."""
98
+ return get_client().delete_pod(namespace, name)
99
+
100
+
101
+ @mcp.tool()
102
+ def server_info() -> str:
103
+ """Report the effective configuration (context, read-only, allowlist)."""
104
+ return _json(get_client().config.as_dict())
105
+
106
+
107
+ def main() -> None:
108
+ """Console-script entry point: run the server over stdio."""
109
+ mcp.run()
110
+
111
+
112
+ if __name__ == "__main__":
113
+ main()
@@ -0,0 +1,136 @@
1
+ Metadata-Version: 2.4
2
+ Name: whitecapdata-dev
3
+ Version: 0.1.0
4
+ Summary: WhiteCapData-Dev — an MCP server to operate a k3s / Kubernetes cluster (health, logs, and guarded restart/scale/delete) straight from your AI agent.
5
+ Project-URL: Homepage, https://github.com/Michael-WhiteCapData/WhiteCapData-Dev
6
+ Project-URL: Repository, https://github.com/Michael-WhiteCapData/WhiteCapData-Dev
7
+ Project-URL: Issues, https://github.com/Michael-WhiteCapData/WhiteCapData-Dev/issues
8
+ Author: Michael Tierney
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: claude,devops,homelab,k3s,k8s,kubernetes,mcp,model-context-protocol
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: System Administrators
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: System :: Systems Administration
19
+ Requires-Python: >=3.11
20
+ Requires-Dist: kubernetes>=29
21
+ Requires-Dist: mcp>=1.2
22
+ Provides-Extra: dev
23
+ Requires-Dist: pytest-cov>=5; extra == 'dev'
24
+ Requires-Dist: pytest>=8; extra == 'dev'
25
+ Requires-Dist: ruff>=0.6; extra == 'dev'
26
+ Description-Content-Type: text/markdown
27
+
28
+ # WhiteCapData-Dev
29
+
30
+ **Operate a k3s / Kubernetes cluster straight from your AI agent — safe by default.**
31
+
32
+ [![CI](https://github.com/Michael-WhiteCapData/WhiteCapData-Dev/actions/workflows/ci.yml/badge.svg)](https://github.com/Michael-WhiteCapData/WhiteCapData-Dev/actions/workflows/ci.yml)
33
+ [![PyPI](https://img.shields.io/pypi/v/whitecapdata-dev?color=3775A9&logo=pypi&logoColor=white)](https://pypi.org/project/whitecapdata-dev/)
34
+ [![Python](https://img.shields.io/badge/python-3.11%2B-3776AB?logo=python&logoColor=white)](https://www.python.org/)
35
+ [![MCP](https://img.shields.io/badge/MCP-server-D97757)](https://modelcontextprotocol.io/)
36
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
37
+
38
+ An [MCP](https://modelcontextprotocol.io/) server that lets an agent (Claude Code, Claude Desktop, Cursor, …) inspect and operate a **Kubernetes / k3s** cluster — your homelab box, a dev cluster, whatever your kubeconfig points at — **without shelling out to `kubectl`**. It talks to the Kubernetes API directly using your existing kubeconfig (or an in-cluster service account).
39
+
40
+ The design goal is **safe by default**: reads are always on; every mutating action (restart / scale / delete) is gated *before the API call* by a read-only switch and a namespace allowlist, so an over-eager agent can't touch `kube-system` or nuke a deployment you didn't sandbox.
41
+
42
+ > **Name note:** the PyPI package is `whitecapdata-dev` (the `homelab-k8s`-style name was taken); the import package and tools are k8s/homelab-focused as described here.
43
+
44
+ ---
45
+
46
+ ## Why you'd want this
47
+
48
+ - 🩺 **One-call health.** `cluster_summary` gives node + pod totals and the unhealthy pods, so the agent starts triage with real data.
49
+ - 🔒 **Safe by default.** Mutations are blocked unless the namespace is on your allowlist; flip `HOMELAB_MCP_READONLY=1` to make the whole server read-only.
50
+ - 🧰 **The operations you actually do.** Pods, deployments, events, logs, node health, rollout-restart, scale, delete-pod.
51
+ - 🪶 **No bespoke backend.** Uses the standard Kubernetes API + your kubeconfig — nothing to deploy server-side.
52
+ - ✅ **Tested.** Pure logic is unit-tested with fakes; guard logic is tested against a mocked API. No cluster needed to run the suite.
53
+
54
+ ## Requirements
55
+
56
+ - A reachable cluster and a working **kubeconfig** (the same one `kubectl` uses), or run it in-cluster with a service account.
57
+ - Python 3.11+ (or just `uvx`).
58
+
59
+ ## Install
60
+
61
+ ```bash
62
+ uvx whitecapdata-dev # run directly
63
+ # or
64
+ pip install whitecapdata-dev # then run: whitecapdata-dev
65
+ ```
66
+
67
+ ### Claude Code
68
+
69
+ ```bash
70
+ claude mcp add homelab -- uvx whitecapdata-dev
71
+ ```
72
+
73
+ ### Claude Desktop / Cursor
74
+
75
+ ```jsonc
76
+ {
77
+ "mcpServers": {
78
+ "homelab": {
79
+ "command": "uvx",
80
+ "args": ["whitecapdata-dev"],
81
+ "env": {
82
+ "HOMELAB_MCP_MUTABLE_NAMESPACES": "default,apps,monitoring",
83
+ "HOMELAB_MCP_READONLY": "0"
84
+ }
85
+ }
86
+ }
87
+ }
88
+ ```
89
+
90
+ ## Tools
91
+
92
+ | Tool | Kind | Description |
93
+ | --- | --- | --- |
94
+ | `cluster_summary` | read | Node/pod health totals + unhealthy pods |
95
+ | `list_pods` | read | Pods (optionally one namespace), unhealthy first |
96
+ | `list_deployments` | read | Deployments with ready/desired replicas |
97
+ | `list_events` | read | Recent events, Warnings first |
98
+ | `pod_logs` | read | Tail a pod's logs |
99
+ | `node_health` | read | Per-node readiness, kubelet, capacity, pressure |
100
+ | `restart_deployment` | **write** | Rollout-restart (allowlisted namespaces) |
101
+ | `scale_deployment` | **write** | Scale to N replicas (0..max, allowlisted) |
102
+ | `delete_pod` | **write** | Delete a pod; its controller recreates it (allowlisted) |
103
+ | `server_info` | read | Effective config (context, read-only, allowlist) |
104
+
105
+ ## Configuration
106
+
107
+ | Variable | Default | Description |
108
+ | --- | --- | --- |
109
+ | `HOMELAB_MCP_CONTEXT` | current-context | kubeconfig context to use |
110
+ | `HOMELAB_MCP_READONLY` | `0` | `1`/`true` disables all mutating tools |
111
+ | `HOMELAB_MCP_MUTABLE_NAMESPACES` | `default,apps,monitoring,ci` | Namespaces mutations may touch; `*` = all |
112
+ | `HOMELAB_MCP_MAX_REPLICAS` | `10` | Upper bound for `scale_deployment` |
113
+
114
+ ## Safety model
115
+
116
+ 1. **Read-only switch** — `HOMELAB_MCP_READONLY=1` rejects every mutating tool up front.
117
+ 2. **Namespace allowlist** — mutating tools refuse any namespace not in `HOMELAB_MCP_MUTABLE_NAMESPACES` (default a homelab-friendly set; `*` opts into all).
118
+ 3. **Bounded scale** — `scale_deployment` clamps to `0..HOMELAB_MCP_MAX_REPLICAS`.
119
+
120
+ The cluster's own RBAC still applies on top — this server can only do what the kubeconfig identity is permitted to do.
121
+
122
+ ## Development
123
+
124
+ ```bash
125
+ git clone https://github.com/Michael-WhiteCapData/WhiteCapData-Dev
126
+ cd WhiteCapData-Dev
127
+ uv pip install -e ".[dev]"
128
+ ruff check .
129
+ pytest # no cluster required — APIs are faked/mocked
130
+ ```
131
+
132
+ See [CONTRIBUTING.md](CONTRIBUTING.md).
133
+
134
+ ## License
135
+
136
+ [MIT](LICENSE) © Michael Tierney
@@ -0,0 +1,10 @@
1
+ homelab_mcp/__init__.py,sha256=nYcCY6IHWd9AiTvbYryXTPSH2ehII6m8X2Gwt_TNKmY,468
2
+ homelab_mcp/config.py,sha256=f-h5nm7sn0T4--0jGcF6qjZukee4VDZDVsnNV_-40WA,2088
3
+ homelab_mcp/format.py,sha256=dQuDyBHWKU-g6FrzCZ-JOKlAgL-UEI30zY7V7r4b-Ug,4662
4
+ homelab_mcp/kube.py,sha256=KPNsGcMAJMu8xcw__6uNbdl_qzwY-RC3jY-u1XdCU_s,4925
5
+ homelab_mcp/server.py,sha256=oX7DR5rln6Q5XaLLd-Eo9z3fDR4FcSGd8oZJWXXDwTg,3100
6
+ whitecapdata_dev-0.1.0.dist-info/METADATA,sha256=Xg598St0ZJ93lsrx_pV3Azvnk1tQ_xOWWznC7ovwUDU,6117
7
+ whitecapdata_dev-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
8
+ whitecapdata_dev-0.1.0.dist-info/entry_points.txt,sha256=3kAnizcfuUcuUaoK5_kPU9jF0I7lqEone9sO5Mabssc,61
9
+ whitecapdata_dev-0.1.0.dist-info/licenses/LICENSE,sha256=CY7xjvDIH4rbWyhYFOZZaAfXsrsdo5apgxDnsY-xq8g,1072
10
+ whitecapdata_dev-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ whitecapdata-dev = homelab_mcp.server:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Michael Tierney
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.