NAME
MCP::K8s - MCP Server for Kubernetes with RBAC-aware dynamic tools
VERSION
version 0.003
SYNOPSIS
# Start the MCP server on stdio (for Claude Desktop, Claude Code, etc.)
use MCP::K8s;
MCP::K8s->run_stdio;
# Or use the included script:
$ mcp-k8s
# Configure via environment variables:
$ export MCP_K8S_CONTEXT="my-cluster"
$ export MCP_K8S_NAMESPACES="default,production"
$ mcp-k8s
# Direct token authentication:
$ export MCP_K8S_TOKEN="eyJhbGci..."
$ export MCP_K8S_SERVER="https://my-cluster:6443"
$ mcp-k8s
# In-cluster: auto-detects when running as a Kubernetes pod
# Programmatic usage with custom API:
use MCP::K8s;
my $k8s = MCP::K8s->new(
api => $my_kubernetes_rest_instance,
namespaces => ['default', 'staging'],
);
$k8s->to_stdio;
# Embedded, with a kubeconfig outside the default location:
my $k8s = MCP::K8s->new(
kubeconfig_path => '/etc/myapp/clusters/prod/kubeconfig',
context_name => 'prod',
);
DESCRIPTION
MCP::K8s provides an MCP (Model Context Protocol) server that gives AI assistants like Claude access to Kubernetes clusters.
The key innovation: the server dynamically discovers what the connected service account can do via RBAC and only exposes those capabilities as MCP tools. A read-only service account gets read-only tools; a cluster-admin gets everything. Tool descriptions include the specific resources and namespaces available, so the LLM always knows exactly what it can do.
How it works
- 1. Register — Registers the MCP tools at construction; no cluster connection needed yet
- 2. Connect — On the first request, authenticates via direct token, in-cluster service account, or kubeconfig
- 3. Discover — Submits
SelfSubjectRulesReviewrequests to discover RBAC permissions per namespace, then rewrites the tool descriptions to reflect them - 4. Serve — Answers MCP requests over any transport (stdio, HTTP, embedded via Net::Async::MCP), checking permissions on every tool call
Why generic tools?
Kubernetes has 50+ built-in resource types plus unlimited Custom Resources. Instead of creating hundreds of specific tools (list_pods, get_deployment, delete_configmap...), MCP::K8s uses 10 generic tools with a resource parameter — the same pattern as kubectl get, kubectl delete, etc. This keeps the tool count manageable for MCP clients while supporting every resource type including CRDs.
Built on top of Kubernetes::REST (API client), IO::K8s (typed objects), and MCP::Server (protocol implementation).
context_name
Optional. Kubeconfig context name to use. Read from $ENV{MCP_K8S_CONTEXT} by default. If not set, the kubeconfig's current-context is used.
kubeconfig_path
Optional. Path to the kubeconfig file used by the kubeconfig auth tier, passed through to "kubeconfig_path" in Kubernetes::REST::Kubeconfig. For an embedder that keeps its kubeconfigs outside the default location: with a same-named context in ~/.kube/config, only the explicit path tells the two apart. Accepts whatever Kubeconfig accepts there — a single path, a KUBECONFIG-style path list, or an arrayref. Not read from the environment; if not set, Kubeconfig does its own lookup ($ENV{KUBECONFIG}, then ~/.kube/config). Has no effect when "token" or the in-cluster service account engages an earlier tier.
token
Optional. Bearer token for direct authentication. Read from $ENV{MCP_K8S_TOKEN} by default. When set, bypasses kubeconfig entirely and connects using this token directly.
server_endpoint
Optional. Kubernetes API server URL. Read from $ENV{MCP_K8S_SERVER} by default. Used with "token" for direct authentication, or with in-cluster auth. Defaults to https://kubernetes.default.svc.cluster.local when running in-cluster.
namespaces
ArrayRef of namespace names to operate on. Configured via:
$ENV{MCP_K8S_NAMESPACES}— comma-separated list (e.g."default,production")Auto-discovery — lists all namespaces from the cluster
In-cluster fallback — the service account's mounted namespace, if listing fails
Fallback —
['default']if discovery fails outside a cluster
api
Kubernetes::REST instance for cluster communication. Built automatically from kubeconfig using Kubernetes::REST::Kubeconfig. Can be provided directly for testing or custom configurations.
permissions
MCP::K8s::Permissions instance holding the discovered RBAC permissions. Built and populated automatically on first access via SelfSubjectRulesReview.
json
JSON::MaybeXS encoder instance. Configured with utf8, pretty, canonical, and convert_blessed for consistent, readable output.
server
Returns $self for backward compatibility. Since MCP::K8s now inherits from MCP::Server, the object itself is the server.
_resource_plural
my $plural = $self->_resource_plural('Pod'); # => 'pods'
my $plural = $self->_resource_plural('Ingress'); # => 'ingresses'
Convert a Kubernetes Kind name (e.g. Pod, Deployment) to its plural form used in RBAC rules (e.g. pods, deployments). Uses a 4-tier lookup:
- 1. Static
%RESOURCE_PLURALSmap (fast, zero-cost) - 2.
resource_plural()on the IO::K8s class the Kind resolves to via the client'sexpand_class - 3. API server discovery cache (lazy, one-time query)
- 4. Heuristic fallback (lowercase + simple pluralization)
Tier 2 answers every built-in Kind: IO::K8s carries the upstream plurals on its classes, and Kubernetes::REST's expand_class resolves a name its built-in map knows without a request, even when resource_map_from_cluster is set. It also answers for a CRD provider's class registered with the client, which brings its own resource_plural().
For a Kind nothing ships, expand_class consults the client's aggregated discovery (GET /api, GET /apis) — the same resolution the tool's own API call does right after, so this tier moves that cost rather than adding it. Such a Kind resolves to IO::K8s::Unstructured or to a class name that does not load; neither has resource_plural(), so the lookup falls through.
Tier 3 is therefore the source for CRDs without a class: it asks /api/v1 and /apis and gets back the plurals the API server itself uses.
_discover_resource_plurals
$self->_discover_resource_plurals;
Query the API server's discovery endpoints (/api/v1 and /apis) to build a Kind-to-plural mapping. Results are cached in _resource_plurals_cache. Failures are silently ignored — the cache simply remains empty and callers fall through to heuristic pluralization.
_resolve_namespace
my $ns = $self->_resolve_namespace($args);
Resolve the namespace for a tool call. If $args->{namespace} is provided, uses that. Otherwise, if only one namespace is accessible, auto-fills it. Returns undef if the namespace cannot be determined (the tool should handle this case).
_is_conflict_error
if ($self->_is_conflict_error("$@")) { ... }
Decide whether a failed create failed because the resource already exists (HTTP 409 / AlreadyExists), which is the signal for "k8s_apply" to fall back to a patch.
This is a string match, deliberately. Kubernetes::REST 1.108 does not raise a typed exception for API errors: create runs the response through _check_response, which croaks with
Kubernetes API error (create <class>): <status> <body>
Kubernetes::REST::Error and Kubernetes::REST::RemoteError still exist, but only as compatibility helpers for the deprecated v0 API — no code path in 1.108 throws them, so there is no ->code to check. The match stays wide (status code or reason) so that it survives a reworded error as long as either token is still in it; narrowing it to the exact croak format above would trade one fragility for a worse one.
Revisit when the client grows typed errors.
_format_resource_summary
my $summary = $self->_format_resource_summary($io_k8s_object);
Extract a concise summary hashref from an IO::K8s object, suitable for LLM consumption. Includes metadata (name, namespace, labels, creation time), kind, status fields (phase, replicas, conditions), and key spec fields (containers, ports, type).
The summary is intentionally compact — for full details, the k8s_get tool with output => 'json' should be used.
_format_list
my $summaries = $self->_format_list($list->items);
Format an arrayref of IO::K8s objects into an arrayref of summary hashrefs using "_format_resource_summary".
_tools
Override of "_tools" in MCP::Server. Enriches the tool descriptions with the discovered RBAC permissions before handing the list to the caller, then defers to the parent implementation.
_tools is the single funnel every transport passes through for tools/list (and tools/call), so this is the one place that covers stdio, the inherited HTTP transport, and an embedded server driven through Net::Async::MCP alike. Hooking anywhere else — as run_stdio alone used to — leaves every other path serving the static descriptions, and the descriptions are what tell the LLM which resources exist per namespace.
Discovery stays lazy: the first ->api access happens here, on the first request, not in BUILD. _update_tool_descriptions is idempotent via its own guard, so the per-request cost after the first one is a boolean.
MCP TOOLS
All tools are registered on the "server" during construction. Each tool checks RBAC permissions before executing and returns clear error messages on denial. Tool descriptions dynamically include which resources and namespaces are available.
k8s_permissions
Show what the current Kubernetes service account is allowed to do. Returns a Markdown-formatted RBAC summary. The LLM should call this first to understand its capabilities.
No parameters required.
k8s_list
List Kubernetes resources with optional filtering.
Parameters:
resource(string, required) — Resource type:Pod,Deployment,Service,ConfigMap, etc.namespace(string) — Target namespace. Auto-detected if only one is accessible.label_selector(string) — Label filter, e.g.app=web,env=prodfield_selector(string) — Field filter, e.g.status.phase=Running
Returns JSON with count and items (array of resource summaries).
k8s_get
Get a single Kubernetes resource by name.
Parameters:
resource(string, required) — Resource typename(string, required) — Resource namenamespace(string) — Target namespaceoutput(string) — Format:summary(default),json, oryaml
k8s_create
Create a Kubernetes resource from a manifest. The apiVersion and kind fields are auto-populated from the resource type via IO::K8s.
Parameters:
resource(string, required) — Resource typemanifest(object, required) — Resource manifest (metadata, spec, etc.)namespace(string) — Target namespace (also auto-populated in metadata)
Returns JSON confirmation with the created resource name.
k8s_patch
Partially update a Kubernetes resource.
Parameters:
resource(string, required) — Resource typename(string, required) — Resource namepatch(object, required) — Fields to changenamespace(string) — Target namespacepatch_type(string) — Strategy:strategic(default;mergewithsubresource),merge, orjsonsubresource(string) —statusto write the status subresource
See "patch" in Kubernetes::REST for details on patch strategies.
Status writes need subresource => 'status'. Once a resource has a status subresource — every CRD declaring one, plus the built-ins that always had one — the API server strips status from any write to the main endpoint and still answers 2xx. Without this parameter the patch reports success and stores nothing. With it, the call goes to "patch_status" in Kubernetes::REST, which addresses /status, and the patch type defaults to merge rather than strategic, because custom resources answer a strategic merge patch with 415. An explicit patch_type still wins.
The status subresource is a separate resource in RBAC as well: this path requires patch on <plural>/status, not on <plural>.
k8s_delete
Delete a Kubernetes resource by name.
Parameters:
resource(string, required) — Resource typename(string, required) — Resource namenamespace(string) — Target namespace
k8s_logs
Get container logs from a pod. Essential for debugging. Reads the pod's log subresource through Kubernetes::REST's log().
Parameters:
name(string, required) — Pod namenamespace(string) — Target namespace (required for logs)container(string) — Container name (required for multi-container pods)tail_lines(integer) — Number of lines from end (default: 100)previous(boolean) — Get logs from previous container instance
k8s_events
Get Kubernetes events for debugging. Supports filtering by involved object name and arbitrary field selectors.
Parameters:
namespace(string) — Target namespaceinvolved_object(string) — Filter events by object name (e.g. pod name)field_selector(string) — Field selector filter (e.g.reason=BackOff)
Returns JSON with count and items (array of event summaries).
k8s_rollout_restart
Trigger a rolling restart of a Deployment, StatefulSet, or DaemonSet. Works by patching the pod template annotation kubectl.kubernetes.io/restartedAt with the current timestamp — the same mechanism as kubectl rollout restart.
Parameters:
resource(string, required) — One ofDeployment,StatefulSet,DaemonSetname(string, required) — Resource namenamespace(string) — Target namespace
k8s_apply
Create or update a Kubernetes resource (like kubectl apply). Tries to create the resource first; if it already exists (409 Conflict), falls back to a strategic merge patch.
Parameters:
resource(string, required) — Resource type (e.g.Deployment,ConfigMap)manifest(object, required) — Resource manifest. Must includemetadata.name.namespace(string) — Target namespace
Returns JSON confirmation with the action taken (created or updated).
run_stdio
# As class method:
MCP::K8s->run_stdio;
# As instance method:
my $k8s = MCP::K8s->new(%opts);
$k8s->run_stdio;
Start the MCP server on stdio. If called as a class method, creates a new instance first. This is the main entry point used by the mcp-k8s script.
Discovery is run here, before the transport starts. "_tools" would do it on the first tools/list anyway, so this is not what makes the descriptions correct — it is what makes an unreachable cluster fail at process start, on stderr, instead of turning into a JSON-RPC internal error once a client is already attached.
ENVIRONMENT
KUBECONFIG-
Path to kubeconfig file. Default:
~/.kube/config. Standard Kubernetes environment variable, also used bykubectl. MCP_K8S_CONTEXT-
Kubeconfig context to use. Default: the kubeconfig's
current-context. MCP_K8S_TOKEN-
Bearer token for direct authentication. Bypasses kubeconfig entirely. Useful for CI/CD pipelines or when you have a service account token.
MCP_K8S_SERVER-
Kubernetes API server URL. Used with
MCP_K8S_TOKENor in-cluster auth. Default when in-cluster:https://kubernetes.default.svc.cluster.local. MCP_K8S_NAMESPACES-
Comma-separated list of namespaces to operate on. Default: auto-discovered from the cluster (lists all namespaces the service account can see). Falls back to
defaultif discovery fails.
AUTHENTICATION
MCP::K8s supports three authentication methods, tried in order:
- 1. Direct token — Set
MCP_K8S_TOKEN(and optionallyMCP_K8S_SERVER) - 2. In-cluster — Auto-detected when running as a Kubernetes pod (reads mounted service account token from
/var/run/secrets/kubernetes.io/serviceaccount/token) - 3. Kubeconfig — Reads
~/.kube/config(or$KUBECONFIG), optionally filtered byMCP_K8S_CONTEXT. An embedder names a specific file with the "kubeconfig_path" constructor argument.
For in-cluster and direct token auth, the CA certificate at /var/run/secrets/kubernetes.io/serviceaccount/ca.crt is automatically used if present.
RBAC SETUP
Use a dedicated ServiceAccount with minimal permissions for AI access. Example RBAC manifests are included in the examples/ directory:
examples/readonly-serviceaccount.yaml— Read-only access (recommended starting point)examples/deployer-serviceaccount.yaml— Read + deploy/restart capabilitiesexamples/full-ops-serviceaccount.yaml— Full access except secrets
RBAC is the single source of truth — if the service account shouldn't have access, don't grant it via RBAC. MCP::K8s does not implement application-layer permission filtering.
CLAUDE DESKTOP INTEGRATION
Add this to your Claude Desktop MCP configuration (~/.config/claude/claude_desktop_config.json):
{
"mcpServers": {
"kubernetes": {
"command": "mcp-k8s",
"env": {
"MCP_K8S_CONTEXT": "my-cluster",
"MCP_K8S_NAMESPACES": "default,production"
}
}
}
}
CLAUDE CODE INTEGRATION
Add to your project's .mcp.json or global MCP settings:
{
"mcpServers": {
"kubernetes": {
"command": "mcp-k8s",
"env": {
"MCP_K8S_CONTEXT": "dev-cluster"
}
}
}
}
LANGERTHA RAIDER INTEGRATION
Use Langertha::Raider to build an autonomous AI agent that can interact with your Kubernetes cluster using MCP::K8s as its tool source:
use IO::Async::Loop;
use Future::AsyncAwait;
use Net::Async::MCP;
use Langertha::Engine::Anthropic;
use Langertha::Raider;
use MCP::K8s;
my $k8s = MCP::K8s->new(
namespaces => ['default', 'production'],
);
my $loop = IO::Async::Loop->new;
my $mcp = Net::Async::MCP->new(server => $k8s->server);
$loop->add($mcp);
async sub main {
await $mcp->initialize;
my $engine = Langertha::Engine::Anthropic->new(
api_key => $ENV{ANTHROPIC_API_KEY},
model => 'claude-sonnet-4-6',
mcp_servers => [$mcp],
);
my $raider = Langertha::Raider->new(
engine => $engine,
mission => 'You are a Kubernetes operations assistant. '
. 'Always check permissions first, then help the user '
. 'investigate and manage their cluster.',
);
# First raid: discover capabilities
my $r1 = await $raider->raid_f('What can I do on this cluster?');
say $r1;
# Second raid: uses history from the first
my $r2 = await $raider->raid_f('List all pods and check for any issues.');
say $r2;
}
main()->get;
The Raider maintains conversation history across raids, so the LLM can reference earlier context (e.g. the RBAC permissions it discovered) in follow-up interactions.
A ready-to-run demo is included in examples/raider-configmap-demo.pl — an AI creates, reads, updates, and deletes a ConfigMap. See the script's POD for requirements.
SECURITY CONSIDERATIONS
The server inherits the permissions of whatever kubeconfig context it connects with. Use a dedicated service account with minimal RBAC permissions for AI assistant access.
All tool calls check RBAC permissions before executing. Even if the service account has broad permissions, the permission check provides a clear audit trail.
Secrets are supported as a resource type. If your service account can read secrets, the LLM will be able to read them too. Consider excluding
secretsfrom RBAC roles used for AI access.
SEE ALSO
MCP::K8s::Permissions — RBAC discovery engine
MCP::Kubernetes — Alias for this module
Langertha::Raider — Autonomous agent with conversation history and MCP tools
Kubernetes::REST — The underlying Kubernetes API client
IO::K8s — Typed Kubernetes resource objects
MCP::Server — MCP protocol implementation
Kubernetes::REST::Kubeconfig — Kubeconfig parsing
https://modelcontextprotocol.io/ — Model Context Protocol specification
https://kubernetes.io/docs/reference/access-authn-authz/rbac/ — Kubernetes RBAC
SUPPORT
Issues
Please report bugs and feature requests on GitHub at https://github.com/Getty/p5-mcp-k8s/issues.
CONTRIBUTING
Contributions are welcome! Please fork the repository and submit a pull request.
AUTHOR
Torsten Raudssus <torsten@raudssus.de>
COPYRIGHT AND LICENSE
This software is copyright (c) 2026 by Torsten Raudssus.
This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.