<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title>Mcp - Tag - arleo.eu</title><link>https://www.arleo.eu/en/tags/mcp/</link><description>Mcp - Tag - arleo.eu</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Thu, 14 May 2026 09:43:19 +0200</lastBuildDate><atom:link href="https://www.arleo.eu/en/tags/mcp/" rel="self" type="application/rss+xml"/><item><title>hugo-mcp Cloudflare plugin: smart cache purge</title><link>https://www.arleo.eu/en/posts/hugo-mcp-plugin-cloudflare/</link><pubDate>Thu, 14 May 2026 09:43:19 +0200</pubDate><author>Jmr</author><guid>https://www.arleo.eu/en/posts/hugo-mcp-plugin-cloudflare/</guid><description><![CDATA[<div class="featured-image">
                <img src="/images/hugo-mcp-plugin-cloudflare-featured.jpg" referrerpolicy="no-referrer">
            </div><h2 id="tldr">TL;DR</h2>
<p>The Cloudflare plugin in <a href="https://github.com/jmrGrav/hugo-mcp/releases/tag/v2.0.0" target="_blank" rel="noopener noreffer ">hugo-mcp v2.0</a> implements 3 cache purge modes (<code>full</code>, <code>partial</code>, <code>smart</code>). The <code>partial</code> mode computes the linked URLs to invalidate (canonical + sitemap + RSS + listing + home) to preserve 95% of the CDN cache on every modification. Concretely: 6 URLs purged instead of wiping everything. This post details the computation, the pitfalls, and why <code>smart</code> became the default.</p>]]></description></item><item><title>hugo-mcp v2.0: a Python plugin-system in 200 lines</title><link>https://www.arleo.eu/en/posts/hugo-mcp-plugins-architecture/</link><pubDate>Sat, 09 May 2026 23:52:28 +0200</pubDate><author>Jmr</author><guid>https://www.arleo.eu/en/posts/hugo-mcp-plugins-architecture/</guid><description><![CDATA[<div class="featured-image">
                <img src="/images/hugo-mcp-plugins-architecture-featured.jpg" referrerpolicy="no-referrer">
            </div><h2 id="tldr">TL;DR</h2>
<p><a href="https://github.com/jmrGrav/hugo-mcp/releases/tag/v2.0.0" target="_blank" rel="noopener noreffer ">hugo-mcp v2.0.0</a> introduces a <strong>Python plugin-system</strong> that lets anyone add hooks after each <code>create_page</code> / <code>update_page</code> / <code>delete_page</code> operation. 200 lines of code for the core, 3 production plugins shipped (IndexNow, Google Indexing, Cloudflare). This post explains the design, the trade-offs, the security posture, and shows how to write your own plugin in 5 minutes.</p>]]></description></item><item><title>MCP security sprint delivered: v1.9.0, 10 chantiers, hardened ecosystem</title><link>https://www.arleo.eu/en/posts/sprint-securite-mcp-livre/</link><pubDate>Sat, 09 May 2026 18:44:12 +0200</pubDate><author>Jmr</author><guid>https://www.arleo.eu/en/posts/sprint-securite-mcp-livre/</guid><description><![CDATA[<div class="featured-image">
                <img src="/images/sprint-securite-mcp-livre-featured.jpg" referrerpolicy="no-referrer">
            </div><h2 id="tldr">TL;DR</h2>
<p>On May 9, 2026, I delivered all 10 chantiers of the MCP security sprint that <a href="/en/posts/roadmap-sprint-securite-mcp/" rel="">I had announced earlier in the day</a> in a single marathon session. <code>hugo-mcp</code> is now at <strong>v1.9.0</strong> (<a href="https://github.com/jmrGrav/hugo-mcp/releases/tag/v1.9.0" target="_blank" rel="noopener noreffer ">GitHub Release</a>), commit <code>1404f83</code> GPG-signed.</p>
<p>Here&rsquo;s the high-level recap + a pedagogical deep-dive on 2 chantiers with real value beyond my specific context: <strong>C2 token rotation</strong> and <strong>C6 internal TLS</strong>.</p>
<h2 id="recap-of-10-chantiers">Recap of 10 chantiers</h2>
<table>
  <thead>
      <tr>
          <th>#</th>
          <th>Chantier</th>
          <th>Implementation</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>C1</td>
          <td>Rate limiting</td>
          <td><code>slowapi</code>, 60 req/min per IP</td>
      </tr>
      <tr>
          <td>C2</td>
          <td>Token rotation</td>
          <td><code>tokens.json</code> + <code>token_mgr.py</code> CLI</td>
      </tr>
      <tr>
          <td>C3</td>
          <td>JSON audit logs</td>
          <td><code>structlog</code>, machine-readable events</td>
      </tr>
      <tr>
          <td>C4</td>
          <td>Strict Pydantic v2</td>
          <td><code>CreatePageArgs</code> / <code>UpdatePageArgs</code> with constraints</td>
      </tr>
      <tr>
          <td>C5</td>
          <td>bcrypt cost-12</td>
          <td>Tokens hashed in storage</td>
      </tr>
      <tr>
          <td>C6</td>
          <td>NUC ↔ VM TLS</td>
          <td>EC P-256 cert, uvicorn SSL, proxy verifies the cert</td>
      </tr>
      <tr>
          <td>C7</td>
          <td>requirements.lock</td>
          <td>SHA-256 hashes via <code>pip-compile --generate-hashes</code></td>
      </tr>
      <tr>
          <td>C8</td>
          <td>Info disclosure</td>
          <td>Docs off, generic exception handler, <code>proxy_hide_header</code></td>
      </tr>
      <tr>
          <td>C9</td>
          <td>nginx WAF</td>
          <td>POST + <code>application/json</code> enforcement on <code>/mcp</code>, OWASP CRS active</td>
      </tr>
      <tr>
          <td>C10</td>
          <td>Backup DR</td>
          <td><code>backup.sh</code> GPG-encrypted, 30-day retention</td>
      </tr>
  </tbody>
</table>
<p>Full details in the <a href="https://github.com/jmrGrav/hugo-mcp/blob/main/CHANGELOG.md" target="_blank" rel="noopener noreffer ">CHANGELOG v1.9.0</a> and commit <a href="https://github.com/jmrGrav/hugo-mcp/commit/1404f83" target="_blank" rel="noopener noreffer ">1404f83</a>.</p>]]></description></item><item><title>Roadmap: MCP Security Sprint — 4 domains, 10 chantiers (DELIVERED ✅)</title><link>https://www.arleo.eu/en/posts/roadmap-sprint-securite-mcp/</link><pubDate>Sat, 09 May 2026 13:07:33 +0200</pubDate><author>Jmr</author><guid>https://www.arleo.eu/en/posts/roadmap-sprint-securite-mcp/</guid><description><![CDATA[<div class="featured-image">
                <img src="/images/roadmap-sprint-securite-mcp-featured.jpg" referrerpolicy="no-referrer">
            </div><h2 id="status--delivered--sprint-closed-on-may-9-2026">Status: ✅ DELIVERED — sprint closed on May 9, 2026</h2>
<p><strong>Update May 9, 2026 (end of session)</strong>: all 10 chantiers delivered in a single marathon session, <strong>and all 3 coordinated releases published the same day</strong> (<code>hugo-mcp</code> v1.9.0, <code>mcp-oauth-proxy</code> v2.1.0, <code>mcp-installer</code> v1.3.0). Every component of the MCP ecosystem hardened in one day. Full technical recap: <a href="/en/posts/sprint-securite-mcp-livre/" rel="">MCP security sprint delivered: v1.9.0, 10 chantiers, hardened ecosystem</a>.</p>
<p>This page stays published as an archive — to show the trajectory of an announced sprint, then kept. All original content below is preserved.</p>
<hr>
<h2 id="original-status-pre-delivery">Original status (pre-delivery)</h2>
<p>This page publicly documented an ongoing hardening sprint on arleo.eu&rsquo;s MCP infrastructure. For security reasons, specific details of each chantier were not exposed until fixes were delivered (&ldquo;sec-first&rdquo; philosophy: don&rsquo;t publish an attack roadmap).</p>
<h2 id="why-this-transparency">Why this transparency</h2>
<p>I debated whether to publish this page. Arguments for transparency:</p>
<ul>
<li><strong>Public commitment</strong> = healthy pressure on yourself to deliver</li>
<li><strong>Documentation</strong> of a homelab whose goal is to learn and share</li>
<li><strong>Honesty</strong> with readers consuming other technical articles</li>
</ul>
<p>Arguments against:</p>
<ul>
<li><strong>Attack roadmap</strong>: if I precisely list what&rsquo;s not yet protected, I give clues to a patient attacker</li>
<li><strong>Artificial pressure</strong>: announcing a sprint then not delivering = worse than announcing nothing</li>
</ul>
<p>Adopted compromise: publish the <strong>direction</strong> and <strong>hardening domains</strong> without specifying what&rsquo;s weak today. Specific technical detail published on delivery.</p>
<h2 id="sprint-scope">Sprint scope</h2>
<p>The sprint covered 10 chantiers grouped into 4 domains:</p>
<h3 id="1-application-hardening-fastapi--pydantic">1. Application hardening (FastAPI + Pydantic)</h3>
<p>Reinforcement of MCP entry layers: strict input validation, unified error handling, no information leak in responses (stack traces, internal paths, lib versions).</p>
<h3 id="2-authentication-and-tokens">2. Authentication and tokens</h3>
<p>Refactor of MCP access token management: lifetime, rotation, revocation, hashing in storage. Allow cutting off a compromised client&rsquo;s access without redeploying the service.</p>
<h3 id="3-observability-and-audit">3. Observability and audit</h3>
<p>Integration of JSON structured logs ingested in BetterStack via Vector. Each MCP call must produce a traceable event: who, what, when, duration, status. Enables anomaly detection in near real-time.</p>
<h3 id="4-infrastructure-and-resilience">4. Infrastructure and resilience</h3>
<p>TLS for internal traffic between NUC and VM, dedicated ModSec rules on the <code>/mcp</code> path, disaster recovery runbook (token theft, server compromise, data loss).</p>
<h2 id="what-was-already-in-place-before-the-sprint">What was already in place before the sprint</h2>
<p>To not create false impressions, here are the layers <strong>already in place</strong> on infrastructure before the sprint (so out of scope):</p>
<ul>
<li>nginx + mandatory TLS 1.3 (Mozilla Modern config)</li>
<li>Cloudflare WAF + Bot Management + IP whitelist</li>
<li>ModSecurity + OWASP CRS 4.x on all vhosts</li>
<li>CrowdSec in WAF mode + Cloudflare bouncer</li>
<li>systemd hardening at level 1.7 (cf. <a href="/en/posts/hardening-systemd-mcp/" rel="">systemd hardening</a>)</li>
<li>HMAC validation on webhooks</li>
<li>Frontmatter Pydantic validation (1 chantier of 4 in the validation category)</li>
</ul>
<p>The sprint aimed to <strong>complete</strong> this base, not replace it.</p>
<h2 id="method">Method</h2>
<p>For each chantier:</p>
<ol>
<li>Implemented locally + unit tests</li>
<li>Validated on <code>mcp-test-vm</code> (pre-prod VM)</li>
<li>Deployed to prod only after passing tests</li>
<li>Structured JSON audit log for deployment traceability</li>
<li>Post-mortem or technical write-up published <strong>after</strong> delivery</li>
</ol>
<p>No direct prod deployment without pre-prod step.</p>
<h2 id="published-releases">Published releases</h2>]]></description></item><item><title>Roadmap: Git webhook → automatic Hugo rebuild</title><link>https://www.arleo.eu/en/posts/roadmap-webhook-git-rebuild/</link><pubDate>Sat, 09 May 2026 13:06:50 +0200</pubDate><author>Jmr</author><guid>https://www.arleo.eu/en/posts/roadmap-webhook-git-rebuild/</guid><description><![CDATA[<div class="featured-image">
                <img src="/images/roadmap-webhook-git-rebuild-featured.jpg" referrerpolicy="no-referrer">
            </div><h2 id="status--backlog--not-yet-implemented">Status: 🗂️ BACKLOG — not yet implemented</h2>
<p>This page documents an architectural intent to be implemented in a future iteration. The code is not yet in production.</p>
<h2 id="context">Context</h2>
<p>In <a href="/en/posts/strategie-4-mcp-vs-git/" rel="">Strategy 4 (separating MCP / Git)</a>, I explained why <code>content/</code> is in <code>.gitignore</code> on the arleo.eu repo: so that no conflict is possible between MCP writes and Git writes.</p>
<p>Concretely, this means that when I push a new version of <code>layouts/</code>, <code>themes/</code>, <code>static/</code>, <code>hugo.toml</code>, or <code>deploy.sh</code> from VS Code, <strong>nothing happens automatically</strong> server-side. I have to SSH into the Hugo VM and manually run <code>git pull &amp;&amp; hugo --minify &amp;&amp; rsync</code>.</p>
<p>Not critical (structure pushes happen ~1× per week), but it&rsquo;s unnecessary friction. So: GitHub webhook → auto-rebuild.</p>
<h2 id="target-architecture">Target architecture</h2>]]></description></item><item><title>Post-mortem: Cloudflare Bot Management blocked MCP webhooks</title><link>https://www.arleo.eu/en/posts/postmortem-cf-bot-blocking-mcp/</link><pubDate>Sat, 09 May 2026 13:06:02 +0200</pubDate><author>Jmr</author><guid>https://www.arleo.eu/en/posts/postmortem-cf-bot-blocking-mcp/</guid><description><![CDATA[<div class="featured-image">
                <img src="/images/postmortem-cf-bot-blocking-mcp-featured.jpg" referrerpolicy="no-referrer">
            </div><h2 id="the-symptom">The symptom</h2>
<p>I just finished a webhook endpoint in <code>hugo-mcp-proxy</code> that will receive notifications from GitHub on every push to the arleo.eu repo. Clean implementation: HMAC-SHA256, rate limiting, IPAddressAllow GitHub ranges in systemd.</p>
<p>Functional test from an external client:</p>
<div class="code-block code-line-numbers open" data-start="0">
    <div class="code-header language-bash">
        <span class="code-title"><i class="arrow fas fa-angle-right" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h" aria-hidden="true"></i></span>
        <span class="copy" title="Copy to clipboard"><i class="far fa-copy" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">$ curl -X POST https://mcp-hugo.arleo.eu/webhook/test <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>    -H <span class="s2">&#34;Content-Type: application/json&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>    -d <span class="s1">&#39;{&#34;test&#34;: true}&#39;</span></span></span></code></pre></div></div>
<p>Response: <strong>403 Forbidden</strong>.</p>
<p>Strange. The service is running, my source IP is whitelisted, the HMAC is correct. Why 403?</p>
<h2 id="server-side-investigation">Server-side investigation</h2>
<p>NUC nginx logs:</p>
<div class="code-block code-line-numbers open" data-start="0">
    <div class="code-header language-">
        <span class="code-title"><i class="arrow fas fa-angle-right" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h" aria-hidden="true"></i></span>
        <span class="copy" title="Copy to clipboard"><i class="far fa-copy" aria-hidden="true"></i></span>
    </div><pre tabindex="0"><code>$ sudo tail -100 /var/log/nginx/mcp-hugo.access.log | grep webhook</code></pre></div>
<p>Empty. No request reaches nginx.</p>
<p><code>mcp-oauth-proxy</code> logs:</p>
<div class="code-block code-line-numbers open" data-start="0">
    <div class="code-header language-">
        <span class="code-title"><i class="arrow fas fa-angle-right" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h" aria-hidden="true"></i></span>
        <span class="copy" title="Copy to clipboard"><i class="far fa-copy" aria-hidden="true"></i></span>
    </div><pre tabindex="0"><code>$ sudo journalctl -u mcp-oauth-proxy -n 100 | grep webhook</code></pre></div>
<p>Empty too. The request doesn&rsquo;t even reach the service.</p>
<p>Either it&rsquo;s blocked by the firewall before nginx (CrowdSec or ufw), or upstream by Cloudflare.</p>
<h2 id="the-truth-at-cloudflare">The truth at Cloudflare</h2>]]></description></item><item><title>Post-mortem: mcp-installer regenerated tokens on every rerun</title><link>https://www.arleo.eu/en/posts/postmortem-mcp-installer-idempotence/</link><pubDate>Sat, 09 May 2026 13:05:11 +0200</pubDate><author>Jmr</author><guid>https://www.arleo.eu/en/posts/postmortem-mcp-installer-idempotence/</guid><description><![CDATA[<div class="featured-image">
                <img src="/images/postmortem-mcp-installer-idempotence-featured.jpg" referrerpolicy="no-referrer">
            </div><h2 id="the-bug">The bug</h2>
<p><code>mcp-installer</code> is a bash script I wrote to automate the installation of <code>mcp-oauth-proxy</code> (FastAPI + nginx + systemd) on a new host. Standard workflow: clone, run, done.</p>
<p>Except when re-running the script on an <strong>already installed</strong> host (e.g. to update the version), I discovered an idempotence bug: all secrets were regenerated.</p>
<div class="code-block code-line-numbers open" data-start="0">
    <div class="code-header language-bash">
        <span class="code-title"><i class="arrow fas fa-angle-right" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h" aria-hidden="true"></i></span>
        <span class="copy" title="Copy to clipboard"><i class="far fa-copy" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">$ sudo ./install.sh
</span></span><span class="line"><span class="cl"><span class="o">[</span>+<span class="o">]</span> Generating MCP_TOKEN...
</span></span><span class="line"><span class="cl"><span class="o">[</span>+<span class="o">]</span> Generating CLIENT_ID...
</span></span><span class="line"><span class="cl"><span class="o">[</span>+<span class="o">]</span> Generating CLIENT_SECRET...
</span></span><span class="line"><span class="cl"><span class="o">[</span>+<span class="o">]</span> Generating TOKEN_SECRET...
</span></span><span class="line"><span class="cl"><span class="o">[</span>+<span class="o">]</span> Writing /etc/mcp-oauth-proxy/.env...</span></span></code></pre></div></div>
<p>If you already have a <code>.env</code> with tokens in service, the installer <strong>overwrites</strong> them. All already-registered OAuth clients (Claude.ai in my case) end up with invalid credentials. Connection breaks.</p>
<h2 id="why-it-happened">Why it happened</h2>
<p>The script used this logic:</p>
<div class="code-block code-line-numbers" data-start="0">
    <div class="code-header language-bash">
        <span class="code-title"><i class="arrow fas fa-angle-right" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h" aria-hidden="true"></i></span>
        <span class="copy" title="Copy to clipboard"><i class="far fa-copy" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nv">MCP_TOKEN</span><span class="o">=</span><span class="k">$(</span>openssl rand -hex 32<span class="k">)</span>
</span></span><span class="line"><span class="cl"><span class="nv">CLIENT_ID</span><span class="o">=</span><span class="k">$(</span>openssl rand -hex 16<span class="k">)</span>
</span></span><span class="line"><span class="cl"><span class="nv">CLIENT_SECRET</span><span class="o">=</span><span class="k">$(</span>openssl rand -hex 32<span class="k">)</span>
</span></span><span class="line"><span class="cl"><span class="nv">TOKEN_SECRET</span><span class="o">=</span><span class="k">$(</span>openssl rand -hex 32<span class="k">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">cat &gt; /etc/mcp-oauth-proxy/.env <span class="s">&lt;&lt;EOF
</span></span></span><span class="line"><span class="cl"><span class="s">MCP_TOKEN=$MCP_TOKEN
</span></span></span><span class="line"><span class="cl"><span class="s">CLIENT_ID=$CLIENT_ID
</span></span></span><span class="line"><span class="cl"><span class="s">CLIENT_SECRET=$CLIENT_SECRET
</span></span></span><span class="line"><span class="cl"><span class="s">TOKEN_SECRET=$TOKEN_SECRET
</span></span></span><span class="line"><span class="cl"><span class="s">EOF</span></span></span></code></pre></div></div>
<p>The &ldquo;generate then write&rdquo; pattern is correct for first install. But on rerun, it completely ignores the existing <code>.env</code>.</p>
<p>It&rsquo;s the classic mistake: the author (me) tested the script <strong>only on a fresh host</strong>. The &ldquo;reinstall on existing host&rdquo; mode was never tested.</p>
<h2 id="bug-reproduction">Bug reproduction</h2>
<div class="code-block code-line-numbers open" data-start="0">
    <div class="code-header language-bash">
        <span class="code-title"><i class="arrow fas fa-angle-right" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h" aria-hidden="true"></i></span>
        <span class="copy" title="Copy to clipboard"><i class="far fa-copy" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">$ sudo ./install.sh
</span></span><span class="line"><span class="cl"><span class="o">[</span>OK<span class="o">]</span> Installation <span class="nb">complete</span>
</span></span><span class="line"><span class="cl">$ cat /etc/mcp-oauth-proxy/.env <span class="p">|</span> head -1
</span></span><span class="line"><span class="cl"><span class="nv">MCP_TOKEN</span><span class="o">=</span>a7f3e9d2c4b8a1...
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">$ sudo ./install.sh
</span></span><span class="line"><span class="cl"><span class="o">[</span>OK<span class="o">]</span> Installation <span class="nb">complete</span>
</span></span><span class="line"><span class="cl">$ cat /etc/mcp-oauth-proxy/.env <span class="p">|</span> head -1
</span></span><span class="line"><span class="cl"><span class="nv">MCP_TOKEN</span><span class="o">=</span>8c2f6a1b9e4d7c...   ← DIFFERENT</span></span></code></pre></div></div>
<p>Reproducible test in 30 seconds. Almost comical I didn&rsquo;t catch it sooner.</p>
<h2 id="the-lesson-idempotent-is-a-contract-not-a-hope">The lesson: &ldquo;idempotent&rdquo; is a contract, not a hope</h2>
<p>An installation script must be <strong>idempotent by default</strong>. Meaning: <code>./install.sh</code> once, twice, five times → system in the same stable state.</p>
<p>I had this contract <strong>in my head</strong>. I didn&rsquo;t have it <strong>in the code</strong>.</p>
<h2 id="the-fix-pre-flight-checks--explicit-force-flag">The fix: pre-flight checks + explicit force flag</h2>]]></description></item><item><title>Post-mortem: 3 MCP timeouts — IPAddressDeny + Cloudflare + NFS</title><link>https://www.arleo.eu/en/posts/postmortem-mcp-timeouts-cloudflare/</link><pubDate>Sat, 09 May 2026 13:04:20 +0200</pubDate><author>Jmr</author><guid>https://www.arleo.eu/en/posts/postmortem-mcp-timeouts-cloudflare/</guid><description><![CDATA[<div class="featured-image">
                <img src="/images/postmortem-mcp-timeouts-cloudflare-featured.jpg" referrerpolicy="no-referrer">
            </div><h2 id="context">Context</h2>
<p>I deployed a Hugo MCP Server (FastAPI, 7 tools) that lets me edit arleo.eu from Claude.ai. Architecture: <code>claude.ai → mcp-oauth-proxy NUC → hugo-mcp-proxy NUC → MCP server VM</code>.</p>]]></description></item><item><title>Strategy 4: separating content (MCP) from structure (Git)</title><link>https://www.arleo.eu/en/posts/strategie-4-mcp-vs-git/</link><pubDate>Sat, 09 May 2026 13:03:25 +0200</pubDate><author>Jmr</author><guid>https://www.arleo.eu/en/posts/strategie-4-mcp-vs-git/</guid><description><![CDATA[<div class="featured-image">
                <img src="/images/strategie-4-mcp-vs-git-featured.jpg" referrerpolicy="no-referrer">
            </div><h2 id="the-problem">The problem</h2>
<p>You have a Hugo site. You want to:</p>
<ol>
<li><strong>Edit content via Claude.ai</strong> (publish an article, fix a typo, update a draft) without touching an SSH terminal.</li>
<li><strong>Version the structure</strong> (layouts, themes, hugo.toml, deploy scripts) in Git, like a serious dev.</li>
</ol>
<p>First instinct: &ldquo;everything in Git&rdquo;. Articles too. The MCP commits, pushes, GitHub webhook triggers a rebuild. Clean, GitOps-philosophy.</p>
<p>Except it doesn&rsquo;t work that well. Here&rsquo;s why, and the simple solution I call <strong>Strategy 4</strong>.</p>
<h2 id="why-everything-in-git-breaks-in-practice">Why &ldquo;everything in Git&rdquo; breaks in practice</h2>
<p>Imagine your MCP <code>git commit</code>s on every <code>create_page</code>. Naive strategy, often suggested. Here are the problems:</p>
<h3 id="problem-1--mcp--git-conflict">Problem 1 — MCP ↔ Git conflict</h3>
<p>You push a new layout from your laptop (<code>layouts/index.html</code> modified). At the same time, the MCP is committing a new article version. Race condition: the MCP might <code>git pull --rebase</code> and fail, or worse, overwrite your local commit.</p>
<h3 id="problem-2--mcps-git-identity">Problem 2 — MCP&rsquo;s Git identity</h3>
<p>Whose commits is the MCP making? With which GPG key? If you have a &ldquo;signed commits required&rdquo; policy, the MCP needs to manage a GPG key, which has to be secured, rotated, etc.</p>
<h3 id="problem-3--unwanted-auto-commits">Problem 3 — Unwanted auto-commits</h3>
<p>You&rsquo;re testing, you create a draft article to experiment, you delete it. But the MCP already committed. Now you have a &ldquo;wip test&rdquo; commit in history, to rebase or squash manually.</p>
<h3 id="problem-4--asymmetric-reversibility">Problem 4 — Asymmetric reversibility</h3>
<p>A <code>git revert</code> repo-side has no effect on files the MCP already created. You end up with a desynchronized repo and filesystem state.</p>
<h2 id="strategy-4-separate-the-zones">Strategy 4: separate the zones</h2>
<p>The idea: <strong>MCP and Git never write to the same files</strong>.</p>
<table>
  <thead>
      <tr>
          <th>Zone</th>
          <th>Who edits</th>
          <th>Versioned in Git?</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>content/**/*.md</code></td>
          <td><strong>MCP only</strong></td>
          <td>❌ NO (<code>.gitignore</code>)</td>
      </tr>
      <tr>
          <td><code>layouts/</code>, <code>themes/</code>, <code>static/</code>, <code>hugo.toml</code>, <code>deploy.sh</code></td>
          <td><strong>Git push only</strong></td>
          <td>✅ YES</td>
      </tr>
  </tbody>
</table>
<p>Repo-side <code>.gitignore</code>:</p>
<div class="code-block code-line-numbers open" data-start="0">
    <div class="code-header language-">
        <span class="code-title"><i class="arrow fas fa-angle-right" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h" aria-hidden="true"></i></span>
        <span class="copy" title="Copy to clipboard"><i class="far fa-copy" aria-hidden="true"></i></span>
    </div><pre tabindex="0"><code>content/
public/
resources/</code></pre></div>
<p>Implications:</p>
<ul>
<li>The MCP can write to <code>content/</code> whenever. No Git conflict possible.</li>
<li>You can <code>git reset --hard</code> repo-side with confidence — <code>content/</code> stays intact.</li>
<li>No need for the MCP to manage a Git identity.</li>
<li>No &ldquo;commit pollution&rdquo;.</li>
</ul>
<h2 id="trade-off-no-content-versioning">Trade-off: no content versioning</h2>
<p>You lose Git versioning of content. That&rsquo;s a real loss:</p>
<ul>
<li>No <code>git blame</code> on an article to see who wrote what.</li>
<li>No <code>git log content/csp-nonce/index.fr.md</code> for history.</li>
<li>No PR review for articles.</li>
</ul>
<p><strong>Mitigation</strong>: VM snapshots + daily encrypted <code>content/</code> backup to QNAP. That covers <strong>disaster recovery</strong>, but not fine-grained versioning (who-changed-what-when).</p>
<p>For my personal homelab (single author, technical articles, no editorial validation workflow), it&rsquo;s an acceptable trade-off. For a 10-contributor team blog, I&rsquo;d reconsider.</p>
<h2 id="final-architecture">Final architecture</h2>]]></description></item><item><title>Hugo MCP Server: Connecting Claude.ai to a Static Hugo Site</title><link>https://www.arleo.eu/en/posts/hugo-mcp-server/</link><pubDate>Sun, 03 May 2026 19:00:00 +0200</pubDate><author>Jmr</author><guid>https://www.arleo.eu/en/posts/hugo-mcp-server/</guid><description><![CDATA[<div class="featured-image">
                <img src="/images/hugo-mcp-server-featured.jpg" referrerpolicy="no-referrer">
            </div><h2 id="-in-short">⚡ In short</h2>
<p>Connect Claude.ai to a <strong>Hugo</strong> site hosted in a KVM VM in 30 minutes: a FastAPI server exposes 6 MCP tools (read, create, modify, delete pages, rebuild the site) via JSON-RPC 2.0, an OAuth proxy reuses existing infrastructure, and every modification automatically triggers a Hugo rebuild + Cloudflare cache purge.</p>
<p>The code is available on GitHub:</p>
<ul>
<li>🔌 Hugo MCP Server: <a href="https://github.com/jmrGrav/hugo-mcp" target="_blank" rel="noopener noreffer ">jmrGrav/hugo-mcp</a></li>
<li>🔐 OAuth Proxy: <a href="https://github.com/jmrGrav/mcp-oauth-proxy" target="_blank" rel="noopener noreffer ">jmrGrav/mcp-oauth-proxy</a></li>
</ul>
<h2 id="-why">🧠 Why</h2>
<p>Anthropic&rsquo;s <a href="https://modelcontextprotocol.io/" target="_blank" rel="noopener noreffer ">MCP (Model Context Protocol)</a> allows Claude.ai to connect to external data sources via standardized tools. Unlike Grav CMS which is dynamic (PHP), Hugo generates pure static HTML — making content management via MCP even more powerful: every modification is compiled and deployed instantly.</p>]]></description></item></channel></rss>