Lesson 05 of 14 · Modules
MCP servers for infrastructure
Expose your platform to AI safely with the Model Context Protocol: what MCP servers are (tools, resources, prompts), building a small read-only Kubernetes diagnostics server in Python, connecting it to a client, and the permission design that keeps it safe.
What MCP is
The Model Context Protocol is an open protocol (introduced by Anthropic in 2024 and now supported by many clients and vendors) that lets AI applications connect to external systems in a standard way:
- Tools: functions the model can call (e.g.
list_pods(namespace)). - Resources: data the client can read (e.g. a runbook, a config file).
- Prompts: reusable prompt templates.
Transports: stdio (the client starts the server as a local process) and Streamable HTTP (remote servers, with authentication).
MCP is like a standard plug socket. Before, every appliance needed its own special adaptor for every house. With a standard socket, any appliance (AI client) can plug into any house (your systems). But you still decide which sockets exist and how much power each one gives: a socket for the reading lamp, not one wired straight to the fuse box.
A read-only Kubernetes diagnostics server
# server.py: read-only Kubernetes diagnostics over MCP (Python SDK, FastMCP)
from kubernetes import client, config
from mcp.server.fastmcp import FastMCP
config.load_kube_config() # uses a read-only context (see RBAC below)
core = client.CoreV1Api()
mcp = FastMCP("k8s-readonly")
MAX_ITEMS = 200
@mcp.tool()
def list_problem_pods(namespace: str = "") -> list[dict]:
"""List pods that are not Running/Succeeded or have restarts, optionally in one namespace."""
pods = (core.list_namespaced_pod(namespace) if namespace
else core.list_pod_for_all_namespaces()).items
out = []
for p in pods:
restarts = sum(cs.restart_count for cs in (p.status.container_statuses or []))
if p.status.phase not in ("Running", "Succeeded") or restarts > 0:
out.append({"namespace": p.metadata.namespace, "pod": p.metadata.name,
"phase": p.status.phase, "restarts": restarts})
return out[:MAX_ITEMS]
@mcp.tool()
def recent_events(namespace: str, limit: int = 50) -> list[dict]:
"""Return the most recent events in a namespace (warnings first)."""
events = core.list_namespaced_event(namespace).items
events.sort(key=lambda e: (e.type != "Warning",
-(e.last_timestamp or e.event_time or e.metadata.creation_timestamp).timestamp()))
return [{"type": e.type, "reason": e.reason, "object": f"{e.involved_object.kind}/{e.involved_object.name}",
"message": (e.message or "")[:300]} for e in events[:min(limit, MAX_ITEMS)]]
if __name__ == "__main__":
mcp.run() # stdio transport by default
$ pip install "mcp[cli]" kubernetes
$ claude mcp add k8s-ro -- python server.py
(Check the MCP Python SDK docs for your version; the FastMCP API is the SDK's recommended high-level interface.)
RBAC: make it safe by construction
apiVersion: v1
kind: ServiceAccount
metadata:
name: mcp-readonly
namespace: ops-tools
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: mcp-readonly-view
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: view # built-in: read most objects, no Secrets, no writes
subjects:
- kind: ServiceAccount
name: mcp-readonly
namespace: ops-tools
Create a kubeconfig for this ServiceAccount (short-lived token) and point the server at it. Even if the model is confused or manipulated by text in logs (prompt injection), it cannot write or read Secrets.
Design rules for infra MCP servers
- Narrow tools with typed, validated parameters; no generic shell or
kubectlpassthrough. - Cap output (items, message length): big dumps waste context and can hide injected instructions.
- Redact sensitive fields (env vars, annotations with tokens) before returning data.
- Log every call (tool, arguments, caller, time).
- Add write tools only later, behind explicit approval flows (lesson 12), and with separate credentials.
- Prefer existing, well-reviewed servers where they fit, but review their permissions like any third-party code.
Try it: your first MCP server
- Create the
mcp-readonlyServiceAccount and binding in a kind cluster, and a kubeconfig using a token fromkubectl create token mcp-readonly -n ops-tools. - Run the server above with that kubeconfig and register it with your MCP client.
- Break a deployment (bad image) and ask the assistant "what's wrong in namespace X?"; watch it call your tools.
- Try to make it delete something: confirm there's no tool for it, and that the kubeconfig would be denied anyway (
kubectl auth can-i delete pods --as=system:serviceaccount:ops-tools:mcp-readonly). - Add a third tool (e.g.
deployment_rollout_status) with input validation and output caps.
Going deeper: MCP in production
- Remote servers over Streamable HTTP need proper authentication/authorization (per-user identity where possible) and TLS.
- Consider per-environment servers (dev writeable, prod read-only) with clearly different names.
- Treat tool outputs as untrusted input to the model: data from logs, tickets and web pages may contain instructions.
Recap
- MCP standardises tools, resources and prompts for AI clients; transports stdio and Streamable HTTP.
- Build narrow, validated, capped tools; no generic command execution.
- Enforce safety at the platform layer: a ServiceAccount bound to
view(read-only, no Secrets). - Log calls, redact data, and add write capabilities only with approvals and separate credentials.
This site is a public version of my personal engineering knowledge hub. It intentionally excludes confidential company information and internal operational details.