whitecapdata-dev 0.1.1__tar.gz → 0.1.3__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 (25) hide show
  1. whitecapdata_dev-0.1.3/.dockerignore +12 -0
  2. {whitecapdata_dev-0.1.1 → whitecapdata_dev-0.1.3}/CHANGELOG.md +14 -0
  3. whitecapdata_dev-0.1.3/Dockerfile +29 -0
  4. {whitecapdata_dev-0.1.1 → whitecapdata_dev-0.1.3}/PKG-INFO +17 -4
  5. {whitecapdata_dev-0.1.1 → whitecapdata_dev-0.1.3}/README.md +13 -0
  6. {whitecapdata_dev-0.1.1 → whitecapdata_dev-0.1.3}/pyproject.toml +3 -3
  7. {whitecapdata_dev-0.1.1 → whitecapdata_dev-0.1.3}/server.json +3 -3
  8. whitecapdata_dev-0.1.3/src/homelab_mcp/__init__.py +12 -0
  9. whitecapdata_dev-0.1.3/src/homelab_mcp/server.py +227 -0
  10. whitecapdata_dev-0.1.1/src/homelab_mcp/__init__.py +0 -12
  11. whitecapdata_dev-0.1.1/src/homelab_mcp/server.py +0 -113
  12. {whitecapdata_dev-0.1.1 → whitecapdata_dev-0.1.3}/.github/workflows/ci.yml +0 -0
  13. {whitecapdata_dev-0.1.1 → whitecapdata_dev-0.1.3}/.github/workflows/publish-mcp.yml +0 -0
  14. {whitecapdata_dev-0.1.1 → whitecapdata_dev-0.1.3}/.github/workflows/release.yml +0 -0
  15. {whitecapdata_dev-0.1.1 → whitecapdata_dev-0.1.3}/.gitignore +0 -0
  16. {whitecapdata_dev-0.1.1 → whitecapdata_dev-0.1.3}/CONTRIBUTING.md +0 -0
  17. {whitecapdata_dev-0.1.1 → whitecapdata_dev-0.1.3}/LICENSE +0 -0
  18. {whitecapdata_dev-0.1.1 → whitecapdata_dev-0.1.3}/src/homelab_mcp/config.py +0 -0
  19. {whitecapdata_dev-0.1.1 → whitecapdata_dev-0.1.3}/src/homelab_mcp/format.py +0 -0
  20. {whitecapdata_dev-0.1.1 → whitecapdata_dev-0.1.3}/src/homelab_mcp/kube.py +0 -0
  21. {whitecapdata_dev-0.1.1 → whitecapdata_dev-0.1.3}/tests/conftest.py +0 -0
  22. {whitecapdata_dev-0.1.1 → whitecapdata_dev-0.1.3}/tests/test_config.py +0 -0
  23. {whitecapdata_dev-0.1.1 → whitecapdata_dev-0.1.3}/tests/test_format.py +0 -0
  24. {whitecapdata_dev-0.1.1 → whitecapdata_dev-0.1.3}/tests/test_kube.py +0 -0
  25. {whitecapdata_dev-0.1.1 → whitecapdata_dev-0.1.3}/tests/test_server_tools.py +0 -0
@@ -0,0 +1,12 @@
1
+ .git
2
+ .github
3
+ tests
4
+ __pycache__
5
+ *.pyc
6
+ .pytest_cache
7
+ .ruff_cache
8
+ dist
9
+ build
10
+ *.egg-info
11
+ .venv
12
+ .mypy_cache
@@ -4,6 +4,20 @@ All notable changes to this project are documented here. The format is based on
4
4
  [Keep a Changelog](https://keepachangelog.com/), and this project adheres to
5
5
  [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## 0.1.3 (2026-09-23)
8
+
9
+ ### Fixed
10
+
11
+ * Constrain MCP below version 2 so fresh installations retain the supported FastMCP API.
12
+
13
+ ## [0.1.2] - 2026-06-22
14
+
15
+ ### Added
16
+ - `Dockerfile` and `.dockerignore` for running the server as a container (MCP over stdio).
17
+
18
+ ### Changed
19
+ - Enriched every tool docstring and documented all parameters for clearer agent tool-selection.
20
+
7
21
  ## [0.1.0] - 2026-06-20
8
22
 
9
23
  ### Added
@@ -0,0 +1,29 @@
1
+ # syntax=docker/dockerfile:1
2
+ # Container image for the WhiteCapData-Dev (k3s / Kubernetes) MCP server.
3
+ #
4
+ # The server speaks MCP (JSON-RPC) over stdio and talks to your cluster through a
5
+ # mounted kubeconfig. Run interactively (-i), mounting your kubeconfig read-only:
6
+ #
7
+ # docker build -t whitecapdata-dev .
8
+ # docker run --rm -i \
9
+ # -v "$HOME/.kube/config:/home/app/.kube/config:ro" \
10
+ # -e HOMELAB_MCP_READONLY=1 \
11
+ # whitecapdata-dev
12
+ #
13
+ # Set HOMELAB_MCP_READONLY=1 to disable all mutating tools (recommended for a
14
+ # first run). See the README for the full environment-variable reference.
15
+ FROM python:3.12-slim
16
+
17
+ ENV PYTHONUNBUFFERED=1 \
18
+ PIP_NO_CACHE_DIR=1 \
19
+ PIP_DISABLE_PIP_VERSION_CHECK=1 \
20
+ KUBECONFIG=/home/app/.kube/config
21
+
22
+ WORKDIR /app
23
+ COPY . /app
24
+ RUN pip install . \
25
+ && useradd --create-home --uid 10001 app
26
+
27
+ USER app
28
+
29
+ ENTRYPOINT ["whitecapdata-dev"]
@@ -1,7 +1,7 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: whitecapdata-dev
3
- Version: 0.1.1
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.
3
+ Version: 0.1.3
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
5
  Project-URL: Homepage, https://github.com/Michael-WhiteCapData/WhiteCapData-Dev
6
6
  Project-URL: Repository, https://github.com/Michael-WhiteCapData/WhiteCapData-Dev
7
7
  Project-URL: Issues, https://github.com/Michael-WhiteCapData/WhiteCapData-Dev/issues
@@ -18,7 +18,7 @@ Classifier: Programming Language :: Python :: 3.12
18
18
  Classifier: Topic :: System :: Systems Administration
19
19
  Requires-Python: >=3.11
20
20
  Requires-Dist: kubernetes>=29
21
- Requires-Dist: mcp>=1.2
21
+ Requires-Dist: mcp<2,>=1.2
22
22
  Provides-Extra: dev
23
23
  Requires-Dist: pytest-cov>=5; extra == 'dev'
24
24
  Requires-Dist: pytest>=8; extra == 'dev'
@@ -89,6 +89,19 @@ claude mcp add homelab -- uvx whitecapdata-dev
89
89
  }
90
90
  ```
91
91
 
92
+ ## Run with Docker
93
+
94
+ A [`Dockerfile`](Dockerfile) is included. The server speaks MCP over stdio and reaches
95
+ your cluster through a mounted kubeconfig. Run interactively (`-i`), starting read-only:
96
+
97
+ ```bash
98
+ docker build -t whitecapdata-dev .
99
+ docker run --rm -i \
100
+ -v "$HOME/.kube/config:/home/app/.kube/config:ro" \
101
+ -e HOMELAB_MCP_READONLY=1 \
102
+ whitecapdata-dev
103
+ ```
104
+
92
105
  ## Tools
93
106
 
94
107
  | Tool | Kind | Description |
@@ -62,6 +62,19 @@ claude mcp add homelab -- uvx whitecapdata-dev
62
62
  }
63
63
  ```
64
64
 
65
+ ## Run with Docker
66
+
67
+ A [`Dockerfile`](Dockerfile) is included. The server speaks MCP over stdio and reaches
68
+ your cluster through a mounted kubeconfig. Run interactively (`-i`), starting read-only:
69
+
70
+ ```bash
71
+ docker build -t whitecapdata-dev .
72
+ docker run --rm -i \
73
+ -v "$HOME/.kube/config:/home/app/.kube/config:ro" \
74
+ -e HOMELAB_MCP_READONLY=1 \
75
+ whitecapdata-dev
76
+ ```
77
+
65
78
  ## Tools
66
79
 
67
80
  | Tool | Kind | Description |
@@ -4,8 +4,8 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "whitecapdata-dev"
7
- version = "0.1.1"
8
- description = "WhiteCapData-Dev an MCP server to operate a k3s / Kubernetes cluster (health, logs, and guarded restart/scale/delete) straight from your AI agent."
7
+ version = "0.1.3"
8
+ description = "WhiteCapData-Dev — an MCP server to operate a k3s / Kubernetes cluster (health, logs, and guarded restart/scale/delete) straight from your AI agent."
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
11
11
  requires-python = ">=3.11"
@@ -21,7 +21,7 @@ classifiers = [
21
21
  "Topic :: System :: Systems Administration",
22
22
  ]
23
23
  dependencies = [
24
- "mcp>=1.2",
24
+ "mcp>=1.2,<2",
25
25
  "kubernetes>=29",
26
26
  ]
27
27
 
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json",
3
3
  "name": "io.github.Michael-WhiteCapData/WhiteCapData-Dev",
4
- "description": "Operate a k3s / Kubernetes cluster from an MCP client health, logs, and guarded restart/scale/delete, safe by default.",
4
+ "description": "Operate a k3s / Kubernetes cluster from your AI agent — safe-by-default MCP server.",
5
5
  "repository": {
6
6
  "url": "https://github.com/Michael-WhiteCapData/WhiteCapData-Dev",
7
7
  "source": "github"
8
8
  },
9
- "version": "0.1.1",
9
+ "version": "0.1.3",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "pypi",
13
13
  "identifier": "whitecapdata-dev",
14
- "version": "0.1.1",
14
+ "version": "0.1.3",
15
15
  "transport": { "type": "stdio" },
16
16
  "environmentVariables": [
17
17
  {
@@ -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.3"
@@ -0,0 +1,227 @@
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 Annotated, Any
12
+
13
+ from mcp.server.fastmcp import FastMCP
14
+ from pydantic import Field
15
+
16
+ from .config import Config
17
+ from .kube import KubeClient
18
+
19
+ mcp = FastMCP("homelab")
20
+
21
+ _client: KubeClient | None = None
22
+
23
+
24
+ def get_client() -> KubeClient:
25
+ """Lazily build the cluster client (so import never touches a cluster)."""
26
+ global _client
27
+ if _client is None:
28
+ _client = KubeClient(Config.from_env())
29
+ return _client
30
+
31
+
32
+ def set_client(client: KubeClient) -> None:
33
+ """Replace the module-level client (used by tests)."""
34
+ global _client
35
+ _client = client
36
+
37
+
38
+ def _json(data: Any) -> str:
39
+ return json.dumps(data, indent=2, default=str)
40
+
41
+
42
+ # -- reads -------------------------------------------------------------------
43
+
44
+
45
+ @mcp.tool()
46
+ def cluster_summary() -> str:
47
+ """Summarize overall cluster health in a single call.
48
+
49
+ Start here when triaging a cluster: it returns node and pod totals plus the
50
+ full list of unhealthy pods, giving the fastest picture of what is wrong
51
+ before drilling in with `list_pods`, `pod_logs`, or `node_health`. Read-only.
52
+ Returns a JSON object with node counts, pod phase totals, and unhealthy-pod
53
+ details (name, namespace, phase, restart count).
54
+ """
55
+ return _json(get_client().cluster_summary())
56
+
57
+
58
+ @mcp.tool()
59
+ def list_pods(
60
+ namespace: Annotated[
61
+ str,
62
+ Field(
63
+ description="Restrict the listing to this namespace. Empty string "
64
+ "(the default) lists pods across all namespaces."
65
+ ),
66
+ ] = "",
67
+ ) -> str:
68
+ """List pods with phase, restart count, and node, unhealthy pods sorted first.
69
+
70
+ Use after `cluster_summary` to enumerate pods, optionally scoped to one
71
+ namespace, then feed a pod name into `pod_logs`. Read-only. Returns a JSON
72
+ array of pod objects.
73
+ """
74
+ return _json(get_client().list_pods(namespace))
75
+
76
+
77
+ @mcp.tool()
78
+ def list_deployments(
79
+ namespace: Annotated[
80
+ str,
81
+ Field(
82
+ description="Restrict the listing to this namespace. Empty string "
83
+ "(the default) lists deployments across all namespaces."
84
+ ),
85
+ ] = "",
86
+ ) -> str:
87
+ """List deployments with their ready/desired replica counts and namespace.
88
+
89
+ Use to check rollout health or to find a deployment to `restart_deployment`
90
+ or `scale_deployment`. A deployment whose ready count is below its desired
91
+ count is still rolling out or degraded. Read-only. Returns a JSON array.
92
+ """
93
+ return _json(get_client().list_deployments(namespace))
94
+
95
+
96
+ @mcp.tool()
97
+ def list_events(
98
+ limit: Annotated[
99
+ int,
100
+ Field(description="Maximum number of recent events to return, newest first."),
101
+ ] = 30,
102
+ ) -> str:
103
+ """Return recent cluster events, most relevant first (Warnings before Normal).
104
+
105
+ Use to find out *why* something is unhealthy: scheduling failures, image-pull
106
+ errors, failed probes, and OOM kills all surface here. Pair this with
107
+ `cluster_summary` when a pod is failing but the reason is not obvious.
108
+ Read-only. Returns a JSON array of up to `limit` events.
109
+ """
110
+ return _json(get_client().list_events(limit))
111
+
112
+
113
+ @mcp.tool()
114
+ def pod_logs(
115
+ namespace: Annotated[str, Field(description="Namespace the pod runs in, e.g. 'kube-system'.")],
116
+ pod: Annotated[str, Field(description="Exact pod name, as shown by `list_pods`.")],
117
+ tail: Annotated[
118
+ int,
119
+ Field(description="Number of log lines to return from the end of the stream."),
120
+ ] = 200,
121
+ ) -> str:
122
+ """Return the last `tail` lines of a single pod's logs as plain text.
123
+
124
+ Use this to investigate a specific pod after `cluster_summary` or `list_pods`
125
+ flags it as unhealthy (CrashLoopBackOff, restarts, errors). Reads the current
126
+ container's stdout/stderr only — it does not follow/stream or fetch
127
+ previous-container logs. Returns the raw log text, or an error message if the
128
+ pod or namespace does not exist.
129
+ """
130
+ return get_client().pod_logs(namespace, pod, tail)
131
+
132
+
133
+ @mcp.tool()
134
+ def node_health() -> str:
135
+ """Report per-node readiness, kubelet version, capacity, and pressure conditions.
136
+
137
+ Use to diagnose node-level problems — NotReady nodes, or memory/disk/PID
138
+ pressure — when pods are stuck Pending or being evicted. Read-only. Returns a
139
+ JSON array with one object per node.
140
+ """
141
+ return _json(get_client().node_health())
142
+
143
+
144
+ # -- mutations (guarded by read-only + namespace allowlist) ------------------
145
+
146
+
147
+ @mcp.tool()
148
+ def restart_deployment(
149
+ namespace: Annotated[
150
+ str,
151
+ Field(
152
+ description="Namespace of the deployment; must be in the operator's mutable-namespace allowlist."
153
+ ),
154
+ ],
155
+ name: Annotated[str, Field(description="Name of the deployment to restart.")],
156
+ ) -> str:
157
+ """Trigger a rolling restart of a deployment (like `kubectl rollout restart`).
158
+
159
+ Use to recycle a deployment's pods after a config/secret change or to clear a
160
+ stuck state, without changing the replica count. Mutating: rejected unless
161
+ read-only mode is off and the namespace is in the mutable-namespace allowlist.
162
+ Returns a JSON status object describing the triggered rollout.
163
+ """
164
+ return get_client().restart_deployment(namespace, name)
165
+
166
+
167
+ @mcp.tool()
168
+ def scale_deployment(
169
+ namespace: Annotated[
170
+ str,
171
+ Field(
172
+ description="Namespace of the deployment; must be in the operator's mutable-namespace allowlist."
173
+ ),
174
+ ],
175
+ name: Annotated[str, Field(description="Name of the deployment to scale.")],
176
+ replicas: Annotated[
177
+ int,
178
+ Field(description="Target replica count, from 0 up to the operator's configured maximum."),
179
+ ],
180
+ ) -> str:
181
+ """Scale a deployment to a specific replica count.
182
+
183
+ Use to manually scale a workload up or down. Mutating: rejected unless
184
+ read-only mode is off, the namespace is allowlisted, and `replicas` is within
185
+ the operator's configured 0..max bound. Returns a JSON status object with the
186
+ applied replica count.
187
+ """
188
+ return get_client().scale_deployment(namespace, name, replicas)
189
+
190
+
191
+ @mcp.tool()
192
+ def delete_pod(
193
+ namespace: Annotated[
194
+ str,
195
+ Field(description="Namespace of the pod; must be in the operator's mutable-namespace allowlist."),
196
+ ],
197
+ name: Annotated[str, Field(description="Name of the pod to delete.")],
198
+ ) -> str:
199
+ """Delete a single pod so its controller recreates it.
200
+
201
+ Use to force-recreate a wedged pod (managed by a Deployment, StatefulSet,
202
+ etc.) without restarting the whole deployment. Mutating: rejected unless
203
+ read-only mode is off and the namespace is allowlisted. Returns a JSON status
204
+ object.
205
+ """
206
+ return get_client().delete_pod(namespace, name)
207
+
208
+
209
+ @mcp.tool()
210
+ def server_info() -> str:
211
+ """Report the server's effective configuration.
212
+
213
+ Returns the target kube context, the read-only flag, the mutable-namespace
214
+ allowlist, and the maximum replica bound. Use this to confirm what the server
215
+ is permitted to do before attempting a mutation, or to understand why a
216
+ mutating call was rejected. Read-only. Returns a JSON object.
217
+ """
218
+ return _json(get_client().config.as_dict())
219
+
220
+
221
+ def main() -> None:
222
+ """Console-script entry point: run the server over stdio."""
223
+ mcp.run()
224
+
225
+
226
+ if __name__ == "__main__":
227
+ main()
@@ -1,12 +0,0 @@
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.1"
@@ -1,113 +0,0 @@
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()