Build a Read-Only Homelab MCP Server for Your LLM

Build a read-only MCP server so Claude or any LLM can safely query your Proxmox and Prometheus homelab — no tool can change a thing. FastMCP, tested.

On this page
  1. What MCP is, in one minute
  2. Build the server
  3. Read-only by design — the four layers
  4. Connect it to Claude
  5. The flourish: a fully self-hosted loop
  6. What’s next

I wanted to be able to ask my AI assistant plain questions about my homelab — “are all my nodes up?”, “how many disks are being monitored?”, “any firing alerts?” — and get real answers from the real cluster. What I absolutely did not want was to hand a language model a button that could reboot a node or delete a VM. Convenience is lovely right up until the model confidently does the wrong thing at 2 a.m.

The resolution is a read-only MCP server: a small program that exposes your homelab to an AI as a set of query tools and nothing else, connecting with credentials that can only read. The safety isn’t a promise you extract from the model — it’s baked into the architecture. There is simply no tool, and no permission, to change anything. This flagship builds one, and every number you’ll see below came out of my own running cluster through the server I’m about to show you.

The AI can ask anything — and change nothingAI assistantMCP clientMCP serverread tools onlyprometheus_querycluster_targets_upProxmoxPVEAuditor tokenPrometheusinstant queriesreboot_node()?no such tool exists — nothing to reject, nothing to fear
First: make these values your own

The addresses below are examples. Replace 10.0.0.104 with your Prometheus host and any Proxmox address with your own. Keep every credential (the read-only API token) in your secret store and pass it via an environment variable — never paste a real token into the server file. If a value looks specific to one machine, it’s a placeholder to change, not a literal to copy.


What MCP is, in one minute

The Model Context Protocol (MCP) is an open standard — originally from Anthropic — for connecting AI assistants to tools. An MCP server publishes a set of tools (typed functions); an MCP client (Claude Desktop, Claude Code, and a growing list of others) lets the model discover and call them. Instead of the model hallucinating your cluster’s state, it asks your server and gets the real answer.

The protocol itself is neutral about safety — a tool can do anything you program it to. Which is exactly why the interesting design decision is what you choose to expose. We’re going to expose only reads.


Build the server

I’ll use Python and FastMCP, where a decorated function becomes a tool. Two tools is enough to be genuinely useful: a general Prometheus query, and a friendly “are my targets up?” summary.

1Write a read-only server10 min
server.py — read-only homelab tools

import os, requests
from fastmcp import FastMCP

PROM_URL = os.environ.get("PROM_URL", "http://10.0.0.104:9090")
mcp = FastMCP("homelab-readonly")

@mcp.tool
def prometheus_query(promql: str) -> dict:
  """Run a read-only Prometheus instant query."""
  r = requests.get(f"{PROM_URL}/api/v1/query",
                   params={"query": promql}, timeout=10)
  r.raise_for_status()
  data = r.json()["data"]["result"]
  return {"query": promql, "series": len(data),
          "sample": [{"metric": s["metric"], "value": s["value"][1]}
                     for s in data[:5]]}

@mcp.tool
def cluster_targets_up() -> dict:
  """How many scrape targets are up vs down (read-only)."""
  r = requests.get(f"{PROM_URL}/api/v1/query",
                   params={"query": "up"}, timeout=10)
  r.raise_for_status()
  res = r.json()["data"]["result"]
  up = sum(1 for s in res if s["value"][1] == "1")
  return {"targets_total": len(res), "targets_up": up,
          "targets_down": len(res) - up}

if __name__ == "__main__":
  mcp.run()   # stdio transport by default

Notice what’s not here: there is no reboot, no delete, no create. The tool surface is the security boundary.

2Smoke-test it over the real MCP protocol3 min

You don’t need to wire it into a chat client to know it works. FastMCP ships an in-memory client, so a five-line script exercises the actual protocol — list the tools, call one, see real data. This is the unedited output from my cluster:

A real MCP round-trip

tools/list -> ['prometheus_query', 'cluster_targets_up']
call cluster_targets_up  -> {'targets_total': 27, 'targets_up': 27, 'targets_down': 0}
call prometheus_query('count(smartctl_device_smart_status)')
                       -> {'series': 1, 'sample': [{'value': '8'}]}

Twenty-seven scrape targets, all up; eight disks reporting SMART. The model asked; the cluster answered; nothing changed.


Read-only by design — the four layers

“Read-only” isn’t one setting; it’s a posture you build in layers, so a mistake at any single layer can’t hand an AI the keys.

Four independent layers — each one alone would stop a mistake1. Read-only credsPVEAuditor token,query-only DB user2. Read tools onlyno write/delete toolever registered3. Bounded inputsinstant queries only,no admin endpoints4. Private scopelocalhost / Tailscale,never the internetGive the token to Proxmox as PVEAuditor; give the AI only the tools you wrote; keep it off the public net.
The credential is the real lock

The single most important choice is the read-only credential. In Proxmox, that’s a token bound to the built-in PVEAuditor role — it can read cluster state and nothing else. Even if you later fat-finger a write tool into the server, an audit token has no permission to carry it out. Store the token in your secret manager and inject it as an environment variable; it never belongs in the code.


Connect it to Claude

To use it from an MCP client, register the server. In Claude Desktop or Claude Code that’s a small entry pointing at your server command:

MCP client config (mcpServers entry)

{
"mcpServers": {
  "homelab-readonly": {
    "command": "python",
    "args": ["/opt/homelab-mcp/server.py"],
    "env": { "PROM_URL": "http://10.0.0.104:9090" }
  }
}
}

Now you can ask, in plain English, “are all my Prometheus targets up?” and the assistant calls cluster_targets_up and tells you. I keep mine reachable only over Tailscale, so the tools are available from my laptop or phone but never from the open internet.


The flourish: a fully self-hosted loop

Here’s the part I find genuinely delightful. Pair this with the Olla endpoint from the last post and a local model, and the entire loop is yours: a local LLM, load-balanced across your Ollama nodes, calling a read-only tool server to answer questions about your cluster — no cloud in the path, and no way for any of it to change a thing. Ask your homelab how it’s doing, and your homelab answers, on your hardware.


What’s next

You’ve given an AI a safe pair of eyes on your infrastructure. Next in this flagship run we get our hands dirty in the network layer: Proxmox VE 9.2’s new SDN fabrics, building a routed, self-healing network between cluster nodes with WireGuard and route-map filtering.


Related posts:

Comments

Comments are powered by GitHub Discussions — sign in with a GitHub account to join the conversation.