<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Josh Hall</title>
    <description>The latest articles on DEV Community by Josh Hall (@josh_hall_b54941047f33661).</description>
    <link>https://dev.to/josh_hall_b54941047f33661</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3955665%2F5c7afa25-0fc4-4187-aa56-5c4a901dc0ac.png</url>
      <title>DEV Community: Josh Hall</title>
      <link>https://dev.to/josh_hall_b54941047f33661</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/josh_hall_b54941047f33661"/>
    <language>en</language>
    <item>
      <title>Build a Read-Only Homelab MCP Server for Your LLM</title>
      <dc:creator>Josh Hall</dc:creator>
      <pubDate>Sun, 06 Sep 2026 18:28:52 +0000</pubDate>
      <link>https://dev.to/josh_hall_b54941047f33661/build-a-read-only-homelab-mcp-server-for-your-llm-336o</link>
      <guid>https://dev.to/josh_hall_b54941047f33661/build-a-read-only-homelab-mcp-server-for-your-llm-336o</guid>
      <description>&lt;p&gt;I wanted to ask my AI assistant plain questions about my homelab — "are all my nodes up?", "any firing alerts?" — and get real answers from the real cluster. What I absolutely did &lt;strong&gt;not&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;The fix is a &lt;strong&gt;read-only MCP server&lt;/strong&gt;: a small program that exposes your homelab to an AI as &lt;em&gt;query&lt;/em&gt; 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. Every number below came out of my own running cluster.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Make the addresses your own.&lt;/strong&gt; Replace &lt;code&gt;10.0.0.104&lt;/code&gt; with your Prometheus host. Keep 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.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What MCP is, in one minute
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://modelcontextprotocol.io" rel="noopener noreferrer"&gt;Model Context Protocol&lt;/a&gt; (MCP) is an open standard — originally from Anthropic — for connecting AI assistants to tools. An &lt;strong&gt;MCP server&lt;/strong&gt; publishes typed-function tools; an &lt;strong&gt;MCP client&lt;/strong&gt; (Claude Desktop, Claude Code, and a growing list) lets the model discover and call them. Instead of hallucinating your cluster's state, the model &lt;em&gt;asks&lt;/em&gt; your server and gets the real answer.&lt;/p&gt;

&lt;p&gt;The protocol is neutral about safety — a tool can do anything you program. Which is why the interesting decision is &lt;strong&gt;what you choose to expose&lt;/strong&gt;. We expose only reads.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build the server
&lt;/h2&gt;

&lt;p&gt;I'll use Python and &lt;a href="https://github.com/jlowin/fastmcp" rel="noopener noreferrer"&gt;FastMCP&lt;/a&gt;, 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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastmcp&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastMCP&lt;/span&gt;

&lt;span class="n"&gt;PROM_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PROM_URL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://10.0.0.104:9090&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;mcp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastMCP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;homelab-readonly&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@mcp.tool&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;prometheus_query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;promql&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Run a read-only Prometheus instant query.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;PROM_URL&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/api/v1/query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                     &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;promql&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;result&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;promql&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;series&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sample&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;metric&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;metric&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;
                       &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;]]}&lt;/span&gt;

&lt;span class="nd"&gt;@mcp.tool&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;cluster_targets_up&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;How many scrape targets are up vs down (read-only).&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;PROM_URL&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/api/v1/query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                     &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;up&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;result&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;up&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;targets_total&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;targets_up&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;up&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;targets_down&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;up&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;mcp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="c1"&gt;# stdio transport by default
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice what's &lt;em&gt;not&lt;/em&gt; here: no &lt;code&gt;reboot&lt;/code&gt;, no &lt;code&gt;delete&lt;/code&gt;, no &lt;code&gt;create&lt;/code&gt;. &lt;strong&gt;The tool surface is the security boundary.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;FastMCP ships an in-memory client, so a five-line script exercises the real protocol. This is unedited output from my cluster:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tools/list -&amp;gt; ['prometheus_query', 'cluster_targets_up']
call cluster_targets_up  -&amp;gt; {'targets_total': 27, 'targets_up': 27, 'targets_down': 0}
call prometheus_query('count(smartctl_device_smart_status)')
                         -&amp;gt; {'series': 1, 'sample': [{'value': '8'}]}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;h2&gt;
  
  
  Read-only &lt;em&gt;by design&lt;/em&gt; — four layers
&lt;/h2&gt;

&lt;p&gt;"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:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Read-only credentials&lt;/strong&gt; — a Proxmox token bound to the built-in &lt;code&gt;PVEAuditor&lt;/code&gt; role; a query-only DB user.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read tools only&lt;/strong&gt; — no write/delete tool is ever registered.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bounded inputs&lt;/strong&gt; — instant queries only, no admin endpoints.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Private scope&lt;/strong&gt; — localhost / Tailscale, never the public internet.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The credential is the real lock.&lt;/strong&gt; The single most important choice is the read-only credential. In Proxmox that's a token bound to &lt;code&gt;PVEAuditor&lt;/code&gt; — 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.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Connect it to Claude
&lt;/h2&gt;

&lt;p&gt;Register the server in your MCP client (Claude Desktop or Claude Code):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"homelab-readonly"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"python"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"/opt/homelab-mcp/server.py"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"env"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"PROM_URL"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://10.0.0.104:9090"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you can ask, in plain English, "are all my Prometheus targets up?" and the assistant calls &lt;code&gt;cluster_targets_up&lt;/code&gt; and tells you. I keep mine reachable only over Tailscale — available from my laptop or phone, never from the open internet.&lt;/p&gt;

&lt;h2&gt;
  
  
  The flourish: a fully self-hosted loop
&lt;/h2&gt;

&lt;p&gt;Pair this with a local Ollama cluster behind a load-balancing endpoint and the whole loop is yours: a &lt;strong&gt;local&lt;/strong&gt; LLM calling a &lt;strong&gt;read-only&lt;/strong&gt; tool server to answer questions about &lt;strong&gt;your&lt;/strong&gt; cluster — no cloud in the path, and no way for any of it to change a thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read the full, updated versions
&lt;/h2&gt;

&lt;p&gt;I keep the maintained versions of this and the surrounding series on my homelab site, documented from a working 4-node Proxmox cluster:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://peira.dev/blog/read-only-homelab-mcp-server/" rel="noopener noreferrer"&gt;Build a Read-Only Homelab MCP Server (full guide)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://peira.dev/blog/olla-ollama-load-balancer/" rel="noopener noreferrer"&gt;Load-Balance a Multi-Node Ollama Cluster With Olla&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://peira.dev/blog/searxng-owui-private-web-search/" rel="noopener noreferrer"&gt;Give Your Local AI Private Web Search With SearXNG&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://peira.dev/blog/read-only-homelab-mcp-server/" rel="noopener noreferrer"&gt;peira.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>devops</category>
      <category>llm</category>
      <category>mcp</category>
    </item>
    <item>
      <title>What Is the *arr Stack? Sonarr, Radarr, and Prowlarr Explained</title>
      <dc:creator>Josh Hall</dc:creator>
      <pubDate>Sun, 06 Sep 2026 15:12:06 +0000</pubDate>
      <link>https://dev.to/josh_hall_b54941047f33661/what-is-the-arr-stack-sonarr-radarr-and-prowlarr-explained-4ocf</link>
      <guid>https://dev.to/josh_hall_b54941047f33661/what-is-the-arr-stack-sonarr-radarr-and-prowlarr-explained-4ocf</guid>
      <description>&lt;p&gt;If you've spent any time in homelab or self-hosting circles, you've seen people mention their "Arr stack" — often written &lt;code&gt;*arr&lt;/code&gt;, where the asterisk is a wildcard for Son*&lt;em&gt;arr&lt;/em&gt;&lt;em&gt;, Rad&lt;/em&gt;&lt;em&gt;arr&lt;/em&gt;&lt;em&gt;, Lid&lt;/em&gt;&lt;em&gt;arr&lt;/em&gt;*, and friends. It sounds like arcane jargon, but the idea is simple: a set of apps that turn "I want to watch this" into an organized file in your library without you touching a download client.&lt;/p&gt;

&lt;p&gt;This post explains what each piece does and how they fit together — the concepts, no setup yet.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A note on use: the arr stack is just a tool. Point it only at content you have the legal right to download where you live — your own media, public-domain works, and Linux ISOs are the intended use.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem the Arr stack solves
&lt;/h2&gt;

&lt;p&gt;Managing a media library by hand means searching a dozen indexers, squinting at release names to guess which is good, downloading, renaming the file into something your media server understands, moving it into the right folder, and refreshing the library. Then doing it again for the next episode. It starts as a hobby and quietly becomes a part-time job.&lt;/p&gt;

&lt;p&gt;The Arr stack automates every step. You tell it &lt;em&gt;what&lt;/em&gt; you want, and it handles the searching, grabbing, renaming, and filing, then tells your media server to rescan so the title just appears, ready to watch.&lt;/p&gt;

&lt;p&gt;One thing I learned running this myself: "hands-off" holds right up until the auto-downloading quietly stops. Nothing errors — the library just stops growing, and because everything still &lt;em&gt;looks&lt;/em&gt; fine, you don't catch it for a while. Build it so you'll notice when one link stops handing off to the next.&lt;/p&gt;

&lt;h2&gt;
  
  
  The players
&lt;/h2&gt;

&lt;p&gt;Each app is a focused piece of the pipeline. All are open-source and part of the &lt;a href="https://wiki.servarr.com" rel="noopener noreferrer"&gt;Servarr&lt;/a&gt; family, so they share a near-identical interface.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;App&lt;/th&gt;
&lt;th&gt;Handles&lt;/th&gt;
&lt;th&gt;Thinks in terms of&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://sonarr.tv" rel="noopener noreferrer"&gt;Sonarr&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;TV shows&lt;/td&gt;
&lt;td&gt;series → seasons → episodes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://radarr.video" rel="noopener noreferrer"&gt;Radarr&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Movies&lt;/td&gt;
&lt;td&gt;single films + editions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://prowlarr.com" rel="noopener noreferrer"&gt;Prowlarr&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Indexers&lt;/td&gt;
&lt;td&gt;the sources everything searches&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A download client (e.g. &lt;a href="https://www.qbittorrent.org" rel="noopener noreferrer"&gt;qBittorrent&lt;/a&gt;)&lt;/td&gt;
&lt;td&gt;The actual transfer&lt;/td&gt;
&lt;td&gt;torrents / Usenet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A media server (&lt;a href="https://www.plex.tv" rel="noopener noreferrer"&gt;Plex&lt;/a&gt; / &lt;a href="https://jellyfin.org" rel="noopener noreferrer"&gt;Jellyfin&lt;/a&gt;)&lt;/td&gt;
&lt;td&gt;Playback&lt;/td&gt;
&lt;td&gt;libraries you stream&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Sonarr and Radarr are essentially the same program&lt;/strong&gt; with different logic — Sonarr understands episode numbering and air schedules, Radarr understands releases and cuts. There are siblings too: Lidarr (music), Readarr (books, now retired). Most media setups only need Sonarr and Radarr.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  How Prowlarr changed the game
&lt;/h2&gt;

&lt;p&gt;An &lt;strong&gt;indexer&lt;/strong&gt; is a searchable catalog of available releases — a torrent tracker or a Usenet index. Before Prowlarr, you added every indexer to Sonarr &lt;strong&gt;and&lt;/strong&gt; Radarr separately, kept two credential sets current, and repeated it every time an indexer changed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://prowlarr.com" rel="noopener noreferrer"&gt;Prowlarr&lt;/a&gt; is the &lt;strong&gt;indexer manager&lt;/strong&gt;: you add each indexer once, and it &lt;strong&gt;syncs it into every connected Arr app automatically&lt;/strong&gt;. Prowlarr stores the indexer definitions; Sonarr and Radarr register as "applications" and receive the synced indexers.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Prowlarr decides where to search; Sonarr and Radarr decide what to grab.&lt;/strong&gt; Prowlarr never downloads anything — it just gives the others a shared, always-current list of places to look.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The pipeline, end to end
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  You add a title  (or an import list adds it automatically)
        |
        v
  Radarr / Sonarr   -- decides it's "wanted", asks Prowlarr's indexers
        |
        v
  Prowlarr indexers -- return candidate releases, scored by quality rules
        |
        v
  Download client   -- grabs the chosen release (torrent behind a VPN)
        |
        v
  Radarr / Sonarr   -- imports: renames + moves the file into the library
        |
        v
  Media server      -- rescans; the title appears, ready to stream
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every arrow is automatic. The only human input is the first line — and even that can be automated with import lists that pull from a watchlist or "popular" feeds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where quality rules fit in
&lt;/h2&gt;

&lt;p&gt;The Arr apps don't grab the first result they find. Each uses a &lt;strong&gt;quality profile&lt;/strong&gt; — your rules for resolution, source, file size, and codecs — to score candidates and pick the best fit. The community-maintained &lt;a href="https://trash-guides.info" rel="noopener noreferrer"&gt;TRaSH Guides&lt;/a&gt; are the reference for tuning these.&lt;/p&gt;

&lt;h2&gt;
  
  
  What about a VPN?
&lt;/h2&gt;

&lt;p&gt;Only one piece needs one. Sonarr, Radarr, and Prowlarr move metadata and searches — no swarm traffic. The &lt;strong&gt;download client&lt;/strong&gt; is what joins torrent swarms and exposes your IP, so the hardened setup routes &lt;strong&gt;only the torrent client&lt;/strong&gt; through a VPN container with a kill-switch, leaving the rest on your LAN.&lt;/p&gt;

&lt;h2&gt;
  
  
  The wider *arr family
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.bazarr.media/" rel="noopener noreferrer"&gt;Bazarr&lt;/a&gt;&lt;/strong&gt; watches your Sonarr and Radarr libraries and fetches &lt;strong&gt;subtitles&lt;/strong&gt; automatically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://lidarr.audio/" rel="noopener noreferrer"&gt;Lidarr&lt;/a&gt;&lt;/strong&gt; does for &lt;strong&gt;music&lt;/strong&gt; what Sonarr does for TV; &lt;strong&gt;Whisparr&lt;/strong&gt; handles adult content.&lt;/li&gt;
&lt;li&gt;For &lt;strong&gt;requests&lt;/strong&gt;, &lt;a href="https://github.com/fallenbagel/jellyseerr" rel="noopener noreferrer"&gt;Jellyseerr&lt;/a&gt; gives the household a streaming-style page that hands approved titles to Radarr/Sonarr.&lt;/li&gt;
&lt;li&gt;One to skip: &lt;strong&gt;Readarr&lt;/strong&gt; (books) was retired in 2025 when its metadata source broke.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;p&gt;I keep the full, continually-updated versions of these guides — deploy with Docker Compose, automation, subtitles, music, and requests — on my homelab site, documented from a working 4-node Proxmox cluster:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://peira.dev/blog/arr-stack-explained/" rel="noopener noreferrer"&gt;What Is the Arr Stack? (full guide)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://peira.dev/blog/arr-stack-docker-deploy/" rel="noopener noreferrer"&gt;Deploy the Arr Stack with Docker Compose&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://peira.dev/blog/arr-stack-automation/" rel="noopener noreferrer"&gt;Automating the Arr Stack&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://peira.dev/blog/bazarr-subtitles-arr-stack/" rel="noopener noreferrer"&gt;Bazarr: Automatic Subtitles&lt;/a&gt; · &lt;a href="https://peira.dev/blog/lidarr-music-arr-stack/" rel="noopener noreferrer"&gt;Lidarr: Music&lt;/a&gt; · &lt;a href="https://peira.dev/blog/jellyseerr-requests-arr-stack/" rel="noopener noreferrer"&gt;Jellyseerr: Requests&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://peira.dev/blog/arr-stack-explained/" rel="noopener noreferrer"&gt;peira.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>selfhosted</category>
      <category>homelab</category>
      <category>docker</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
