Skip to main content

MCP server for managing a MicroK8s cluster from the Claude CLI

Project description

microk8s-mcp โ€” Model Context Protocol server

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.yaml in this repo grants cluster-wide write. The claude-mcp-operator ClusterRoleBinding is active, so kubectl apply -f rbac.yaml gives the ServiceAccount create/update/patch/delete on 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, apply will not remove the binding for you:

kubectl delete clusterrolebinding claude-mcp-operator

To narrow instead of removing, replace the ClusterRoleBinding with a RoleBinding per namespace. That is the only limit the API server enforces; MICROK8S_MCP_NAMESPACES is 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 microk8s to see where it landed.
  • Restart. Claude Code loads MCP servers at session start, so a server registered mid-session won't appear in /mcp until 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_resource takes one kind and one name; there is no --all and 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

microk8s_mcp-0.1.1.tar.gz (35.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

microk8s_mcp-0.1.1-py3-none-any.whl (15.4 kB view details)

Uploaded Python 3

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

Hashes for microk8s_mcp-0.1.1.tar.gz
Algorithm Hash digest
SHA256 a17e36aae2fc687b3bbae2d9e58e12d88e0393d715b1d055b276052d5eb0ae17
MD5 ed1c4c8ca65932eff6f7ade2ad6752ee
BLAKE2b-256 abc378b454f09dfaedfdd0318edf31de8df5f8178bb63c76287f950dcb510143

See more details on using hashes here.

Provenance

The following attestation bundles were made for microk8s_mcp-0.1.1.tar.gz:

Publisher: release.yml on pierrejochem/microk8s-mcp

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for microk8s_mcp-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 2befb3edfff2798d0954693f9a5ba74e5e3f8a4680e66e465f318d16dc4d4977
MD5 e12117f0dcd22d0a4445bceaa2d4817e
BLAKE2b-256 66ab3719634444b45e9ba99f01fe8b4e63cbf175ffb18c3284c402b5db5be22d

See more details on using hashes here.

Provenance

The following attestation bundles were made for microk8s_mcp-0.1.1-py3-none-any.whl:

Publisher: release.yml on pierrejochem/microk8s-mcp

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page