whitecapdata-dev 0.1.1__tar.gz → 0.1.2__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.
- whitecapdata_dev-0.1.2/.dockerignore +12 -0
- {whitecapdata_dev-0.1.1 → whitecapdata_dev-0.1.2}/CHANGELOG.md +8 -0
- whitecapdata_dev-0.1.2/Dockerfile +29 -0
- {whitecapdata_dev-0.1.1 → whitecapdata_dev-0.1.2}/PKG-INFO +14 -1
- {whitecapdata_dev-0.1.1 → whitecapdata_dev-0.1.2}/README.md +13 -0
- {whitecapdata_dev-0.1.1 → whitecapdata_dev-0.1.2}/pyproject.toml +1 -1
- {whitecapdata_dev-0.1.1 → whitecapdata_dev-0.1.2}/server.json +3 -3
- {whitecapdata_dev-0.1.1 → whitecapdata_dev-0.1.2}/src/homelab_mcp/__init__.py +1 -1
- whitecapdata_dev-0.1.2/src/homelab_mcp/server.py +227 -0
- whitecapdata_dev-0.1.2/uv.lock +1790 -0
- whitecapdata_dev-0.1.1/src/homelab_mcp/server.py +0 -113
- {whitecapdata_dev-0.1.1 → whitecapdata_dev-0.1.2}/.github/workflows/ci.yml +0 -0
- {whitecapdata_dev-0.1.1 → whitecapdata_dev-0.1.2}/.github/workflows/publish-mcp.yml +0 -0
- {whitecapdata_dev-0.1.1 → whitecapdata_dev-0.1.2}/.github/workflows/release.yml +0 -0
- {whitecapdata_dev-0.1.1 → whitecapdata_dev-0.1.2}/.gitignore +0 -0
- {whitecapdata_dev-0.1.1 → whitecapdata_dev-0.1.2}/CONTRIBUTING.md +0 -0
- {whitecapdata_dev-0.1.1 → whitecapdata_dev-0.1.2}/LICENSE +0 -0
- {whitecapdata_dev-0.1.1 → whitecapdata_dev-0.1.2}/src/homelab_mcp/config.py +0 -0
- {whitecapdata_dev-0.1.1 → whitecapdata_dev-0.1.2}/src/homelab_mcp/format.py +0 -0
- {whitecapdata_dev-0.1.1 → whitecapdata_dev-0.1.2}/src/homelab_mcp/kube.py +0 -0
- {whitecapdata_dev-0.1.1 → whitecapdata_dev-0.1.2}/tests/conftest.py +0 -0
- {whitecapdata_dev-0.1.1 → whitecapdata_dev-0.1.2}/tests/test_config.py +0 -0
- {whitecapdata_dev-0.1.1 → whitecapdata_dev-0.1.2}/tests/test_format.py +0 -0
- {whitecapdata_dev-0.1.1 → whitecapdata_dev-0.1.2}/tests/test_kube.py +0 -0
- {whitecapdata_dev-0.1.1 → whitecapdata_dev-0.1.2}/tests/test_server_tools.py +0 -0
|
@@ -4,6 +4,14 @@ 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.2] - 2026-06-22
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- `Dockerfile` and `.dockerignore` for running the server as a container (MCP over stdio).
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- Enriched every tool docstring and documented all parameters for clearer agent tool-selection.
|
|
14
|
+
|
|
7
15
|
## [0.1.0] - 2026-06-20
|
|
8
16
|
|
|
9
17
|
### 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,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: whitecapdata-dev
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
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
|
|
@@ -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,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "whitecapdata-dev"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.2"
|
|
8
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" }
|
|
@@ -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
|
|
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.
|
|
9
|
+
"version": "0.1.2",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "pypi",
|
|
13
13
|
"identifier": "whitecapdata-dev",
|
|
14
|
-
"version": "0.1.
|
|
14
|
+
"version": "0.1.2",
|
|
15
15
|
"transport": { "type": "stdio" },
|
|
16
16
|
"environmentVariables": [
|
|
17
17
|
{
|
|
@@ -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()
|