MCP server for managing a MicroK8s cluster from the Claude CLI
Project description
An MCP server that lets the claude CLI operate a MicroK8s cluster: inspect
workloads, read logs, triage failures, and โ when you unlock it โ scale, apply
manifests, restart rollouts, and manage addons.
The server is read-only by default, and every mutating capability sits behind a separate switch.
๐ Full documentation โ install guide ยท configuration ยท tool reference ยท security model ยท troubleshooting
[!WARNING]
rbac.yamlin this repo grants cluster-wide write. Theclaude-mcp-operatorClusterRoleBinding is active, sokubectl apply -f rbac.yamlgives the ServiceAccountcreate/update/patch/deleteon workloads in every namespace, plus node cordon/drain and pod eviction. Secrets stay excluded, and the credential cannot escalate its own permissions.If you want read-only, comment out that binding before applying โ and if you already applied it,
applywill not remove the binding for you:kubectl delete clusterrolebinding claude-mcp-operatorTo narrow instead of removing, replace the ClusterRoleBinding with a
RoleBindingper namespace. That is the only limit the API server enforces;MICROK8S_MCP_NAMESPACESis what this server is willing to do, not what the credential is able to do. Audit the real answer with:kubectl --kubeconfig ~/.kube/claude-mcp.kubeconfig auth can-i --list
Topologies
The server has two backends and you can use either or both.
kubectl backend talks to the API server over the network using a
kubeconfig. This drives every kubectl-level tool.
node backend runs microk8s ... on the node itself, for addon management
and snap-level status. It runs locally if the server is on the node, otherwise
over SSH.
A) workstation microk8s node
claude CLI โโ stdio โโ mcp server โโ :16443 โโ kube-apiserver
โโ ssh โโโโโ microk8s snap
B) workstation microk8s node
claude CLI โโ ssh โโโโโโโโโโโโโโโโโโโโโโโโโโโโ mcp server โโ localhost
Topology A is the better default: the MCP server runs on your workstation,
holds a scoped kubeconfig, and only needs SSH for addon commands. If you skip
the SSH config, everything still works except microk8s_status and
manage_addon.
Install
On whichever machine runs the server:
git clone <this-repo> microk8s-mcp && cd microk8s-mcp
make install
That is an editable install into ./.venv โ the venv links back to the
checkout, so edits are live and the source must stay put. For an install that
outlives the checkout, put the venv somewhere stable and copy the package in:
make install VENV=~/.local/share/microk8s-mcp/venv EDITABLE=0
EDITABLE=0 copies rather than links, so the repo can later be moved or
deleted. The cost is that source edits need a reinstall; the target
force-reinstalls every run, because pip would otherwise treat the same version
as already satisfied and leave stale code in place while reporting success.
Pass the same VENV= to check, register and register-rw.
kubectl needs to be on PATH for the kubectl backend. If it isn't, the server
falls back to microk8s kubectl via the node backend โ but note that fallback
runs under MicroK8s' own admin credential, so the scoped kubeconfig and the
bundled reader role stop applying. The server logs a warning when it happens.
Install kubectl locally if you want the RBAC scoping to mean anything;
make register then bakes its absolute path in as MICROK8S_MCP_KUBECTL.
make help lists every setup target; make doctor reports on prerequisites.
Step-by-step instructions, including both topologies, are in install.txt.
Give it a scoped identity
Don't hand it your admin kubeconfig.
If the node is another machine โ the usual case, and topology A โ do it all from your workstation over SSH:
make node-setup SSH_HOST=pierre@k8s.lan
That applies rbac.yaml on the node, mints the kubeconfig there, and writes it
to ~/.kube/claude-mcp.kubeconfig mode 600 on this machine. Nothing is
installed on the node.
APISERVER defaults to https://<node>:16443, where the node is SSH_HOST put
through ssh -G โ so an ~/.ssh/config alias resolves to the address kubectl
will actually need, since kubectl doesn't read that file. Override it when the
node answers on a different address than you reach it at. The address is
validated before anything is minted: unresolvable is a hard stop, unreachable
is a warning. make node-check SSH_HOST=... runs those preconditions without
changing anything.
Sitting on the node already? The same two steps, locally:
make rbac
make kubeconfig APISERVER=https://k8s.lan:16443
Or without make:
microk8s kubectl apply -f rbac.yaml
APISERVER=https://k8s.lan:16443 bash make-kubeconfig.sh > claude-mcp.kubeconfig
In that case copy the file to the machine running the server and chmod 600 it.
The bundled reader role grants get/list/watch across the cluster except Secrets โ deliberately, so cluster credentials can never end up in a model context.
As noted at the top, the claude-mcp-operator binding is active in this repo,
so applying rbac.yaml unchanged grants cluster-wide write. Three ways to shape
that grant, in descending order of how much they actually protect you:
| Approach | Enforced by | Effect |
|---|---|---|
Comment out the claude-mcp-operator binding |
API server | No writes at all |
Replace it with per-namespace RoleBindings |
API server | Writes only in the namespaces you name |
MICROK8S_MCP_NAMESPACES=apps,staging |
This server | Server declines elsewhere; credential could still do it |
Only the first two constrain the credential itself. The third is what the server is willing to do โ useful, but it is not a security boundary. Set both layers.
Register with the Claude CLI
make register SSH_HOST=user@host and make register-rw SSH_HOST=user@host NAMESPACES=apps,staging wrap the two commands below with absolute paths filled
in โ including MICROK8S_MCP_KUBECTL, which matters because Claude Code
launches the server with its own PATH.
register-rw takes four switches, and prints the resulting grant before it
acts:
| Variable | Default | Effect |
|---|---|---|
RW_SERVER_NAME |
microk8s-rw |
Pass microk8s to replace the read-only entry instead of running both |
ALLOW_ADDON_CHANGES |
true |
manage_addon may enable/disable addons โ snap-level root on the node, outside RBAC entirely |
ALLOW_NODE_OPS |
false |
node_maintenance may cordon/drain. On a single-node cluster a drain evicts everything |
ALLOW_PROTECTED_WRITES |
false |
Permits writes to kube-system, kube-public, kube-node-lease, default |
make register-rw SSH_HOST=user@host NAMESPACES=apps,staging ALLOW_NODE_OPS=true
The long forms, if you'd rather be explicit:
Read-only, which is where you should start:
claude mcp add --transport stdio microk8s \
--env MICROK8S_MCP_KUBECONFIG=/home/pierre/.kube/claude-mcp.kubeconfig \
--env MICROK8S_MCP_SSH_HOST=pierre@k8s.lan \
-- /home/pierre/microk8s-mcp/.venv/bin/microk8s-mcp
Full management, scoped to a couple of namespaces:
claude mcp add --transport stdio microk8s \
--env MICROK8S_MCP_KUBECONFIG=/home/pierre/.kube/claude-mcp.kubeconfig \
--env MICROK8S_MCP_SSH_HOST=pierre@k8s.lan \
--env MICROK8S_MCP_MODE=read-write \
--env MICROK8S_MCP_NAMESPACES=apps,staging \
--env MICROK8S_MCP_ALLOW_ADDON_CHANGES=true \
-- /home/pierre/microk8s-mcp/.venv/bin/microk8s-mcp
All flags go before the server name; -- separates them from the command.
Use --scope user (or make register SCOPE=user) to make it available in every
project rather than just the current directory. Verify with claude mcp list,
then /mcp inside a session to see the tool inventory.
If claude mcp list doesn't show it, check these before assuming it's
broken:
- Scope. A local-scope entry only resolves when your working directory is
inside that project. Run
claude mcp get microk8sto see where it landed. - Restart. Claude Code loads MCP servers at session start, so a server
registered mid-session won't appear in
/mcpuntil you restart โ regardless of scope.
A user-scope entry pointing into a repo .venv is worth avoiding: deleting or
moving that checkout then breaks the server in every project at once. Pair
SCOPE=user with an out-of-repo EDITABLE=0 install.
Two servers side by side works well: register a microk8s read-only entry
pointed at everything, and a microk8s-rw entry scoped to the namespaces you
actually deploy to.
Configuration
| Variable | Default | Purpose |
|---|---|---|
MICROK8S_MCP_MODE |
read-only |
Set read-write to unlock mutating tools |
MICROK8S_MCP_KUBECONFIG |
(kubectl default) | Path to the scoped kubeconfig |
MICROK8S_MCP_CONTEXT |
(current) | kubeconfig context to use |
MICROK8S_MCP_KUBECTL |
kubectl |
Path to the kubectl binary |
MICROK8S_MCP_SSH_HOST |
(unset) | user@host for the node backend |
MICROK8S_MCP_SSH_KEY |
(agent/default) | Identity file for SSH |
MICROK8S_MCP_NAMESPACES |
* |
Comma-separated namespace allowlist |
MICROK8S_MCP_PROTECTED_NAMESPACES |
kube-system,kube-public,kube-node-lease,default |
Readable, but writes blocked |
MICROK8S_MCP_ALLOW_PROTECTED_WRITES |
false |
Permit writes to protected namespaces |
MICROK8S_MCP_ALLOW_ADDON_CHANGES |
false |
Permit enable/disable of addons |
MICROK8S_MCP_ALLOW_NODE_OPS |
false |
Permit cordon/uncordon/drain |
MICROK8S_MCP_TIMEOUT |
60 |
Per-command timeout, seconds |
MICROK8S_MCP_MAX_OUTPUT |
24000 |
Output clipped past this many chars |
MICROK8S_MCP_LOG_LEVEL |
INFO |
Every command is logged to stderr |
Tools
Read โ cluster_overview, list_resources, get_resource,
describe_resource, get_logs, get_events, top, api_resources,
rollout_status, microk8s_status
Write โ apply_manifest (server-side dry run by default, returns a diff),
scale_workload, rollout_restart, delete_resource (single named object,
requires confirm=True), manage_addon, node_maintenance
Prompts โ triage_namespace, health_report
Security notes
A few things are omitted on purpose:
- No
kubectl exec, no port-forward, no raw shell. An exec tool would be arbitrary code execution inside your cluster driven by model output. If you want it, add it yourself and understand what you're accepting. - No Secret reads in the bundled RBAC role.
- No bulk deletes.
delete_resourcetakes one kind and one name; there is no--alland no selector path. - No shell interpolation. Commands are built as argv lists, never strings;
kinds, names and namespaces are regex-validated so nothing can be smuggled in
as a kubectl flag. Remote SSH commands are
shlex.quoted per argument.
The layers are independent: RBAC is what the credential is able to do, and the
env vars are what this server is willing to do. Set both. If the token is
bound only to the reader role, MICROK8S_MCP_MODE=read-write still can't hurt
you โ the API server refuses.
The asymmetry matters, and it only runs one way. A narrow RBAC binding makes a
permissive env var harmless. A narrow env var does not make a permissive
binding safe: MICROK8S_MCP_NAMESPACES=apps against a cluster-wide
ClusterRoleBinding still leaves a token that can write anywhere, and anything
else holding that file ignores this server's opinion entirely. Audit the layer
that is actually enforced:
kubectl --kubeconfig ~/.kube/claude-mcp.kubeconfig auth can-i --list
kubectl --kubeconfig ~/.kube/claude-mcp.kubeconfig auth can-i list secrets -A # expect: no
One more path worth knowing: if the server can't find a local kubectl, it
falls back to microk8s kubectl on the node, which runs under that node's
admin credential โ the scoped kubeconfig and both roles stop applying. It
logs a warning when this happens. MICROK8S_MCP_KUBECTL with an absolute path
(what make register sets) keeps you on the scoped path.
Every command executed is logged to stderr. Capture it if you want an audit trail:
--env MICROK8S_MCP_LOG_LEVEL=INFO
Troubleshooting
Server shows disconnected in claude mcp list โ Claude Code launches
subprocesses with a different environment than your shell. Use absolute paths
to the venv binary, as in the examples above.
connection refused on :16443 โ MicroK8s only binds the API server to
addresses in its certificate SANs. If you're connecting by hostname or a
non-primary IP, add it to /var/snap/microk8s/current/certs/csr.conf.template
and run microk8s refresh-certs --cert server.crt.
microk8s: command not found over SSH โ the snap bin directory isn't in a
non-interactive shell's PATH. Point at it directly:
--env MICROK8S_MCP_MICROK8S=/snap/bin/microk8s.
metrics-server errors from top โ enable the addon:
microk8s enable metrics-server.
Development
make dev # install with the dev extras (pytest, ruff)
make ci # lint + tests + dependency audit, exactly what CI runs
make test runs pytest from / on purpose: python -c and pytest both prepend
the working directory to sys.path, so running from the repo root imports the
local source and would pass even with nothing installed in the venv. The same
reasoning applies to make check, which also prints resolved from: so you can
see which copy of the code answered.
The suite covers the places where a bug is dangerous rather than merely wrong:
argument injection through _tok/_ns (a name of --all must never reach
kubectl), read-only mode refusing every mutating tool before it shells out,
delete_resource requiring confirm=True, and backend selection โ a
mis-selected backend silently runs under the node's admin credential instead of
the scoped one.
CI
| Workflow | Trigger | Does |
|---|---|---|
ci.yml |
push to main, PRs |
ruff, pytest on Python 3.10โ3.13, build + wheel smoke test, pip-audit |
release.yml |
pushing a v* tag |
verifies tag matches pyproject version, re-runs lint and tests, builds, creates a GitHub Release with sdist + wheel |
Cut a release with git tag v0.2.0 && git push origin v0.2.0. The tag/version
check runs first, so a mismatched tag fails before anything is published.
Dependabot watches pip and github-actions weekly; alerts and automated
security fixes are enabled on the repo.
License
MIT โ see LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file microk8s_mcp-0.1.1.tar.gz.
File metadata
- Download URL: microk8s_mcp-0.1.1.tar.gz
- Upload date:
- Size: 35.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a17e36aae2fc687b3bbae2d9e58e12d88e0393d715b1d055b276052d5eb0ae17
|
|
| MD5 |
ed1c4c8ca65932eff6f7ade2ad6752ee
|
|
| BLAKE2b-256 |
abc378b454f09dfaedfdd0318edf31de8df5f8178bb63c76287f950dcb510143
|
Provenance
The following attestation bundles were made for microk8s_mcp-0.1.1.tar.gz:
Publisher:
release.yml on pierrejochem/microk8s-mcp
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
microk8s_mcp-0.1.1.tar.gz -
Subject digest:
a17e36aae2fc687b3bbae2d9e58e12d88e0393d715b1d055b276052d5eb0ae17 - Sigstore transparency entry: 2248263295
- Sigstore integration time:
-
Permalink:
pierrejochem/microk8s-mcp@cdebfdf69978522e584c50c9f551a54b6ac16194 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/pierrejochem
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@cdebfdf69978522e584c50c9f551a54b6ac16194 -
Trigger Event:
push
-
Statement type:
File details
Details for the file microk8s_mcp-0.1.1-py3-none-any.whl.
File metadata
- Download URL: microk8s_mcp-0.1.1-py3-none-any.whl
- Upload date:
- Size: 15.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2befb3edfff2798d0954693f9a5ba74e5e3f8a4680e66e465f318d16dc4d4977
|
|
| MD5 |
e12117f0dcd22d0a4445bceaa2d4817e
|
|
| BLAKE2b-256 |
66ab3719634444b45e9ba99f01fe8b4e63cbf175ffb18c3284c402b5db5be22d
|
Provenance
The following attestation bundles were made for microk8s_mcp-0.1.1-py3-none-any.whl:
Publisher:
release.yml on pierrejochem/microk8s-mcp
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
microk8s_mcp-0.1.1-py3-none-any.whl -
Subject digest:
2befb3edfff2798d0954693f9a5ba74e5e3f8a4680e66e465f318d16dc4d4977 - Sigstore transparency entry: 2248263703
- Sigstore integration time:
-
Permalink:
pierrejochem/microk8s-mcp@cdebfdf69978522e584c50c9f551a54b6ac16194 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/pierrejochem
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@cdebfdf69978522e584c50c9f551a54b6ac16194 -
Trigger Event:
push
-
Statement type: